← Blog

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 <k@andrius.mobi>"

WORKDIR /src
COPY . .

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

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

LABEL maintainer="Andrius Kairiukstis <k@andrius.mobi>"

# Copy runtime dependencies. Crucial for DNS resolution
# in Docker.
COPY --from=builder /src/deps/ /
# Specifically copy DNS-related libraries if not covered
# by the general deps copy. Paths are 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

# (output reformatted to fit; columns abbreviated)
# REPOSITORY     TAG                    SIZE
# smallest-img   scratch-ldd            10MB
# smallest-img   scratch-static-no-dns  6.25MB

Download Examples

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