
Docker Syntax Dockerfile
- 8 installs
- 9 repo stars
- Updated July 8, 2026
- openaec-foundation/docker-claude-skill-package
Helps with devops & ci/cd tasks.
About
docker-syntax-dockerfile is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- docker-syntax-dockerfile
- DevOps & CI/CD
- AI-coding skill
Docker Syntax Dockerfile by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,044 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/docker-claude-skill-package --skill docker-syntax-dockerfileAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/docker-claude-skill-package ↗ |
What it does
Helps with devops & ci/cd tasks.
Files
docker-syntax-dockerfile
Quick Reference
Parser Directives
Parser directives MUST appear at the very top of the Dockerfile, before any instructions, blank lines, or comments.
| Directive | Purpose | Example |
|---|---|---|
# syntax=docker/dockerfile:1 | Enable BuildKit features (heredocs, mounts, etc.) | ALWAYS include this |
# escape=\` | Change escape character (useful on Windows) | Optional |
# check=error=true | Enable build-time lint checks (v1.8.0+) | Optional |
ALWAYS start every Dockerfile with # syntax=docker/dockerfile:1 to enable BuildKit extensions.
All 17 Instructions at a Glance
| Instruction | Purpose | Creates Layer? |
|---|---|---|
FROM | Set base image, start build stage | Yes (base) |
RUN | Execute command during build | Yes |
CMD | Default container command (overridable) | No (metadata) |
ENTRYPOINT | Container executable (persistent) | No (metadata) |
COPY | Copy files from context or stage | Yes |
ADD | Copy with URL download and tar extraction | Yes |
ENV | Set persistent environment variable | Yes |
ARG | Set build-time variable (not persisted) | No |
WORKDIR | Set working directory | Yes |
EXPOSE | Document container port | No (metadata) |
VOLUME | Declare mount point | No (metadata) |
USER | Set user for subsequent instructions | No (metadata) |
HEALTHCHECK | Define container health test | No (metadata) |
LABEL | Add image metadata | Yes |
SHELL | Override default shell | No (metadata) |
STOPSIGNAL | Set container stop signal | No (metadata) |
ONBUILD | Deferred instruction for child images | No (metadata) |
Critical Warnings
NEVER use latest tag in FROM -- ALWAYS pin to a specific version (node:20.11-bookworm-slim) or digest for reproducibility.
NEVER store secrets in ENV or ARG -- they are visible in docker history. ALWAYS use RUN --mount=type=secret instead.
NEVER use ADD when COPY suffices -- ADD has implicit behaviors (auto-extraction, URL download) that make builds less predictable.
NEVER use shell form for ENTRYPOINT -- the application will NOT be PID 1 and will NOT receive signals for graceful shutdown.
NEVER separate apt-get update and apt-get install into different RUN instructions -- the update layer gets cached and becomes stale.
ALWAYS combine related RUN commands with && to minimize layers.
ALWAYS clean up package manager caches in the same RUN layer as the install.
---
Shell Form vs Exec Form
Three instructions support both forms: RUN, CMD, ENTRYPOINT.
| Form | Syntax | Shell Processing | Variable Expansion | Signal Handling |
|---|---|---|---|---|
| Shell | CMD command arg1 | Yes (/bin/sh -c) | Yes ($VAR works) | App is NOT PID 1 |
| Exec | CMD ["command", "arg1"] | No (direct exec) | No (use ENV for vars) | App IS PID 1 |
ALWAYS use exec form for CMD and ENTRYPOINT in production images.
Use shell form for RUN when you need variable expansion, pipes, or command chaining.
---
CMD vs ENTRYPOINT Interaction Matrix
| No ENTRYPOINT | ENTRYPOINT (shell form) | ENTRYPOINT (exec form) | |
|---|---|---|---|
| No CMD | Error -- no command | /bin/sh -c entrypoint_cmd | entrypoint_cmd |
| CMD (exec form) | cmd_executable args | /bin/sh -c entrypoint_cmd (CMD ignored) | entrypoint_cmd cmd_args |
| CMD (shell form) | /bin/sh -c cmd_string | /bin/sh -c entrypoint_cmd (CMD ignored) | entrypoint_cmd /bin/sh -c cmd_string |
Best practice pattern:
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["default-command"]ENTRYPOINT(exec form) sets the fixed executable.CMD(exec form) provides default arguments, overridable viadocker run.- Shell form ENTRYPOINT ALWAYS ignores CMD -- NEVER combine them.
---
COPY vs ADD Decision Guide
| Use Case | Instruction | Why |
|---|---|---|
| Copy local files | COPY | Explicit, predictable, no side effects |
| Copy from build stage | COPY --from | Only option for multi-stage copies |
| Download remote file with checksum | ADD --checksum | Integrity verification built in |
| Clone a Git repository | ADD (Git URL) | Supports branch/tag/commit references |
| Extract a local tar archive | ADD | Auto-extracts tar, tar.gz, tar.bz2, tar.xz |
| Everything else | COPY | ALWAYS prefer COPY by default |
ALWAYS prefer COPY unless you specifically need ADD's extra features.
---
ENV vs ARG Comparison
| Property | ENV | ARG |
|---|---|---|
| Available during build | Yes | Yes |
| Available at runtime | Yes | No |
| Visible in final image | Yes (docker inspect) | No |
| Visible in history | Yes | Yes (NEVER put secrets here) |
| Overridable | docker run --env | docker build --build-arg |
| Scope | Current + subsequent stages | Current stage only |
| Creates layer | Yes | No |
| Survives FROM | Yes (inherited) | No (must re-declare) |
ALWAYS use ARG for build-time-only values (version numbers, build flags). ALWAYS use ENV for values needed at container runtime (PATH, config).
---
HEALTHCHECK Syntax
HEALTHCHECK [OPTIONS] CMD <command>
HEALTHCHECK NONE| Option | Default | Description |
|---|---|---|
--interval=DURATION | 30s | Time between checks |
--timeout=DURATION | 30s | Max time for single check |
--start-period=DURATION | 0s | Grace period on startup |
--retries=N | 3 | Consecutive failures before unhealthy |
Exit codes: 0 = healthy, 1 = unhealthy, 2 = reserved (NEVER use).
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1ALWAYS set --start-period for applications with slow startup.
---
ONBUILD Triggers
ONBUILD ADD . /app/src
ONBUILD RUN /app/src/compile.sh- NOT executed in the current build -- fires in child images using
FROM <this-image>. - Useful for language-stack base images.
- ONBUILD ONBUILD is NOT allowed (no chaining).
- ONBUILD FROM and ONBUILD MAINTAINER are NOT allowed.
---
RUN Mount Types (BuildKit)
| Mount Type | Purpose | Key Flags |
|---|---|---|
--mount=type=cache | Persist package manager caches | target, sharing, id |
--mount=type=bind | Mount context without COPY layer | target, from, source |
--mount=type=secret | Access secrets without baking in | id, target, env |
--mount=type=ssh | Forward SSH agent | id |
--mount=type=tmpfs | Temporary filesystem | target |
See references/instructions.md for complete mount syntax and examples.
---
Variable Substitution
Supported in: ADD, COPY, ENV, EXPOSE, FROM, LABEL, STOPSIGNAL, USER, VOLUME, WORKDIR, ONBUILD.
NOT supported in: RUN exec form, CMD exec form, ENTRYPOINT exec form (use shell form or ENV).
| Modifier | Example | Result |
|---|---|---|
| Default value | ${VAR:-default} | Use default if VAR unset |
| Alternate value | ${VAR:+alternate} | Use alternate if VAR is set |
| Remove prefix | ${VAR#pattern} | Remove shortest prefix match |
| Remove suffix | ${VAR%pattern} | Remove shortest suffix match |
---
Reference Links
- references/instructions.md -- Complete syntax and parameters for all 17 instructions
- references/examples.md -- Production-ready Dockerfile examples for common scenarios
- references/anti-patterns.md -- Instruction misuse patterns with corrections
Official Sources
- https://docs.docker.com/reference/dockerfile/
- https://docs.docker.com/build/building/best-practices/
- https://docs.docker.com/build/building/multi-stage/
- https://docs.docker.com/build/buildkit/
Dockerfile Anti-Patterns
Common instruction misuse patterns with corrections.
Every anti-pattern includes WHY it is wrong and the correct alternative.
---
AP-001: Using latest Tag
Problem: Non-deterministic builds -- different image on each build.
# BAD
FROM node:latest
FROM python# GOOD -- pin to specific version
FROM node:20.11-bookworm-slim
# BEST -- pin to digest for full reproducibility
FROM node:20.11-bookworm-slim@sha256:abc123...Why: latest resolves to a different image after every upstream push. Builds become unreproducible and may break without any Dockerfile change.
---
AP-002: Secrets in ENV or ARG
Problem: Secrets are baked into image layers and visible in docker history.
# BAD -- secret persists in image metadata
ENV API_KEY=sk-1234567890
ARG DATABASE_PASSWORD=secret123
RUN curl -H "Authorization: Bearer $API_KEY" https://api.example.com# GOOD -- secret exists only during RUN, never in any layer
RUN --mount=type=secret,id=api_key,env=API_KEY \
curl -H "Authorization: Bearer $API_KEY" https://api.example.comBuild: docker build --secret id=api_key,src=./api_key.txt .
Why: ENV values persist in the final image (docker inspect). ARG values appear in docker history. Both are extractable by anyone with access to the image.
---
AP-003: ADD When COPY Suffices
Problem: ADD has implicit behaviors that make builds less predictable.
# BAD -- ADD auto-extracts tars, downloads URLs, adds magic
ADD config.json /app/config.json
ADD src/ /app/src/# GOOD -- COPY is explicit with no side effects
COPY config.json /app/config.json
COPY src/ /app/src/Why: ADD auto-extracts tar archives and downloads URLs. When you only need to copy local files, these implicit behaviors create confusion and risk unexpected extraction.
When ADD is correct:
- Downloading a remote file with
ADD --checksum=sha256:... - Cloning a Git repository:
ADD https://github.com/user/repo.git#v1.0 /src - Intentionally extracting a tar archive:
ADD archive.tar.gz /dest/
---
AP-004: Shell Form ENTRYPOINT
Problem: Application is NOT PID 1 and does not receive signals.
# BAD -- runs as /bin/sh -c "/usr/bin/myapp", app is NOT PID 1
ENTRYPOINT /usr/bin/myapp# GOOD -- app IS PID 1, receives SIGTERM for graceful shutdown
ENTRYPOINT ["/usr/bin/myapp"]Why: Shell form wraps the command in /bin/sh -c, making sh PID 1 instead of the application. The sh process does NOT forward signals. docker stop sends SIGTERM, but the application never receives it, leading to a 10-second timeout and forced SIGKILL.
---
AP-005: Separate apt-get update and install
Problem: Cached update layer becomes stale, causing install failures.
# BAD -- apt-get update is cached, install uses stale package list
RUN apt-get update
RUN apt-get install -y curl# GOOD -- always combine in one layer
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*Why: Docker caches each RUN layer independently. When you add a new package later, the apt-get update layer is still cached from weeks/months ago. The install fails because package URLs have changed.
---
AP-006: Not Cleaning Package Manager Cache
Problem: Cache files bloat the image by 30-100MB per install.
# BAD -- cache left in layer
RUN apt-get update && apt-get install -y curl git# GOOD -- clean in same layer
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*Why: Cleanup in a SEPARATE RUN instruction does NOT reduce image size. Docker layers are additive -- deleting files in a new layer adds a "whiteout" entry but the original data remains in the previous layer.
---
AP-007: Running as Root
Problem: Container processes run as root, creating a security risk.
# BAD -- no USER instruction, runs as root
FROM node:20
COPY . /app
CMD ["node", "app.js"]# GOOD -- create and switch to non-root user
FROM node:20
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["node", "app.js"]Why: If an attacker exploits the application, root access inside the container can lead to container escape, host filesystem access, or privilege escalation.
---
AP-008: COPY . . Before Dependency Install
Problem: Every source code change invalidates the dependency cache.
# BAD -- any file change triggers full npm install
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build# GOOD -- dependency files copied first, code changes only affect build
FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildWhy: Docker invalidates a layer's cache when ANY input file changes. Copying everything before npm install means even a one-line code change triggers a full dependency reinstall.
---
AP-009: Using cd Instead of WORKDIR
Problem: cd in RUN does not persist to the next instruction.
# BAD -- cd only lasts within the same RUN
RUN cd /app && npm install
RUN npm run build # FAILS: still in / not /app# GOOD -- WORKDIR persists across instructions
WORKDIR /app
RUN npm install
RUN npm run buildWhy: Each RUN starts from the WORKDIR, not from where the previous RUN ended. cd within a RUN only affects that single instruction.
---
AP-010: Too Many Layers
Problem: Each RUN creates a separate layer, bloating the image.
# BAD -- 4 layers for one logical operation
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*# GOOD -- one layer for the entire operation
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*Why: More layers mean larger images and slower pulls. Cleanup in a later layer does NOT reclaim space from earlier layers.
---
AP-011: ENV Persistence Leak
Problem: Environment variables set with ENV persist in the final image even after unset.
# BAD -- ADMIN_USER persists in the image despite unset
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER # Does NOT remove from image metadata!# GOOD -- use shell variable within single RUN
RUN export ADMIN_USER="mark" \
&& echo $ADMIN_USER > ./mark \
&& unset ADMIN_USERWhy: ENV writes to image metadata. unset in a RUN only affects that shell session. The variable remains in docker inspect output and is available at container runtime.
---
AP-012: No .dockerignore
Problem: Entire project directory (including node_modules, .git) is sent as build context.
# BAD -- no .dockerignore, sends everything
project/
├── .git/ (500MB+ of history)
├── node_modules/ (500MB+ of dependencies)
├── dist/ (rebuilt in container)
└── src/ (what you actually need)# GOOD -- .dockerignore excludes irrelevant files
.git
node_modules
dist
*.md
.env
.env.*Why: Build context is sent to the Docker daemon before build starts. Without .dockerignore, gigabytes of unnecessary data are transferred, slowing every build.
---
AP-013: No HEALTHCHECK
Problem: Docker has no way to detect if the application inside the container is actually working.
# BAD -- no health monitoring
FROM node:20
COPY . /app
CMD ["node", "app.js"]# GOOD -- Docker can detect application failures
FROM node:20
COPY . /app
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"
CMD ["node", "app.js"]Why: Without HEALTHCHECK, Docker only knows if the process is running, not if it is responding correctly. Orchestrators like Docker Compose and Swarm use health status to manage container lifecycle and restarts.
---
AP-014: Pipe Errors Silently Swallowed
Problem: In shell form, pipe failures are masked by the last command's exit code.
# BAD -- if wget fails, wc still succeeds, RUN reports success
RUN wget -O - https://some.site | wc -l > /number# GOOD -- set pipefail so any command failure is caught
RUN set -o pipefail && wget -O - https://some.site | wc -l > /numberWhy: By default, /bin/sh -c only checks the exit code of the last command in a pipe. A failed download could produce an empty file with no error. set -o pipefail makes the pipe return the exit code of the first failing command.
Note: pipefail is a bash feature. If the default shell is dash (common on Debian), use exec form: RUN ["/bin/bash", "-c", "set -o pipefail && ..."].
---
AP-015: Shell Form CMD with ENTRYPOINT
Problem: Shell form CMD produces unexpected process tree when combined with exec form ENTRYPOINT.
# BAD -- results in: entrypoint_cmd /bin/sh -c cmd_string
ENTRYPOINT ["python"]
CMD python app.py# GOOD -- results in: python app.py
ENTRYPOINT ["python"]
CMD ["app.py"]Why: Shell form CMD wraps in /bin/sh -c, which becomes an argument to the ENTRYPOINT. The actual command becomes python /bin/sh -c python app.py, which is not the intended behavior. ALWAYS use exec form for both ENTRYPOINT and CMD.
---
AP-016: Numeric Stage References
Problem: Using --from=0 instead of named stages breaks when stages are reordered.
# BAD -- fragile, breaks if a stage is added before this one
FROM golang:1.22
RUN go build -o /app
FROM alpine:3.19
COPY --from=0 /app /usr/bin/app# GOOD -- named reference survives reordering
FROM golang:1.22 AS build
RUN go build -o /app
FROM alpine:3.19
COPY --from=build /app /usr/bin/appWhy: Numeric indexes are positional. Adding, removing, or reordering stages silently changes what --from=0 refers to. Named stages are explicit and self-documenting.
---
AP-017: VOLUME Before File Operations
Problem: Data written to a VOLUME path during build is silently discarded.
# BAD -- the echo output is lost because /data is a volume
VOLUME /data
RUN echo "config" > /data/config.txt # DISCARDED at runtime!# GOOD -- write files first, declare volume last
RUN mkdir -p /data && echo "config" > /data/config.txt
VOLUME /dataWhy: After a VOLUME instruction, any changes to that directory in subsequent build layers are discarded. The volume mount at runtime replaces the directory contents.
---
AP-018: No --no-install-recommends for apt
Problem: apt installs recommended packages by default, adding unnecessary bloat.
# BAD -- installs curl plus all "recommended" packages
RUN apt-get update && apt-get install -y curl# GOOD -- only installs curl and its hard dependencies
RUN apt-get update && apt-get install -y --no-install-recommends curlWhy: Recommended packages can add 50-200MB of unnecessary software. In container images, you ALWAYS want the minimal set of packages.
Dockerfile Examples -- Production-Ready Templates
All examples verified against Docker Engine 24+ with BuildKit.
ALWAYS start with # syntax=docker/dockerfile:1.---
Go Application (Multi-Stage, Multi-Platform)
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS TARGETARCH
ARG VERSION=dev
WORKDIR /src
# Cache dependency download separately from build
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download
# Build with cached artifacts
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=bind,target=. \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-X main.version=$VERSION" \
-o /bin/app ./cmd
FROM alpine:3.19 AS runtime
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=build /bin/app /usr/bin/app
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
USER appuser:appgroup
EXPOSE 8080
ENTRYPOINT ["/usr/bin/app"]
CMD ["--config", "/etc/app/config.yaml"]---
Node.js Application
# syntax=docker/dockerfile:1
FROM node:20-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --production=false
FROM deps AS build
COPY . .
RUN npm run build
FROM node:20-bookworm-slim AS production
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
ENV NODE_ENV=production
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"
USER appuser:appuser
EXPOSE 3000
ENTRYPOINT ["node"]
CMD ["dist/index.js"]---
Python Application
# syntax=docker/dockerfile:1
FROM python:3.12-slim-bookworm AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
FROM base AS deps
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt
FROM base AS production
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
COPY --from=deps /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=deps /usr/local/bin /usr/local/bin
COPY . .
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
USER appuser:appuser
EXPOSE 8000
ENTRYPOINT ["python"]
CMD ["-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]---
Rust Application
# syntax=docker/dockerfile:1
FROM rust:1.77-bookworm AS build
WORKDIR /app
# Cache dependencies by building a dummy project first
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN --mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/usr/local/cargo/registry/ \
cargo build --release
# Build actual application
COPY src ./src
RUN --mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/usr/local/cargo/registry/ \
cargo build --release && \
cp target/release/myapp /usr/local/bin/
FROM debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
COPY --from=build /usr/local/bin/myapp /usr/local/bin/myapp
USER appuser:appuser
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/myapp"]---
.NET Application
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
WORKDIR /app
COPY --from=build /app/publish .
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
USER appuser:appuser
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]---
Nginx Static Site
# syntax=docker/dockerfile:1
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
FROM nginx:1.25-alpine AS production
# Remove default config
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/app.conf
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]---
Multi-Stage with Tests
# syntax=docker/dockerfile:1
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /bin/app ./cmd
FROM build AS test
RUN go test -v -race ./...
FROM alpine:3.19 AS production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=build /bin/app /usr/bin/app
USER appuser:appgroup
EXPOSE 8080
ENTRYPOINT ["/usr/bin/app"]Build just the test stage: docker build --target test . Build production: docker build --target production .
---
Entrypoint Script Pattern
# syntax=docker/dockerfile:1
FROM postgres:16-bookworm
COPY --chmod=755 docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/
COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["postgres"]docker-entrypoint.sh:
#!/bin/bash
set -e
# Run initialization tasks
if [ "$1" = 'postgres' ]; then
echo "Initializing database..."
chown -R postgres "$PGDATA"
fi
# ALWAYS end with exec "$@" to replace shell with the CMD arguments
# This makes the application PID 1 for proper signal handling
exec "$@"---
Secret and SSH Usage
# syntax=docker/dockerfile:1
FROM node:20-bookworm-slim AS build
WORKDIR /app
# Clone private repository using SSH
RUN --mount=type=ssh \
git clone git@github.com:company/private-lib.git /app/lib
# Use API token without baking into layer
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN \
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
npm ci && \
rm .npmrc
COPY . .
RUN npm run build
FROM node:20-bookworm-slim AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]Build command:
docker build \
--ssh default \
--secret id=npm_token,src=$HOME/.npmrc_token \
-t myapp:latest .---
Minimal Image from Scratch
# syntax=docker/dockerfile:1
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
# Static binary with no CGO dependencies
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/app ./cmd
FROM scratch
# Copy CA certificates for HTTPS
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /bin/app /bin/app
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/bin/app"]The scratch image has zero bytes -- no shell, no package manager, no OS. ONLY use with statically compiled binaries.
---
PHP/Composer with Cache Mounts
# syntax=docker/dockerfile:1
FROM composer:2 AS deps
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/cache \
composer install --no-dev --no-scripts --no-autoloader
FROM php:8.3-fpm-bookworm AS production
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
&& docker-php-ext-install pdo_pgsql opcache \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=deps /app/vendor ./vendor
COPY . .
RUN composer dump-autoload --optimize --no-dev
USER www-data
EXPOSE 9000
CMD ["php-fpm"]Dockerfile Instructions -- Complete Reference
All syntax verified against https://docs.docker.com/reference/dockerfile/
Requires: # syntax=docker/dockerfile:1 parser directive for BuildKit features.---
FROM
Initializes a new build stage and sets the base image. MUST be the first instruction after parser directives and global ARGs.
Syntax
FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]Parameters
| Parameter | Required | Description |
|---|---|---|
<image> | Yes | Base image name |
:<tag> | No | Image tag (defaults to latest) |
@<digest> | No | Pin to exact image digest |
--platform=<platform> | No | Target platform (linux/amd64, linux/arm64, etc.) |
AS <name> | No | Name the build stage for COPY --from=<name> |
Behaviors
- Multiple FROM instructions create multi-stage builds.
- Each FROM clears all prior state (layers, ENV, ARG within stage).
- ARG instructions before FROM are available in the FROM line but NOT in subsequent instructions unless re-declared with
ARG <name>(no default needed). - Tag defaults to
latestwhen omitted -- ALWAYS specify an explicit tag.
Examples
# Pinned version
FROM ubuntu:22.04
# Named stage for multi-stage
FROM golang:1.22 AS builder
# Platform-specific
FROM --platform=linux/arm64 alpine:3.19
# Digest-pinned for reproducibility
FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
# Dynamic base via global ARG
ARG BASE_IMAGE=ubuntu:22.04
FROM ${BASE_IMAGE} AS runtime---
RUN
Executes commands during build in a new layer.
Syntax Forms
# Shell form (processed by /bin/sh -c)
RUN <command>
# Exec form (no shell processing)
RUN ["executable", "param1", "param2"]
# Heredoc form (BuildKit)
RUN <<EOF
commands here
EOFMount Options (BuildKit)
Cache Mount
Persists package manager caches across builds.
RUN --mount=type=cache,target=<path>[,id=<id>][,sharing=<shared|private|locked>][,from=<stage>][,source=<path>][,mode=<mode>][,uid=<uid>][,gid=<gid>] <command>| Parameter | Default | Description |
|---|---|---|
target | Required | Directory to cache |
id | target value | Cache identifier |
sharing | shared | shared, private, or locked |
from | -- | Source stage for initial cache content |
source | -- | Source path within the from stage |
mode | 0755 | Permissions on cache directory |
uid | 0 | Owner user ID |
gid | 0 | Owner group ID |
Package manager cache targets:
| Package Manager | Cache Target(s) | Sharing |
|---|---|---|
| apt | /var/cache/apt + /var/lib/apt | locked (required) |
| npm | /root/.npm | shared |
| pip | /root/.cache/pip | shared |
| Go | /go/pkg/mod + /root/.cache/go-build | shared |
| Cargo (Rust) | /app/target/ + /usr/local/cargo/git/db + /usr/local/cargo/registry/ | shared |
| Bundler (Ruby) | /root/.gem | shared |
| NuGet (.NET) | /root/.nuget/packages | shared |
| Composer (PHP) | /tmp/cache | shared |
Bind Mount
Mount context files without creating a COPY layer. Read-only by default.
RUN --mount=type=bind,target=<path>[,source=<path>][,from=<stage|image>][,rw] <command>| Parameter | Default | Description |
|---|---|---|
target | Required | Mount point inside the build container |
source | . | Source path in the context or stage |
from | Build context | Source stage or external image |
rw | -- | Make mount read-write |
Secret Mount
Access secrets without baking into any layer.
RUN --mount=type=secret,id=<id>[,target=<path>][,env=<varname>][,required][,mode=<mode>][,uid=<uid>][,gid=<gid>] <command>| Parameter | Default | Description |
|---|---|---|
id | Required | Secret identifier (matches --secret id= in build command) |
target | /run/secrets/<id> | File path for the secret |
env | -- | Expose as environment variable instead of file |
required | false | Fail build if secret not provided |
mode | 0400 | File permissions |
Build command: docker build --secret id=mytoken,src=./token.txt .
SSH Mount
Forward the host SSH agent for Git operations.
RUN --mount=type=ssh[,id=<id>][,target=<path>][,required][,mode=<mode>][,uid=<uid>][,gid=<gid>] <command>Build command: docker build --ssh default .
Tmpfs Mount
Temporary filesystem, discarded after RUN completes.
RUN --mount=type=tmpfs,target=<path>[,size=<bytes>] <command>Other RUN Options
| Option | Values | Min Version | Description |
|---|---|---|---|
--network | default, none, host | 1.3 | Control network access during build |
--security | sandbox, insecure | 1.20 | Security mode for the build step |
Key Behaviors
- Shell form uses
/bin/sh -cby default (configurable via SHELL). - Exec form does NOT invoke a shell -- no variable expansion, no pipes, no glob.
- Each RUN creates a new layer -- combine commands with
&&to minimize layers. - Cache invalidation checks the command string only, NOT external resources.
---
CMD
Default command when a container starts. Does NOT execute during build.
Syntax Forms
# Exec form (PREFERRED)
CMD ["executable", "param1", "param2"]
# Default parameters for ENTRYPOINT
CMD ["param1", "param2"]
# Shell form
CMD command param1 param2Behaviors
- Only the LAST CMD in a Dockerfile takes effect.
- Overridden entirely by arguments passed to
docker run. - When combined with exec-form ENTRYPOINT, CMD provides default arguments.
- Shell form wraps in
/bin/sh -c-- the shell becomes PID 1, not the application.
---
ENTRYPOINT
Configures the container to run as an executable.
Syntax Forms
# Exec form (PREFERRED)
ENTRYPOINT ["executable", "param1", "param2"]
# Shell form
ENTRYPOINT command param1 param2Behaviors
- Exec form:
docker runarguments are APPENDED to ENTRYPOINT. - Shell form:
docker runarguments are IGNORED. Runs under/bin/sh -c. - Only the LAST ENTRYPOINT takes effect.
- Override at runtime with
docker run --entrypoint.
Best Practice -- Entrypoint Script
COPY --chmod=755 docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]#!/bin/bash
set -e
# Initialization logic here
exec "$@" # Replace shell with CMD arguments -- app becomes PID 1---
COPY
Copies files from build context or earlier build stages.
Syntax
COPY [OPTIONS] <src> ... <dest>
COPY [OPTIONS] ["<src>", ... "<dest>"]Options
| Option | Description | Min Version |
|---|---|---|
| `--from=<stage\ | image\ | context>` |
--chown=<user>:<group> | Set ownership (Linux only) | -- |
--chmod=<perms> | Set file permissions (octal or symbolic) | 1.2 |
--link[=<boolean>] | Enhanced layer reuse across rebuilds | 1.4 |
--parents[=<boolean>] | Preserve parent directory structure | 1.7 |
--exclude=<pattern> | Exclude matching paths | 1.7 |
Behaviors
- Cache invalidation uses file content checksums (NOT modification timestamps).
- Destination ending with
/is treated as a directory. - Relative paths resolve against WORKDIR.
- Default permissions: 0644 for files, 0755 for directories.
- Glob patterns (
*.txt,src/*.go) are supported in source paths.
Examples
COPY file1.txt /dest/
COPY *.json /app/
COPY --from=build /app/binary /usr/bin/
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
COPY --chmod=755 entrypoint.sh /
COPY --chown=appuser:appgroup config/ /app/config/
COPY --parents src/main.go src/utils.go /app/
COPY --exclude=*.test.go --exclude=*_test.go . /app/src/
COPY --link /app /app---
ADD
Like COPY but with URL download, Git clone, and tar auto-extraction.
Syntax
ADD [OPTIONS] <src> ... <dest>
ADD [OPTIONS] ["<src>", ... "<dest>"]Additional Options (beyond COPY)
| Option | Description | Min Version |
|---|---|---|
--keep-git-dir=<boolean> | Preserve .git directory when cloning | 1.1 |
--checksum=<hash> | Verify integrity of remote sources | 1.6 |
--unpack=<boolean> | Control auto-extraction of archives | 1.17 |
Sources Supported
- Local files and directories
- HTTP/HTTPS URLs
- Git repositories (with branch/tag/commit refs)
- Local tar archives (auto-extracted: tar, tar.gz, tar.bz2, tar.xz)
Examples
# Remote file with checksum
ADD --checksum=sha256:24454f... https://example.com/archive.tar.gz /
# Git repository at specific tag
ADD https://github.com/moby/buildkit.git#v0.14.1:docs /buildkit-docs
# Disable auto-extraction
ADD --unpack=false my-archive.tar.gz .---
ENV
Sets environment variables that persist in the final image and at container runtime.
Syntax
ENV <key>=<value> [<key>=<value>...]Behaviors
- Persists in the final image (visible via
docker inspect). - Each ENV instruction creates a new layer.
- Overridable at runtime via
docker run --env KEY=VALUE. - Values inherit into child stages in multi-stage builds.
- Multiple assignments on one line use the values from BEFORE the line:
ENV abc=hello
ENV abc=bye def=$abc # def=hello (old value of abc)
ENV ghi=$abc # ghi=bye (new value of abc)---
ARG
Build-time variables. NOT persisted in the final image.
Syntax
ARG <name>[=<default value>]Behaviors
- Scope is limited to the current build stage.
- MUST be re-declared after FROM to use within a stage.
- Values are visible in
docker history-- NEVER use for secrets. - Overridable via
docker build --build-arg NAME=VALUE.
Predefined Platform ARGs (BuildKit)
Automatically available without declaration:
| ARG | Example Value | Description |
|---|---|---|
TARGETPLATFORM | linux/amd64 | Target platform |
TARGETOS | linux | Target OS |
TARGETARCH | amd64 | Target architecture |
TARGETVARIANT | v7 | Target variant (e.g., ARM) |
BUILDPLATFORM | linux/amd64 | Build machine platform |
BUILDOS | linux | Build machine OS |
BUILDARCH | amd64 | Build machine architecture |
Predefined Proxy ARGs
Excluded from docker history by default:
HTTP_PROXY, HTTPS_PROXY, FTP_PROXY, NO_PROXY, ALL_PROXY (and lowercase variants).
BuildKit Built-in ARGs
| ARG | Purpose |
|---|---|
BUILDKIT_INLINE_CACHE | Enable inline cache metadata |
BUILDKIT_MULTI_PLATFORM | Deterministic multi-platform output |
BUILDKIT_SANDBOX_HOSTNAME | Set build hostname |
BUILDKIT_CONTEXT_KEEP_GIT_DIR | Preserve .git in context |
BUILDKIT_CACHE_MOUNT_NS | Cache ID namespace |
---
WORKDIR
Sets the working directory for RUN, CMD, ENTRYPOINT, COPY, ADD.
Syntax
WORKDIR /path/to/workdirBehaviors
- Created automatically if it does not exist.
- Relative paths stack:
WORKDIR /athenWORKDIR bthenWORKDIR cresults in/a/b/c. - Supports environment variable expansion:
WORKDIR $DIRPATH. - ALWAYS use WORKDIR instead of
RUN cd /some/path && ....
---
EXPOSE
Documents which ports the container listens on. Does NOT publish them.
Syntax
EXPOSE <port>[/<protocol>] [<port>[/<protocol>]...]- Defaults to TCP if protocol is omitted.
- Ports are published at runtime with
docker run -por-P. - Purely informational -- has no networking effect at build time.
---
VOLUME
Creates a mount point for externally mounted volumes.
Syntax
VOLUME ["/data"]
VOLUME /var/log /var/dbBehaviors
- Marks directories as externally mountable.
- Host directory is specified at container runtime, NOT in the Dockerfile.
- Any data written to a VOLUME path after the VOLUME instruction during build is DISCARDED.
- ALWAYS declare VOLUME for mutable or user-serviceable data (databases, logs).
---
USER
Sets the user and optionally group for subsequent RUN, CMD, ENTRYPOINT.
Syntax
USER <user>[:<group>]
USER <UID>[:<GID>]Example
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
USER appuser:appuser- ALWAYS create the user before switching to it.
- Use
--no-log-initto prevent sparse file issues with large UIDs.
---
HEALTHCHECK
Defines how Docker tests whether the container is working.
Syntax
HEALTHCHECK [OPTIONS] CMD <command>
HEALTHCHECK NONEOptions
| Option | Default | Description |
|---|---|---|
--interval=DURATION | 30s | Time between checks |
--timeout=DURATION | 30s | Max time for single check |
--start-period=DURATION | 0s | Grace period on startup (failures don't count) |
--retries=N | 3 | Consecutive failures to mark unhealthy |
Exit Codes
| Code | Status |
|---|---|
| 0 | Healthy |
| 1 | Unhealthy |
| 2 | Reserved -- NEVER use |
Example
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1- Only the LAST HEALTHCHECK takes effect.
HEALTHCHECK NONEdisables any health check inherited from a base image.
---
LABEL
Adds metadata key-value pairs to the image.
Syntax
LABEL <key>=<value> [<key>=<value>...]OCI Standard Labels
LABEL org.opencontainers.image.title="My App"
LABEL org.opencontainers.image.description="Application description"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.authors="author@example.com"
LABEL org.opencontainers.image.url="https://example.com"
LABEL org.opencontainers.image.source="https://github.com/user/repo"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.created="2024-01-01T00:00:00Z"- Replaces deprecated MAINTAINER instruction.
- View with
docker image inspect --format='{{json .Config.Labels}}' <image>. - ALWAYS use OCI standard label keys where applicable.
---
SHELL
Overrides the default shell for shell-form commands.
Syntax
SHELL ["executable", "parameters"]Examples
# Linux -- switch to bash
SHELL ["/bin/bash", "-c"]
RUN echo "Now using bash"
# Windows -- switch to PowerShell
SHELL ["powershell", "-Command"]
RUN Write-Host 'Hello from PowerShell'- Default on Linux:
["/bin/sh", "-c"] - Default on Windows:
["cmd", "/S", "/C"] - Affects all subsequent shell-form RUN, CMD, ENTRYPOINT.
---
STOPSIGNAL
Sets the system call signal sent to the container to exit.
Syntax
STOPSIGNAL <signal>- Signal can be a name (
SIGTERM) or number (15). - Default is
SIGTERM. - Override at runtime with
docker run --stop-signal.
---
ONBUILD
Adds a trigger instruction executed when the image is used as a base for another build.
Syntax
ONBUILD <INSTRUCTION>Behaviors
- NOT executed in the current build.
- Fires in child images that use
FROM <this-image>. - Useful for language-stack base images (e.g.,
ruby:2.0-onbuild). - ONBUILD triggers execute immediately after the child's FROM instruction.
Restrictions
ONBUILD ONBUILDis NOT allowed (no chaining).ONBUILD FROMis NOT allowed.ONBUILD MAINTAINERis NOT allowed.
Example
# Base image for Node.js apps
ONBUILD COPY package.json /app/
ONBUILD RUN npm install
ONBUILD COPY . /app/---
Environment Variable Substitution
Variables ($variable or ${variable}) are supported in these instructions: ADD, COPY, ENV, EXPOSE, FROM, LABEL, STOPSIGNAL, USER, VOLUME, WORKDIR, ONBUILD.
Modifiers
| Modifier | Syntax | Result |
|---|---|---|
| Default value | ${variable:-default} | Use default if variable is unset |
| Alternate value | ${variable:+alternate} | Use alternate if variable is set |
| Remove shortest prefix | ${variable#pattern} | Strip shortest match from start |
| Remove longest prefix | ${variable##pattern} | Strip longest match from start |
| Remove shortest suffix | ${variable%pattern} | Strip shortest match from end |
| Remove longest suffix | ${variable%%pattern} | Strip longest match from end |
| Replace first | ${variable/find/replace} | Replace first occurrence |
| Replace all | ${variable//find/replace} | Replace all occurrences |
Escape with \$foo or \${foo} for literal dollar signs.