Containers·7 min read·

Dockerfile Best Practices for Smaller and Safer Images

Pin base images, merge RUN layers, drop build tooling, and switch to a non-root user to ship Dockerfiles that produce smaller, safer images on rebuild.

NB

Netbay Engineering

Netbay Engineering

On this page

The gap between a Dockerfile that builds and a Dockerfile that is production-ready shows up in two distinct places: the size of the finished image and what it is allowed to do while running. A 1.4 GB image that executes everything as root keeps server administrators awake at night, because every one of those megabytes is potential attack surface, and every process in the container shares the host kernel's root user namespace. The good news is that writing images well is mostly a handful of small, mechanical habits — pin the base, collapse the RUN lines, drop the build tooling, run as a non-root user — and each habit is easy to inspect in a review.

Dockerfile decisions: size and safety PIN BASE distro + version or exact digest slim variants MERGE RUN one layer per step purge apt lists no-install-recommends DROP TOOLING compilers stay out multi-stage later no dev packages NON-ROOT USER 10001 no setuid bins read-only later Four choices that compound: smaller to pull, safer to run

Pin Your Base Image to Something Reproducible

A build that succeeds today should produce the same image next month. Floating tags such as alpine or ubuntu:latest move under you on every rebuild, which means a one-line upstream change can silently swap the libraries your application was tested against. Pin the distribution and the major version explicitly, and pin to a digest for anything where bit-for-bit reproducibility matters. For most services a slim variant — debian:bookworm-slim or alpine:3.21 — is the right balance of tooling and footprint.

dockerfile
# Reproducible: explicit version tag, not a moving target
FROM debian:bookworm-slim

# Metadata survives into the final image and helps audits
LABEL org.opencontainers.image.source="https://git.example.org/team/server"
LABEL org.opencontainers.image.revision="4f2b9c1"

If an image pulls fine but behaves differently across machines, the first suspect is an unpinned base image. Digests are ugly to read, which is exactly why scripted builds should compute them once, pin them in the Dockerfile, and let tooling (like Renovate or Dependabot) bump them deliberately.

Combine RUN Lines to Merge Layers

Every instruction that can create a layer — RUN, COPY, ADD — adds a stored unit to the image. A chain of five separate RUN lines means five layers and five copies of intermediate junk. The classic pattern is to pair an update and an install with a cleanup of the package lists inside a single RUN, so the apt cache never reaches an earlier layer where later deletes cannot remove it.

dockerfile
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        tzdata && \
    rm -rf /var/lib/apt/lists/*

Two details matter here beyond the merging. --no-install-recommends stops apt from dragging in documentation and extra libraries that bloat the image and enlarge the update window. The rm -rf /var/lib/apt/lists/* collapses the index files into the same layer, so they never survive build time as dead weight.

Order COPY Instructions by How Often They Change

Layer caching is why file ordering matters. Docker starts from the first instruction whose context changed and re-executes everything after it. Put the rarely-changing inputs — dependency manifests like package.json or requirements.txt — before the code that changes on every commit. A rebuild then reuses the dependency layer and only re-copies the application, which turns ten-minute installs into seconds on a warm cache.

dockerfile
FROM node:22-alpine
WORKDIR /app

# Changes rarely: this layer survives most rebuilds
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Changes constantly: re-copied every build
COPY . .
CMD ["node", "src/server.js"]

The same principle applies in reverse for security scanning: keep secrets and credentials out of any COPY layer by using .dockerignore, because layers are reachable even when later layers delete them.

Run as a Non-Root User

Most images default to root, and that is the single worst habitable condition for a container that faces untrusted input. A root process that escapes the container's setup is still root inside it, and it can start poking at host resources with fewer barriers. Create a dedicated system user, chown the application files to it, and switch with USER before the final entrypoint.

A Complete, Safer Dockerfile

dockerfile
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
    apt-get install -y --no-install-recommends ca-certificates && \
    rm -rf /var/lib/apt/lists/*
RUN groupadd --system app && useradd --system --gid app --no-create-home app
COPY --chown=app:app dist/ /app/
WORKDIR /app
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -q -O - http://127.0.0.1:8080/health || exit 1
ENTRYPOINT ["/app/server"]

The file ends in exec-form ENTRYPOINT (a JSON array, no shell wrapper) so signals reach the process directly, and a HEALTHCHECK so the orchestrator knows when the service is actually ready. If the application absolutely needs to bind a port below 1024 or write to a root-owned path, that is a design smell worth fixing in the service, not a reason to fall back to root.

Takeaway

Small scale and safe identity are the two properties of a good Dockerfile, and both come from mechanical rules you can enforce in review: pin the base, squash the RUN steps, order COPY by change frequency, and drop privileges at the end. Combined they produce images that pull faster, scan cleaner, and survive an exploit attempt without giving up the host on the first try. On a Netbay VPS you get a bare Ubuntu or Rocky host with root and Docker-ready disk in under a minute — a perfect place to adopt these rules from day one on the way to netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance