Getting a Dockerfile to build once on your laptop takes five lines. Getting one to rebuild in seconds on the fiftieth commit, ship without a compiler inside, run as a normal user, then pass an image scan takes more thought. The generator above writes the second kind for six common stacks. The sections below explain the reasoning behind its lines, so you know which ones to keep when you edit the file by hand.
What each stack gets from the generator
Every template splits the work into two stages. The first holds compilers, dev dependencies, the package cache. The second receives only the output, so none of the build tooling reaches production.
| Stack | Build stage | Runtime stage | Reason for the runtime pick |
|---|---|---|---|
| Node.js | node:22-alpine, all dependencies | node:22-alpine, production dependencies only | Same base in both stages, so native modules built in stage one load in stage two. |
| Static site | node:22-alpine | nginx:stable-alpine | Node never reaches the final image. Only built HTML, CSS, JS, plus nginx. |
| Python | python:3.13-slim with a venv | python:3.13-slim | Slim over Alpine on purpose. See the next section. |
| Go | golang:1.26-alpine | Distroless static, scratch, or Alpine | A binary built with cgo off needs no libc. The runtime holds the binary plus CA certificates. |
| Java | eclipse-temurin:21-jdk | eclipse-temurin:21-jre | The JRE drops javac plus the JDK tools. |
| PHP | composer:2 | php:8.4-fpm-alpine | Composer stays behind. FPM needs a web server container in front. |
Why Python stays on slim
Most wheels on PyPI target glibc, the C library Debian uses. Alpine ships musl. A package without a musllinux wheel compiles from source during pip install, which means installing gcc plus headers first. The build slows from seconds to minutes, the image grows, then a C extension fails on some musl difference nobody on your team has seen before. Slim skips the whole problem at roughly the same final size.
Why Java skips the openjdk image
Docker deprecated the official openjdk images in 2022, so they stopped receiving security fixes. Older tutorials still show openjdk:17-jdk-slim, which is what the previous version of this page generated too. Eclipse Temurin builds from the same OpenJDK source with regular patch releases. Amazon Corretto plus Azul Zulu work as drop-in alternatives if your company standardizes on one.
Layer order decides rebuild time
Docker caches each instruction. When the inputs of one instruction change, every instruction after it reruns. A COPY treats the content of the files it copies as its input, so a single edited source file invalidates COPY . . plus every line below.
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run buildFROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildEvery template above follows the second pattern: manifest first, install, source last. A few other things bust the cache without anyone noticing:
- The .git folder. Every commit rewrites files under .git, so
COPY . .reruns even when no source changed. The .dockerignore tab excludes the folder. - A local node_modules or .venv. Copied in after the install, it overwrites the clean one, often with binaries compiled for macOS or Windows. The container then fails with an "invalid ELF header" error.
- Early ARG values. Changing a build argument invalidates every step after its first use, so declare ARG as late as the file allows.
- Not timestamps. Docker compares file content checksums, not modification times. Touching a file without editing it keeps the cache.
Non-root without breaking file writes
Containers run as root unless the Dockerfile says otherwise. With user namespaces off, which is the default, a container escape bug then hands the attacker root on the host. Each stack switches users a different way:
- Node.js
- The official image ships a
nodeuser with uid 1000. OneUSER nodeline, no useradd. - Python, Java
- Debian plus Ubuntu bases carry no app user, so the file runs
useraddwith uid 10001. A uid above 10000 avoids matching a real account on the host when you bind-mount folders. - Go
- The distroless
:nonroottag runs as uid 65532. Scratch has no /etc/passwd, so the file sets the numeric65532:65532directly. - Static site
- Stock nginx starts as root. The
nginxinc/nginx-unprivilegedimage runs as uid 101 on port 8080, which is why the port changes when you tick non-root. - PHP
www-dataalready exists. Only storage plus bootstrap/cache get chowned to it.
The generated files never chown the whole app folder. Code owned by root but run by a normal user means a compromised process cannot rewrite its own source. Grant write access only to the folders your app writes to at run time.
Secrets stay out of ENV or ARG
Every ENV value lands in the image config. Anyone who pulls the image reads the value with docker inspect. ARG values used in a RUN step show up in docker history. Type SESSION_SECRET=abc into the variables box above to see the guard: the generator drops the line from the Dockerfile, flags the key, then adds -e SESSION_SECRET to the run command so the value comes from your shell.
Private registries during the build leak the same way. An .npmrc holding an auth token, copied in then deleted two steps later, still sits in the earlier layer. A BuildKit secret mount exposes the file to one RUN step without writing it to any layer:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
docker build --secret id=npmrc,src=$HOME/.npmrc -t your-service .A health check needs a program to run
HEALTHCHECK runs a command inside the container. If the base image lacks the command, the check fails on every attempt, so Docker reports a working app as unhealthy. Here is what each runtime image offers:
| Runtime image | Available client | Check the generator writes |
|---|---|---|
| Node, nginx, Alpine | BusyBox wget | wget -qO- against the health path |
| Python slim | Python, no curl or wget | python -c with urllib |
| Temurin JRE | Varies by release | Installs curl first, then curl -fsS |
| Distroless, scratch | Nothing | No check, plus a note on adding a flag to the binary |
| PHP-FPM | FastCGI only | No check. Probe the nginx container instead |
Kubernetes ignores HEALTHCHECK entirely. Pods use the liveness plus readiness probes from the manifest. The Dockerfile check still matters under Docker Compose, Swarm, or anything reading docker ps.
Where the generator stops
- Monorepos. The Node templates copy one package.json. npm or pnpm workspaces need every workspace manifest copied before the install, or a pruning step such as
turbo prune. - Next.js standalone output. Next writes
.next/standaloneplus.next/static, not a dist folder. Set the start file toserver.js, then edit the runtime COPY lines by hand. - Poetry. Only pip plus uv are covered. Export a requirements.txt from Poetry first, or move the project to uv.
- Tags age. The version lists reflect releases as of the date in the byline. Check Docker Hub for the current patch tag, then pin it, or pin a digest for byte-identical rebuilds.
- Nothing gets built here. The page writes text. A typo in a start file only shows up when the container starts, so run
docker buildplusdocker runlocally once before pushing to CI.