Create the smallest Crystal-lang docker image based on scratch


The official Crystal-lang Docker image is Ubuntu-based and relatively large. However, production-ready images can be significantly smaller by utilizing Docker multi-stage builds. The smallest possible image can be created using the scratch base image. If some pre-execution processing or shell utilities are needed, BusyBox or Alpine Linux are recommended alternatives.

Dockerfile Example

To create a minimal image, compile the application using the official Crystal-lang image in an initial stage, and then copy the resulting binary (and any necessary dynamic libraries) to a scratch image in a subsequent stage, as shown below:

# vim:set ft=dockerfile:
FROM crystallang/crystal:0.31.1 as builder

LABEL maintainer="Andrius Kairiukstis <****>"

WORKDIR /src
COPY . .

RUN shards build --production --progress --verbose --warnings=all
# Identify and copy runtime dependencies
RUN ldd ./bin/app | tr -s '[:blank:]' '\n' | grep '^/' | \
    xargs -I % sh -c 'mkdir -p $(dirname deps%); cp -L % deps%;' # Use -L to copy actual files for symlinks
# RUN find ./deps/

################################################################################
# Final stage: copy artifacts to scratch image
FROM scratch

LABEL maintainer="Andrius Kairiukstis <****>"

# Copy runtime dependencies. These are crucial for DNS resolution in Docker.
COPY --from=builder /src/deps/ /
# Specifically copy DNS-related libraries if not covered by the general deps copy
# Ensure these paths are correct for the builder image (crystallang/crystal:0.31.1)
COPY --from=builder /lib/x86_64-linux-gnu/libnss_dns.so.2 /lib/x86_64-linux-gnu/
COPY --from=builder /lib/x86_64-linux-gnu/libresolv.so.2 /lib/x86_64-linux-gnu/

# Copy the compiled application
COPY --from=builder /src/bin/app /app

ENTRYPOINT ["/app"]

Crystal-lang Notes on DNS Resolution

A common issue with minimal Crystal-lang Docker images (especially those based on scratch) is DNS resolution. This is documented in Crystal-lang GitHub issues such as #2426 and #6099. The most effective solution I’ve found involves copying necessary libnss_*.so and libresolv.so files from the builder image, as demonstrated in the Dockerfile above and discussed in this Gist comment.

Attempting to compile with the --static flag, even when including these DNS libraries, did not consistently resolve DNS issues in my tests. Statically linked images without these libraries are generally only suitable for services that do not require external DNS lookups (e.g., purely listening services).

Resulting Image Sizes

The resulting image sizes are significantly reduced:

  • scratch-based with ldd dependencies (DNS working): ~10MB
  • scratch-based, statically linked (DNS not working): ~6.25MB
$ docker image list dial_demo

# REPOSITORY             TAG                    IMAGE ID      CREATED         SIZE
# smallest-docker-image  scratch-ldd            d819bf2a43f3  21 minutes ago  10MB
# smallest-docker-image  scratch-static-no-dns  595afcfad6f0  13 minutes ago  6.25MB

Download Examples

Example Dockerfiles demonstrating these techniques are available in my sandbox repository on GitHub. This includes configurations for: