Docker for Development: A Modern Workflow Guide

Docker for Development: A Modern Workflow Guide

"Works on my machine" should never be an acceptable answer. Docker solves this by packaging your application with all its dependencies, ensuring it runs identically everywhere—from your laptop to production.

Why Docker for Development?

Consistency

Everyone on your team runs the same environment. No more "I have version X, you have version Y" problems.

Isolation

Each project gets its own environment. No conflicts between projects requiring different Node versions or database versions.

Easy Onboarding

New team members can get started in minutes, not days.

Production Parity

Your development environment closely matches production, catching environment-specific issues early.

Getting Started

Basic Dockerfile

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev"]

Docker Compose for Multi-Container Apps

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
  
  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_PASSWORD=pass
      - POSTGRES_USER=user
      - POSTGRES_DB=myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
  
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Development Best Practices

Use Multi-Stage Builds

Separate your development and production images:

# Development stage
FROM node:20-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
CMD ["npm", "start"]

Build for development:

docker build --target development -t myapp:dev .

Mount Source Code as Volume

Enable hot reloading by mounting your code:

services:
  app:
    volumes:
      - .:/app              # Mount source code
      - /app/node_modules   # But don't overwrite node_modules

Use .dockerignore

Exclude unnecessary files:

node_modules
npm-debug.log
.env
.git
.gitignore
README.md
.vscode
.idea
dist
build

Environment Variables

Use .env files for local development:

services:
  app:
    env_file:
      - .env.local

Never commit .env files with secrets!

Database Persistence

Always use volumes for databases:

services:
  postgres:
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Useful Commands

# Start all services
docker compose up

# Start in detached mode
docker compose up -d

# Rebuild containers
docker compose up --build

# Stop all services
docker compose down

# Stop and remove volumes (fresh start)
docker compose down -v

# View logs
docker compose logs -f app

# Execute commands in running container
docker compose exec app npm test

# Open shell in container
docker compose exec app sh

# List running containers
docker compose ps

Development Workflow

Initial Setup

# Clone repository
git clone https://github.com/yourteam/project.git
cd project

# Start containers
docker compose up -d

# Run migrations
docker compose exec app npm run migrate

# Open app
open http://localhost:3000

Daily Development

# Start your day
docker compose up -d

# Run tests
docker compose exec app npm test

# Run database migrations
docker compose exec app npm run migrate

# End your day
docker compose down

Debugging

# View logs
docker compose logs -f app

# Check container status
docker compose ps

# Restart a service
docker compose restart app

# Rebuild after dependency changes
docker compose up --build

Common Issues and Solutions

Port Already in Use

# Find what's using the port
lsof -i :3000

# Use a different port in docker-compose.yml
ports:
  - "3001:3000"

Permission Issues (Linux)

# Match container user to host user
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN addgroup -g ${GROUP_ID} user && \
    adduser -D -u ${USER_ID} -G user user
USER user

Slow Performance on Mac

Use delegated or cached volumes:

volumes:
  - .:/app:cached
  - /app/node_modules

Database Not Ready

Add health checks:

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

Performance Optimization

Layer Caching

Order Dockerfile commands from least to most frequently changed:

# These rarely change - cached
FROM node:20-alpine
WORKDIR /app

# These change occasionally - cached if package.json unchanged
COPY package*.json ./
RUN npm ci

# These change frequently - rebuilt often
COPY . .
CMD ["npm", "run", "dev"]

BuildKit

Enable BuildKit for faster builds:

export DOCKER_BUILDKIT=1
docker build .

Or in docker-compose.yml:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      cache_from:
        - myapp:latest

Beyond Development

Once you're comfortable with Docker for development, you're 90% of the way to using it in production. The same Dockerfile (with different stages) can be used for:

  • CI/CD pipelines
  • Staging environments
  • Production deployment

Getting Started Today

  1. Install Docker Desktop
  2. Add a Dockerfile and docker-compose.yml to your project
  3. Run docker compose up
  4. That's it!

Need help containerizing your application? Let's chat.