Skip to main content

Dockerfile Generator

Generate a multi-stage Dockerfile for Node, Python, Go or Java, with a non-root user, layer caching and a .dockerignore that matches.

Your data never leaves the browserUpdated: July 2026
Package manager

For example 20-alpine or 3.12-slim. Leave empty to emit no build step.

Options
Multi-stage
Non-root user
Healthcheck

Manifests are copied before the sources, so a one-line change does not reinstall every dependency. A multi-stage build leaves the compiler and the build cache out of the final image.

Key Takeaways

  • Copying sources before installing dependencies makes a one-line edit reinstall the entire dependency tree; this generator always copies manifests first.
  • In a multi-stage build the final image carries no compiler, no package cache and no source tree: Go runs on alpine:3.20, Java on eclipse-temurin:21-jre.
  • Containers run as uid 0 unless told otherwise. The generator reuses the built-in node account on Node images and creates a real uid 1001 user everywhere else.
  • Without a .dockerignore, node_modules and .git are shipped into the build context, which both slows the build and defeats layer caching.

The 1.2 GB image that should have been 90 MB

Writing a Dockerfile that works is easy: FROM, COPY . ., RUN npm install, CMD. The trouble starts at docker images, where a small Node service turns out to weigh 1.2 GB and contains a compiler toolchain, an npm cache, the test suite, the .git directory and two copies of node_modules. The same service written carefully lands around 90 MB. The difference is not what the image needs, it is what nobody removed.

Layer caching and copy order

Every instruction produces a layer, and Docker skips a layer whose inputs have not changed. When one layer is invalidated, every layer after it is invalidated too. Those two sentences explain the entire difference between the following files.

dockerfile
# WRONG: sources copied firstFROM node:20-alpineWORKDIR /appCOPY . .RUN npm ci          # re-runs on every source editCMD ["node", "dist/index.js"] # RIGHT: manifests firstFROM node:20-alpineWORKDIR /appCOPY package.json package-lock.json* ./RUN npm ci          # re-runs only when the lock file changesCOPY . .CMD ["node", "dist/index.js"]

In the first file, fixing a typo changes the COPY . . layer, so npm ci runs again and the build takes minutes. In the second, the same typo touches only the last layer and the build finishes in seconds. Note the trailing asterisks on the lock files: COPY fails hard on a literal missing path, and no real repository has all three lock files at once, so globbing them is what keeps the template working across npm, yarn and pnpm projects.

Multi-stage builds

A multi-stage file has three FROM lines: deps resolves dependencies, build compiles sources on top of that cached layer, and runtime exists purely to run the result. Only what survives into the runtime stage ships. The saving is largest for languages that need a compiler at build time and nothing at run time.

RuntimeBuild baseRuntime baseLeft behind
Nodenode:20-alpinenode:20-alpineSource tree, build cache
Gogolang:...alpine:3.20The entire Go toolchain
Javamaven:...eclipse-temurin:21-jreJDK and Maven
Pythonpython:...python:...Build-time compilation tooling
PHPphp:...php:...Composer dev dependencies
Go uses alpine rather than scratch, because scratch has no CA bundle and no shell.

Go is where the ratio is most extreme: a statically linked binary needs no toolchain at run time, so an 800 MB golang image becomes an 8 MB alpine one. The JDK-to-JRE swap on the Java side is the same idea with less dramatic numbers.

Running as non-root, and .dockerignore

A container runs as uid 0 unless you say otherwise, so any code execution flaw in your application is root inside the container, and combined with a container escape it becomes a host problem. With the non-root option on, the generator does the job properly rather than emitting a decorative USER line: official Node images ship an unprivileged node account, so it reuses that; everything else gets a real uid 1001 user owning the working directory. Alpine and Debian take different adduser flags, so the tag is sniffed for "alpine" first.

The .dockerignore is the file most often skipped. Without it, docker build uploads the entire directory to the daemon: node_modules, the whole .git history, .env files, coverage reports. That slows the very first step and then lets COPY . . drag the same junk into the image. The generator emits a .dockerignore alongside the Dockerfile, and the **/.env pattern keeps environment files out of the build context entirely.

CMD versus ENTRYPOINT

CMD sets the default command and is replaced wholesale when you run docker run image some-other-command. ENTRYPOINT stays fixed, and anything on the command line is appended to it as arguments. For an image that runs one application server, CMD is the right choice. For an image people invoke like a CLI tool, ENTRYPOINT with the binary plus CMD for default flags reads better.

Warning

Avoid the shell form CMD node dist/index.js. It wraps your process in /bin/sh -c, and sh does not forward SIGTERM, so every docker stop waits out the full ten second kill timeout before the container dies hard. The generator always emits exec form, CMD ["node", "dist/index.js"], so signals reach your process directly.

Frequently Asked Questions

Why is my image so large?
Three usual causes. Build context: with no .dockerignore, node_modules and .git end up inside. Single-stage layout: the compiler, the package cache and the source tree all stay in the final image. Base image choice: node:20 and node:20-alpine differ by several hundred megabytes. Run docker history <image> and start with the biggest layer.
Why does the build cache never hit?
Almost always copy order. If COPY . . precedes the install step, every source edit invalidates that layer and everything after it re-runs. Copy the manifests separately and install immediately afterwards. The second cause is an ARG carrying a timestamp or a random value, which changes the layer input on every single build by definition.
Alpine or slim?
Alpine uses musl libc rather than glibc. For pure JavaScript or Go that is a non-issue and the image is noticeably smaller. For Python packages with native extensions (numpy, pandas, psycopg2) there are often no musl wheels, so pip compiles from source: the build takes far longer and can end up larger than slim. Slim is the sensible default for Python, alpine for Node and Go.
Do I need multi-stage for a small project?
With no build step the gain is modest and a single stage is defensible, for instance a small Python service that runs its sources directly. As soon as a build command exists the difference shows immediately, because the compiler, the bundler and every dev dependency stay behind. For Go it is worth doing at any size, since the ratio is roughly 800 MB against 15 MB.
Why does my container exit immediately?
Docker stops the container when the process running as PID 1 exits. If your command starts something in the background and returns, the container returns with it, so always run in the foreground. The other frequent cause is a CMD path that does not exist in the image: run docker run --rm -it <image> sh and look. In a multi-stage build that usually means the build output was never copied into the runtime stage.