Deploying Racket Web Apps With Docker

Since there was a question about deploying Racket code using Docker on the Racket Reddit this morning, I figured I'd write a quick follow-up to my post about Deploying Racket Web Apps. My preference is still to avoid using Docker and just use the method described in that post by default. But, if I have to use Docker for some reason, then I'll typically use a two-stage build to build a distribution in the first stage and then copy that distribution into the second stage in order to get a minimal Docker image that I can easily ship around.

Given the following app, saved as app.rkt:

#lang racket/base

(require racket/async-channel
         web-server/http
         web-server/servlet-dispatch
         web-server/web-server)

(define ch (make-async-channel))
(define stop
  (serve
   #:dispatch (dispatch/servlet
               (lambda (_req)
                 (response/xexpr
                  '(h1 "Hello!"))))
   #:port 8000
   #:listen-ip "0.0.0.0"
   #:confirmation-channel ch))

(define ready-or-exn (sync ch))
(when (exn:fail? ready-or-exn)
  (raise ready-or-exn))

(with-handlers ([exn:break? void])
  (sync/enable-break never-evt))

(stop)

I would write the following Dockerfile:

FROM racket/racket:8.9-full AS build

COPY app.rkt /code/
WORKDIR /code
RUN raco exe -o app app.rkt
RUN raco dist dist app

FROM debian:bullseye-slim AS final

RUN apt-get update -y && \
    apt-get install -y --no-install-recommends dumb-init && \
    rm -rf /var/lib/apt/lists/*

COPY --from=build /code/dist /app
CMD ["dumb-init", "/app/bin/app"]

When this image gets built, the build stage creates a distribution of the app that gets copied into the final stage. At the end, the build stage is discarded and the end result is a roughly 150MB Docker image with just my code in it and a minimal Debian system. Not quite as minimal as you can get out of using a similar method with Go, but Racket distributions have a high baseline, so a real app wouldn't be much bigger than this.