Official guide with all Dockerfile instructions and production best practices. Essential reference. docs.docker.com →
A Dockerfile is a recipe — each instruction creates a layer in the image.
| Instruction | What it does |
|---|---|
FROM | Base image to build on (always first) |
WORKDIR | Set working directory for all subsequent commands |
COPY | Copy files from host into the image |
RUN | Execute a shell command (creates a layer) |
ENV | Set an environment variable |
EXPOSE | Document which port the container listens on (informational) |
USER | Set which user runs subsequent commands (security) |
CMD | Default command when container starts (overridable) |
ENTRYPOINT | Fixed startup command (not easily overridden) |
ARG | Build-time variable (not in final image) |
Here's a real Dockerfile with every best practice applied:
# ── Stage 1: Dependencies ──────────────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
# Copy ONLY package files first — maximise cache hits
# This layer only rebuilds when dependencies change
COPY package.json package-lock.json ./
RUN npm ci --only=production
# ── Stage 2: Final image ────────────────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app
# Security: don't run as root
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeuser -u 1001
# Copy deps from build stage
COPY --from=deps --chown=nodeuser:nodejs /app/node_modules ./node_modules
# Copy application code
COPY --chown=nodeuser:nodejs . .
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Switch to non-root user
USER nodeuser
# Document port (doesn't actually expose it)
EXPOSE 3000
# Health check — Docker and orchestrators use this
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
# Start the app
CMD ["node", "server.js"]
The first stage (deps) installs dependencies. The second stage (runner) copies only what's needed into a clean image. This keeps the final image small and excludes build tools that shouldn't be in production.
Like .gitignore but for Docker. Without it, COPY . . copies node_modules, .git, and all your secrets into the image. Create .dockerignore in your project root:
node_modules
.git
.gitignore
*.md
.env
.env.*
dist
.DS_Store
coverage
*.log
.nyc_output
If your image is pushed to a registry with a .env file inside it, your secrets are public. Always include .env in .dockerignore. Pass secrets as environment variables at runtime, never at build time.
| Base image | Size | Use when |
|---|---|---|
node:20 | ~1GB | Never in production — too fat |
node:20-slim | ~240MB | Good general default |
node:20-alpine | ~50MB | Best for production — musl libc, minimal attack surface |
cgr.dev/chainguard/node | ~30MB | Maximum security — no shell, distroless |
# Build image
docker build -t myapp:dev .
# Run it
docker run -p 3000:3000 --env-file .env myapp:dev
# Pass a single env var
docker run -p 3000:3000 -e NODE_ENV=production myapp:dev
# Test your health endpoint
curl http://localhost:3000/health
# Inspect the image layers
docker history myapp:dev
# See the final image size
docker images myapp