How to create a minimum Deno 2.0 Docker image
With the release of Deno 2.0, I took the opportunity to experiment with the deno compile feature and its potential in combination with Docker.
This feature enables you to compile your script into a self-contained binary, allowing users to run your program without needing to have Deno installed.
I build a basic "Hello, World!"
server using the oak server that listens on port 8000:
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
router.get("/", (ctx) => {
ctx.response.body = "Hello world";
});
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
app.listen({
port: 8000,
});
app.addEventListener("listen", ({ hostname, port, secure }) => {
console.log(
`Listening on: ${secure ? "https://" : "http://"}${
hostname ?? "localhost"
}:${port}`,
);
});
Using the Deno 2 image
Here’s how the Dockerfile looks using the official Deno 2 image:
# Use the official Deno image from the Docker Hub
FROM denoland/deno:alpine-2.0.4
# Set the working directory
WORKDIR /app
# Copy the application code
COPY main.ts deno.json deno.lock ./
# Install dependencies
RUN deno install
EXPOSE 8000
# Set the command to run the main.ts file
CMD ["deno", "--allow-net", "main.ts"]
Next, let’s build the image and inspect the image list:
❯ docker build -t deno-classic:latest .
❯ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
deno-classic latest 5c74fb50eecd 2 seconds ago 231MB
Compiling with Deno
The next step is to build a binary and use a smaller base image for the final build. Drawing from my experience with Go, I created the following Dockerfile:
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} denoland/deno:2.0.4 AS builder
WORKDIR /deno-app
# Copy the application code
COPY main.ts deno.json deno.lock ./
# Compile the binary
RUN deno compile --output /app/main --allow-net main.ts
# Stage 2: Run
FROM scratch
COPY --from=builder /app/main /
EXPOSE 8000
ENTRYPOINT ["/main"]
However, attempting to run this image resulted in the following error:
❯ docker run -it --rm -p 8000:8000 deno-standalone
exec /main: no such file or directory
Upon closer inspection, it became clear that the binary wasn’t statically compiled:
ldd ./main
linux-vdso.so.1 (0x0000ffffaed7f000)
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 (0x0000ffffaed20000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffffaece0000)
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffffaecb0000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffffa9d60000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffffa9bb0000)
/lib/ld-linux-aarch64.so.1 (0x0000ffffaed42000)
This indicates that the binary cannot be used with the scratch Docker image. As of this writing, there is currently no method to produce a statically linked binary with Deno.
With help from the community, I managed to run it successfully using a smaller image variant based on Distroless from Google: gcr.io/distroless/cc.
❯ docker build -t deno-standalone:latest .
❯ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
deno-standalone latest df29ed80c548 4 seconds ago 169MB
deno-classic latest 5c74fb50eecd About an hour ago 231MB
❯ docker run -it --rm -p 8000:8000 deno-standalone
Listening on: http://0.0.0.0:8000
This approach results in a 62MB reduction—an impressive optimization!
Of course, I would recommend using such a minimal image for production, as it lacks many features and utilities, including a shell.
Hope that helps !