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.