Skip to content

Slim the Docker image: multi-stage build with a slim runtime - #58

Merged
jrodal98 merged 1 commit into
masterfrom
chore/slim-docker-image
Oct 18, 2026
Merged

jrodal98 merged 1 commit into
masterfrom
chore/slim-docker-image

Conversation

@jrodal98

Copy link
Copy Markdown
Owner

The image was over 300MB because the Dockerfile was a single stage on rust:latest: it built the app inside the image and then ran it from that same image, so the published image carried the entire Rust toolchain, the cargo registry cache, and all intermediate build artifacts.

The change (Dockerfile only)

Two stages:

  • Builder (rust:1-bookworm): the usual cargo build --release, plus strip on the binary.
  • Runtime (debian:bookworm-slim): only three things — the stripped binary (/usr/local/bin/brunnylol), static/, and ca-certificates (the AI feature makes outbound HTTPS calls). Same env (BRUNNYLOL_PORT, BRUNNYLOL_DB=/data/...), same exposed port, so the existing run/compose setup is untouched.

Two details that matter:

  • static/ is copied from the builder stage: build.rs generates the favicon PNGs into it during the build, and those are gitignored, so the build-context copy of static/ in a fresh CI checkout doesn't have them.
  • Builder and runtime are both bookworm, so the binary's glibc always matches the runtime's.

Expected size

Measured locally: the release binary is 15.3MB (11.4MB stripped) and links only glibc basics — SQLite is compiled in — and static/ is 100KB. On bookworm-slim (~74MB) the final image should land around 85MB on disk / ~40MB pulled, down from 300MB+.

Verification, honestly scoped

Docker isn't available in my environment, so I could not build the image itself — this PR's Docker workflow run is the end-to-end proof, and it should be green before merging. What I did verify locally:

  • A runtime-layout simulation: a directory containing ONLY the stripped binary and static/, run with the container's env vars — it serves /, the CSS, both favicon files, and the descriptor (all 200s). Everything else the app needs (templates, migrations, commands.yml, API docs) is compiled into the binary via Askama/include_str!, confirmed by auditing the source.
  • ldd on the binary: libc/libm/libgcc only.

Rollback is a plain revert of this one file.

Copy link
Copy Markdown
Owner Author

Update: the new Dockerfile has now been built and run-verified locally (the PR description's "Docker isn't available in my environment" caveat no longer applies).

Setup, for the record: this sandbox had no container runtime, so I installed Podman. Registry pulls from Go-based tooling are blocked here (the egress proxy drops Go's TLS fingerprint), so the base images were fetched with a curl-based OCI puller and loaded into Podman; the build ran with the vfs storage driver and chroot isolation (the VM also lacks /dev/fuse and namespace privileges). The only build-invocation accommodation was pointing cargo at this sandbox's proxy CA bundle — the Dockerfile itself is byte-for-byte the one in this PR; CI won't need any of that.

Results:

  • podman build of this exact Dockerfile: success, both stages (cargo release build + strip; apt ca-certificates; both COPY --from=builder steps).
  • Final image size: 98.5 MB (bookworm-slim 77.9MB + 11.4MB stripped binary + ca-certificates + static), down from 300MB+.
  • Execution: podman run can't create network namespaces in this VM, so the built image's filesystem was exported and the app was run from it via chroot with the image's own env and WORKDIR — it boots, seeds, and serves /, /static/css/main.css, /static/favicon.svg, /static/favicon-64.png, and /opensearch.xml (all 200). That last-but-one matters: the favicon PNGs exist in the runtime image only because build.rs generated them in the builder stage and static/ was copied from there — the full chain works.

@jrodal98
jrodal98 merged commit 55157c8 into master Oct 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant