Docker for Development and Production

Philip Rehberger Nov 13, 2025 8 min read

From local development to production deployment, learn to use Docker effectively across your workflow.

Docker for Development and Production

Docker changed how we build and deploy software. Containers provide consistency from development through production, eliminating the "works on my machine" problem. This guide covers practical Docker usage for web developers.

Why Containers Matter

A Docker container packages your application with its dependencies, runtime, and configuration. The same container runs identically whether it's on your laptop, a colleague's machine, or a production server.

This consistency solves real problems:

  • New developers can run the project in minutes, not hours
  • Staging environments match production exactly
  • Deployments are predictable and repeatable

Writing Efficient Dockerfiles

A Dockerfile describes how to build your container image. Order matters;Docker caches each layer, so put things that change less frequently at the top. This basic example demonstrates the optimal ordering for a Node.js application, with dependencies installed before source code is copied.

FROM node:20-alpine

# Dependencies change less often than code
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Code changes frequently - keep it last
COPY . .

RUN npm run build
CMD ["npm", "start"]

By copying package.json files and installing dependencies before copying the rest of your code, Docker can reuse the cached dependency layer when only your source code changes. This dramatically speeds up rebuilds during development. You'll notice builds take seconds instead of minutes once the dependency layer is cached.

Key practices:

Use specific base image tags: node:20-alpine not node:latest. You want reproducible builds.

Minimize layers: Each RUN command creates a layer. Combine related commands to reduce image size and build time. The following example shows how to consolidate multiple commands into a single layer.

# Bad - three layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Better - one layer
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

The combined version also cleans up the apt cache in the same layer, preventing those temporary files from being stored in the image. This technique can significantly reduce your final image size.

Use .dockerignore: Exclude files that shouldn't be in the image. This speeds up builds and prevents sensitive files from accidentally being included. Create this file in the same directory as your Dockerfile.

node_modules
.git
*.log
.env

Multi-Stage Builds

Multi-stage builds create smaller production images by separating build dependencies from runtime. You define multiple FROM statements and copy only what you need from earlier stages. This is essential for compiled languages and build-heavy applications.

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

# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

The final image only contains what's needed to run, not build tools or development dependencies. This reduces image size and attack surface. Notice the --from=builder syntax that copies files from the earlier build stage. You can have as many stages as you need.

For even smaller images, copy only production dependencies. This approach reinstalls packages in the final stage, excluding devDependencies entirely. You'll typically see a 30-50% reduction in image size.

# Production stage
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Docker Compose for Local Development

Docker Compose defines multi-container applications. A typical web app needs the application, database, and maybe a cache. This configuration file describes all your services and how they connect. You'll use this daily during development.

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp

volumes:
  postgres_data:

The volumes section mounts your code into the container so changes reflect immediately. The anonymous volume /app/node_modules prevents your local node_modules from overwriting the container's dependencies. The named volume postgres_data persists database data between container restarts. You'll appreciate this configuration when you can edit code and see changes without rebuilding.

Start everything with a single command.

docker compose up

Environment Configuration

Never bake secrets into images. Use environment variables to keep configuration separate from code. The following pattern declares the variable in your Dockerfile without providing a default value.

# Good - environment variable
ENV DATABASE_URL=""

Pass values at runtime using the -e flag. This keeps sensitive data out of your image layers and lets you use the same image across different environments.

docker run -e DATABASE_URL=postgres://... myapp

Or use a .env file with Compose for easier management of multiple variables. This is particularly convenient when you have many environment variables.

services:
  app:
    env_file:
      - .env

For production, use your platform's secrets management (AWS Secrets Manager, Kubernetes Secrets, etc.).

Debugging Containers

These commands help you troubleshoot issues inside running containers. Keep them handy as you'll use them frequently.

View logs: Check what your application is outputting to stdout and stderr. Add the -f flag to follow logs in real time.

docker logs container_name
docker compose logs app

Get a shell inside a running container: Useful for inspecting the filesystem or running debugging commands. This is your go-to when something isn't working as expected.

docker exec -it container_name sh

Inspect container details: Shows the full configuration including network settings, mounts, and environment variables. The output is JSON, so you can pipe it to jq for easier reading.

docker inspect container_name

Check what's using disk space: Docker images and volumes can consume significant disk space over time. Run this periodically to keep tabs on usage.

docker system df

Clean up unused resources: Remove stopped containers, unused networks, and dangling images to reclaim disk space. Add the -a flag to remove all unused images, not just dangling ones.

docker system prune

Production Considerations

Run as non-root user: Running as root inside containers is a security risk. Create a dedicated user for your application with the following commands in your Dockerfile.

RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -D appuser
USER appuser

Health checks let orchestrators know when containers are ready. Kubernetes and other platforms use these to determine when to route traffic or restart unhealthy containers. Define a simple HTTP check against your application's health endpoint.

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

Resource limits prevent runaway containers from consuming all available resources on the host. Set these based on your application's normal resource usage with some headroom.

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

Logging: Write logs to stdout/stderr. Let the container platform handle log aggregation.

Security Best Practices

Scan images for vulnerabilities: Docker Scout checks your images against known vulnerability databases. Run this as part of your CI pipeline to catch issues early.

docker scout cves myimage:tag

Use minimal base images: Alpine Linux images are much smaller than Debian-based images and have fewer vulnerabilities.

Don't run as root: Create a dedicated user for your application.

Keep images updated: Rebuild regularly to pick up security patches in base images.

Use multi-stage builds: The final image shouldn't contain build tools, package managers, or source code.

Common Docker Compose Patterns

Override files for different environments: Compose merges multiple files, letting you customize settings without duplicating the entire configuration. This is perfect for environment-specific settings.

docker compose -f docker-compose.yml -f docker-compose.prod.yml up

Wait for dependencies: The depends_on directive with condition: service_healthy ensures services start in the correct order and only after dependencies are actually ready, not just started. This prevents race conditions during startup.

services:
  app:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 5s
      retries: 5

Shared networks for microservices: Named networks let services communicate across separate Compose files or isolate groups of services. This is essential when your application grows beyond a single Compose file.

networks:
  backend:
    driver: bridge

services:
  api:
    networks:
      - backend
  worker:
    networks:
      - backend

Services on the same network can reach each other by service name. The api service can connect to http://worker:port without any additional configuration. Docker's internal DNS handles the resolution automatically.

Conclusion

Docker provides the consistency that modern development and deployment require. Start with a simple Dockerfile, add Compose for local development, and use multi-stage builds for production.

The investment in containerizing your application pays off in faster onboarding, reproducible environments, and more reliable deployments. Once you've worked with containers, you won't want to go back.

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.