Docker for Development: A Practical Workflow
Multi-Stage Builds
Multi-stage builds use multiple FROM statements in a single Dockerfile. Each stage can use a different base image and copy artifacts from previous stages. This pattern lets you use a full development image with build tools for compilation, then copy only the compiled output to a minimal production image. The result is smaller, more secure production images without sacrificing build capabilities.
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:22-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]Optimizing Layer Caching
Docker caches each layer and reuses it if the instruction and context haven't changed. Order your Dockerfile instructions from least-frequently-changed to most-frequently-changed. Copy package.json and lockfiles first, install dependencies, then copy application code. This way, the expensive dependency installation layer is cached as long as dependencies haven't changed, even when application code changes on every commit.
Use a .dockerignore file to exclude node_modules, .git, dist, and other directories from the build context. A smaller build context means faster builds and fewer cache invalidations.
Docker Compose for Development
Docker Compose orchestrates multi-container development environments. Mount your source code as a volume for hot reload, expose debugging ports, and define dependent services (databases, caches, message brokers) alongside your application. Use a separate compose file for development that overrides production settings with development-friendly configuration.
services:
app:
build:
context: .
target: builder
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
- "9229:9229" # Node.js debug port
command: npm run dev
environment:
- NODE_ENV=development
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp_dev
POSTGRES_PASSWORD: devpassword
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Image Security
Use minimal base images (Alpine or distroless) to reduce the attack surface. Run containers as non-root users. Scan images for known vulnerabilities using tools like Trivy, Snyk, or Docker Scout. Pin base image versions to specific digests rather than mutable tags to ensure reproducible builds. Regularly rebuild and redeploy images to pick up security patches in base images and dependencies.
- Use Alpine or distroless base images for production
- Add a non-root USER instruction to your Dockerfile
- Scan images in CI with Trivy or Docker Scout
- Pin base images to SHA digests for reproducibility