Docker
03 / 05

Dockerfile Best Practices

Dockerfile Best Practices

Instruction Reference

# Base image — use specific tags, never :latest in prod
FROM node:20-alpine

# Metadata
LABEL maintainer="team@company.com"

# Set working directory (creates if missing)
WORKDIR /app

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

# ARG — build-time variable (not in final image env)
ARG BUILD_VERSION=latest

# Copy files — use .dockerignore to exclude node_modules etc.
COPY package*.json ./          # copy package files first (cache layer)
COPY . .                       # then source code

# Run commands — chain with && to minimize layers
RUN apt-get update && apt-get install -y     curl     git  && rm -rf /var/lib/apt/lists/*    # clean up in same layer!

# Install dependencies
RUN npm ci --only=production

# Expose port (documentation only — does NOT publish)
EXPOSE 3000

# Non-root user (security best practice)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Healthcheck
HEALTHCHECK --interval=30s --timeout=3s --retries=3   CMD curl -f http://localhost:3000/health || exit 1

# Volume mount point
VOLUME ["/app/data"]

# CMD — default command (overridable at runtime)
CMD ["node", "server.js"]

# ENTRYPOINT — fixed executable (CMD becomes its args)
ENTRYPOINT ["node"]
CMD ["server.js"]

Multi-Stage Builds

Use multiple FROM stages to keep the final image small — build tools and dev dependencies don't end up in production.

# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist   # only copy built output
EXPOSE 3000
CMD ["node", "dist/server.js"]

# Build a specific stage
# docker build --target builder -t myapp:builder .

Next.js Dockerfile Example

FROM node:20-alpine AS base

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

.dockerignore

node_modules
.git
.gitignore
*.md
.env*
.next
dist
coverage
.nyc_output
*.log

Layer Caching Tips

  • Copy package.json before source code — deps only reinstall when package.json changes

  • Chain apt-get update && apt-get install && rm -rf in a single RUN

  • Order instructions from least to most frequently changed

  • Use --mount=type=cache in BuildKit to cache package manager downloads

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free