Hi, I'm James

The Docker downloader pattern

Fetch-and-copy is way better than `apt install`

DevOps Docker

The package manager way

Instead of running apt install ... in your Dockerfile, download pre-built binaries. It’s faster, easier to cache, and easier to troubleshoot when network stuff goes wrong.

For example, for this blog, I had always added Hugo with apt in the build step. Something like:

FROM node:25-bookworm-slim AS build
ARG HUGO_VERSION=0.153.2
RUN sudo apt update && sudo apt install hugo=${HUGO_VERSION}
WORKDIR /app
COPY . /app
RUN hugo --gc --minify
# ...

This works. It also takes (what feels like) an eternity when starting without a layer cache.

The fetch-and-copy way

Now I just download the binary with the curl image and copy the hugo binary to the final image:

FROM curl:latest AS downloader
ARG HUGO_VERSION=0.153.2
ARG HUGO_CHECKSUM=e16e356bfcd549ab9838dee87b24b7f7b14d65b880a6b9f2d5509a4304487eb1
WORKDIR /deps
RUN curl -L \
    "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" \
    -o hugo.tar.gz \
    && echo "${HUGO_CHECKSUM}  hugo.tar.gz" | sha256sum -c - \
    && tar -xzf hugo.tar.gz hugo

FROM node:25-bookworm-slim AS build
COPY --from=downloader /deps/hugo /usr/local/bin/hugo
WORKDIR /app
COPY . /app
RUN hugo --gc --minify
# ...

This also works. And it’s blazing fast when starting without a layer cache (e.g., when standing up a new branch deployment).

Here’s a short list of tools that I’m now installing this way: