Docker Practices That Held Up After Six Months

Updated 2026-04-06|SPUNK13 LLC|spunk.bet

Six months of daily Docker use across a handful of services separates the advice that pays from the advice that gets repeated. Here is which was which.

Held up: ordering the Dockerfile for cache

Copy the dependency manifest, install, then copy the source. It sounds trivial and it is the single largest build-time factor:

COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .

Without the split, every one-character source change reinstalls everything. With it plus a BuildKit cache mount, rebuilds went from around a minute to a few seconds. Six months in, this is still the change with the highest return.

Held up: multi-stage builds and small bases

Building in a full image and copying only the artifact into a slim or distroless base took images from several hundred megabytes to tens. The benefit is felt on every pull — CI jobs, autoscaling, new nodes — not on disk. The caveat learned the hard way: Alpine's musl libc breaks some native modules and can change DNS resolution behaviour, so -slim Debian variants were the safer default when anything compiled was involved.

Held up: running as a non-root user

Adding a USER instruction takes two lines and closes a real gap. The friction is predictable — a volume mounted from the host arrives owned by root and the process cannot write to it. The fix is to set ownership in the Dockerfile with chown during the copy (COPY --chown=app:app) and to match the UID to the host user in development.

Held up: healthchecks, once they were honest

A HEALTHCHECK that hits a real endpoint is what lets an orchestrator distinguish a running container from a working one. The mistake made first time round was a health endpoint that returned 200 unconditionally, which is worse than no healthcheck because it produces confident wrong answers. Checking the database connection inside the handler made it useful — and made one restart loop visible that had previously just been slow requests.

Held up: docker compose watch

Replacing hand-rolled bind mounts with compose watch and a develop.watch block gave file sync and rebuild-on-dependency-change without the permission problems bind mounts cause across operating systems. Small feature, noticeably less friction.

Cost more than it saved: chasing the smallest possible image

Getting an image from 120 MB to 90 MB consumed an afternoon and produced a container with no shell, which then made debugging a production issue considerably harder. Below roughly 150 MB the pull time difference is negligible. The effort is better spent on caching and on scanning the base image, where the returns keep coming.

Cost more than it saved: over-engineering compose files

Layered override files, profiles and YAML anchors across four compose files looked elegant and became the thing nobody could reason about. Two files — one for development, one closer to production — covered every real case with less confusion.

The habits that stuck

Part of the SPUNK13 network

Visit spunk.bet400+ Free ToolsFree Casino
Dev ToolsCasinoMemesPredictionsNFT ArtOrdinalsEbooksAdvertise