Dockerfile Generator
Pick a stack, choose the options, get a commented multi-stage Dockerfile — entirely on this device.
Pick a stack, set the options, press Generate — the commented Dockerfile and its .dockerignore appear here.
Press ? for shortcuts
Do it offline:$ docker build -t myapp .
How to use it
- Choose the stack, and where one applies, the package manager — npm, pnpm or Yarn for Node; pip or uv for Python; Maven or Gradle for Java.
- Set the image options: multi-stage on or off, the non-root user, an optional health probe, and the port your app listens on. Override the pinned base tag only when you need a different line.
- Fill in the app fields — the start command for Node and Python, and any baked-in environment defaults. Treat those pairs as configuration, never credentials: secrets belong in docker run -e.
- Press Generate, read the comments top to bottom, then copy or download the Dockerfile and .dockerignore panes as two files.
Pick a stack — Node, Python, Go, Java, Rust or a static site — choose your options, and press Generate for a Dockerfile where every single instruction carries a comment saying what it does and why it is there, plus the matching .dockerignore. Base tags are pinned, builds are multi-stage by default, and the container runs as a numeric non-root user. Everything is produced on this device; the values you type never leave the page.
Examples
Node.js service — multi-stage (npm)# syntax=docker/dockerfile:1
# Dockerfile — Node.js (npm), multi-stage. Every instruction is commented; reference: https://docs.docker.com/reference/dockerfile/
# Build stage — node:22-alpine pins an LTS line on Alpine — a small base whose tag moves only within 22.x; dev dependencies live and die here.
FROM node:22-alpine AS build
# Every later instruction runs relative to /app; Docker creates it if missing.
WORKDIR /app
# Manifests first: the install layer below is rebuilt only when these files change (layer-cache ordering).
COPY package.json package-lock.json ./
# Exact, reproducible install from the lockfile — dev dependencies included, because the build needs them.
RUN npm ci
# BuildKit-only variant of the install above, with a persistent package cache: RUN --mount=type=cache,target=/root/.npm npm ci
# The source, after the install — code edits reuse the cached dependency layer; .dockerignore keeps node_modules and .git out of the context.
COPY . .
# Produce the production build (dist/ by convention — adjust the runtime COPY below if yours differs).
RUN npm run build
# Runtime stage — starts clean from the same pinned base; only what the app needs is copied in.
FROM node:22-alpine
# The runtime app root.
WORKDIR /app
# Dedicated unprivileged account with a fixed numeric UID (10001) — numeric, so platform runAsNonRoot-style checks can verify it without resolving names.
RUN addgroup -g 10001 -S app && adduser -u 10001 -S app -G app
# Baked-in defaults — configuration only, never credentials; pass secrets at run time with docker run -e or --env-file.
ENV NODE_ENV="production"
# Manifests again for the production install below.
COPY --chown=10001:10001 package.json package-lock.json ./
# Production dependencies only — build tooling never ships.
RUN npm ci --omit=dev
# Only the built output crosses stages — source and dev dependencies stay behind.
COPY --chown=10001:10001 --from=build /app/dist ./dist
# Drop root before the process starts — every later instruction and the running container use this account.
USER 10001
# Documents the listen port for tooling — EXPOSE publishes nothing; map it at run time with -p 3000:3000.
EXPOSE 3000
# Exec-form start command — the process runs as PID 1 and receives stop signals directly. Listen on 0.0.0.0, not 127.0.0.1, to be reachable from outside the container.
CMD ["node", "dist/server.js"]
The flagship shape. Stage one installs from the lockfile and runs the build; stage two starts clean, installs with --omit=dev and copies only dist/ across, owned by UID 10001 — so the start command points into dist/. This exact artifact completed a real docker build -t myapp . and a container start during authoring.
Go service — multi-stage with a health probe# syntax=docker/dockerfile:1
# Dockerfile — Go, multi-stage. Every instruction is commented; reference: https://docs.docker.com/reference/dockerfile/
# Build stage — golang:1.23-alpine pins the toolchain; the compiled binary is all that reaches the runtime stage.
FROM golang:1.23-alpine AS build
# The module root for the build.
WORKDIR /src
# Module files first: the download layer below survives source edits (layer-cache ordering).
COPY go.mod go.sum ./
# Fetch dependencies into the module cache — rebuilt only when go.mod or go.sum changes.
RUN go mod download
# BuildKit-only variant of the install above, with a persistent package cache: RUN --mount=type=cache,target=/go/pkg/mod go mod download
# The source, after the module download.
COPY . .
# A static binary: CGO off so it runs on plain Alpine; -s -w strips debug tables from the release build.
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app .
# Runtime stage — a bare pinned Alpine; the Go toolchain stays behind.
FROM alpine:3.20
# The runtime app root.
WORKDIR /app
# The CA bundle — outbound TLS fails without it on a bare base image.
RUN apk add --no-cache ca-certificates
# Dedicated unprivileged account with a fixed numeric UID (10001) — numeric, so platform runAsNonRoot-style checks can verify it without resolving names.
RUN addgroup -g 10001 -S app && adduser -u 10001 -S app -G app
# The one artifact that ships: the compiled binary.
COPY --chown=10001:10001 --from=build /out/app ./app
# Drop root before the process starts — every later instruction and the running container use this account.
USER 10001
# Documents the listen port for tooling — EXPOSE publishes nothing; map it at run time with -p 8080:8080.
EXPOSE 8080
# Container-level liveness probe — busybox wget ships with Alpine; unhealthy after 3 failed probes.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -qO- http://127.0.0.1:8080/ || exit 1
# Exec form — the binary is PID 1 and receives signals directly (clean shutdowns).
ENTRYPOINT ["/app/app"]
The strongest case for two stages: the golang toolchain image is hundreds of megabytes, the shipped stage is Alpine plus one static binary. CGO_ENABLED=0 keeps that binary free of libc, and the probe uses busybox wget, which Alpine already has.
Python app — single-stage (pip) with env defaults# syntax=docker/dockerfile:1
# Dockerfile — Python (pip), single-stage. Every instruction is commented; reference: https://docs.docker.com/reference/dockerfile/
# Single stage — python:3.12-slim pins one interpreter line on Debian slim — prebuilt wheels install cleanly, where Alpine's musl often forces source builds.
FROM python:3.12-slim
# The app root for everything below.
WORKDIR /app
# Dedicated unprivileged account with a fixed numeric UID (10001) — numeric, so platform runAsNonRoot-style checks can verify it without resolving names.
RUN groupadd --system --gid 10001 app && useradd --system --uid 10001 --gid app app
# Baked-in defaults — configuration only, never credentials; pass secrets at run time with docker run -e or --env-file.
ENV PYTHONDONTWRITEBYTECODE="1" \
PYTHONUNBUFFERED="1" \
LOG_LEVEL="info"
# The manifest first: the install layer below is rebuilt only when it changes (layer-cache ordering).
COPY requirements.txt ./
# Dependencies into the image's interpreter — pin versions in requirements.txt for reproducible builds.
RUN pip install --no-cache-dir -r requirements.txt
# BuildKit-only variant of the install above, with a persistent package cache: RUN --mount=type=cache,target=/root/.cache/pip pip install --no-cache-dir -r requirements.txt
# The application source — .dockerignore keeps __pycache__, .venv and .git out of the context.
COPY --chown=10001:10001 . .
# Drop root before the process starts — every later instruction and the running container use this account.
USER 10001
# Documents the listen port for tooling — EXPOSE publishes nothing; map it at run time with -p 8000:8000.
EXPOSE 8000
# Liveness probe with the interpreter itself — the slim image ships no wget or curl.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/')" || exit 1
# Exec-form start command — the process runs as PID 1 and receives stop signals directly. Listen on 0.0.0.0, not 127.0.0.1, to be reachable from outside the container.
CMD ["python", "main.py"]
Single-stage is a fair choice when the runtime needs the whole source tree anyway. The slim variant carries no wget or curl, so the probe is a one-line urllib call, and the ENV block bakes in interpreter flags plus your own LOG_LEVEL pair.
Static site — build with npm, serve with nginx# syntax=docker/dockerfile:1
# Dockerfile — Static site (nginx), multi-stage. Every instruction is commented; reference: https://docs.docker.com/reference/dockerfile/
# Site-build stage — produces the static bundle with npm; swap this stage's toolchain if your generator isn't npm-based.
FROM node:22-alpine AS build
# The site source root.
WORKDIR /app
# Manifests first: the install layer below is rebuilt only when these files change (layer-cache ordering).
COPY package.json package-lock.json ./
# Exact, reproducible install from the lockfile.
RUN npm ci
# BuildKit-only variant of the install above, with a persistent package cache: RUN --mount=type=cache,target=/root/.npm npm ci
# The site source, after the install.
COPY . .
# Build the static bundle (dist/ by convention — adjust the COPY below if yours differs).
RUN npm run build
# Serving stage — nginx:1.27-alpine pins one server release line on Alpine — the files are served by a battle-tested default config; none of the build toolchain ships.
FROM nginx:1.27-alpine
# The built bundle into nginx's stock web root.
COPY --from=build /app/dist /usr/share/nginx/html
# USER is deliberately not set: the official nginx image starts as root to bind its listen port, then drops privileges — its worker processes run as the unprivileged nginx user.
# Documents the listen port — EXPOSE publishes nothing, and the stock nginx config listens on 80; ship your own nginx.conf to change that.
EXPOSE 80
# No CMD needed — the base image already starts nginx in the foreground.
The bundle is built in a node stage and handed to nginx, whose stock config serves it on 80. Non-root works differently here: the official server image manages its own privilege drop, so the file explains that in place of a USER instruction.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Order layers by how often they change
Docker rebuilds every layer below the first one whose inputs changed, which is why the generated files copy manifests alone, install, and only then copy source: a code edit re-runs one COPY instead of a full dependency install. Putting COPY . . before the install is the most expensive ordering mistake a Dockerfile can make.
The .dockerignore does its work before COPY runs
The build context is packed and sent to the builder first; only then do COPY instructions read from it. A missing .dockerignore means .git, node_modules or target/ travel on every build and a careless COPY . . can bake them in — including .env files. That is why the ignore file ships alongside every Dockerfile here.
USER 10001 where USER app would do
Both drop root, but a name is only meaningful inside the image's own /etc/passwd. Admission checks such as Kubernetes' runAsNonRoot can verify a numeric UID without trusting that file, so a named USER can be rejected where a numeric one passes. The generated account pins UID 10001 and hands file ownership over with COPY --chown.
Common errors
Port 70000 is not usable — a TCP port is a whole number from 1 to 65535.- Ports are 16-bit: 65535 is the ceiling. Check the framework's real default — the field documents what the app actually listens on.
The Node.js stack needs a start command — without one the image cannot know how to launch your app (for example: node dist/server.js).- Compiled stacks ship a binary the file can point at, but an interpreter needs to be told the entry script. Give the command you run locally in production mode.
Base image "node:22 alpine" is not a valid image reference — expected name[:tag][@sha256:<64 hex>], like node:22-alpine.- A space split what should be one reference — variant names are joined to the version with a hyphen. Leave the field empty to keep the stack's pinned default.
Environment name "2FA_MODE" is invalid — a name is letters, digits and underscores, and cannot start with a digit (the POSIX shell-name rule).- The rule comes from the shell, not from Docker: a leading digit would be unreadable as $2FA_MODE. Prefix it — APP_2FA_MODE works everywhere.
"node:latest" pins nothing — :latest is a floating tag, so the image changes under you without a code change. Pin a version tag like node:22-alpine.- This one is a warning, not a refusal — the file is generated anyway. Floating tags make builds unrepeatable and rollbacks meaningless, which is why every default here is version-pinned.
FAQ
Do I always need a multi-stage build?
No — it pays whenever building needs tools that running does not: compilers, dev dependencies, bundlers. For compiled stacks the size difference is dramatic, and secrets or source that existed only in the build stage never reach the shipped image. An interpreted app running from its full source tree loses little by staying single-stage.
How do I get secrets into the container?
At run time, not in the image: docker run -e KEY=value, --env-file .env, or your orchestrator's secret store. Anything written into a Dockerfile — an ENV default, an ARG, a copied file — is readable from the image history by anyone who can pull it. That is why the environment rows here are labelled configuration and why the emitted .dockerignore excludes .env files.
Can this page produce a devcontainer.json too?
No — that is a different artifact with different semantics: a devcontainer.json describes an editor's development environment and may reference a Dockerfile, while this page generates the image recipe itself, aimed at running your app. The files here can serve as the build side of a dev container, but the JSON wrapper is out of scope.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Generate the files
- ⌘/Ctrl + S
- Download the active pane's file
- ⌘/Ctrl + C
- Copy the active pane when no text is selected
- ⌘/Ctrl + Shift + C
- Copy even while text is selected
- t
- Switch the theme
- ?
- Open the shortcut help
- Esc
- Dismiss dialogs and transient state