Learning Guides
Menu

Development Workflows

9 min readDocker for Developers

Development Workflows

Docker can enhance your development experience when configured properly. This chapter covers hot reloading, debugging, IDE integration, and productivity patterns for containerized development.

Development Environment Setup

Development Dockerfile

Create a Dockerfile optimized for development:

DOCKERFILE
# Dockerfile with development target
FROM node:20-alpine AS base
WORKDIR /app
 
# Development stage
FROM base AS development
# Install development dependencies
RUN apk add --no-cache git
COPY package*.json ./
RUN npm install
# Don't copy source - mount it instead
CMD ["npm", "run", "dev"]
 
# Production stage
FROM base AS production
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["npm", "start"]

Development Compose File

YAML
# docker-compose.yml
services:
  app:
    build:
      context: .
      target: development
    volumes:
      # Mount source code
      - ./src:/app/src
      - ./package.json:/app/package.json
      # Prevent overwriting node_modules
      - /app/node_modules
    ports:
      - "3000:3000"
      - "9229:9229" # Debugger
    environment:
      - NODE_ENV=development
    command: npm run dev

Hot Reloading

Node.js with Nodemon

YAML
services:
  app:
    build:
      context: .
      target: development
    volumes:
      - ./src:/app/src
    command: npx nodemon --watch src src/index.js

Node.js with Vite/Next.js

YAML
services:
  frontend:
    build:
      context: .
      target: development
    volumes:
      - ./src:/app/src
      - ./public:/app/public
    ports:
      - "5173:5173" # Vite default
    environment:
      - CHOKIDAR_USEPOLLING=true # For Docker file watching
    command: npm run dev -- --host 0.0.0.0

Note

Set CHOKIDAR_USEPOLLING=true when file change detection doesn't work. This is common on macOS and Windows due to how bind mounts work.

Python with Flask/FastAPI

YAML
services:
  api:
    build: .
    volumes:
      - ./app:/app/app
    ports:
      - "8000:8000"
    environment:
      - FLASK_DEBUG=1
    command: flask run --host=0.0.0.0 --reload
 
  # Or for FastAPI
  fastapi:
    build: .
    volumes:
      - ./app:/app/app
    command: uvicorn app.main:app --host 0.0.0.0 --reload

Go with Air

YAML
services:
  api:
    build:
      context: .
      target: development
    volumes:
      - .:/app
    ports:
      - "8080:8080"
    command: air
TOML
# .air.toml
root = "."
tmp_dir = "tmp"
 
[build]
  cmd = "go build -o ./tmp/main ."
  bin = "tmp/main"
  include_ext = ["go", "tpl", "tmpl", "html"]
  exclude_dir = ["tmp", "vendor"]

Compose Watch

Docker Compose 2.22+ includes built-in watch mode:

YAML
services:
  app:
    build: .
    develop:
      watch:
        # Sync source without rebuild
        - action: sync
          path: ./src
          target: /app/src
 
        # Rebuild on dependency changes
        - action: rebuild
          path: ./package.json
 
        # Sync and restart
        - action: sync+restart
          path: ./config
          target: /app/config
BASH
# Start with watch mode
docker compose watch

Watch Actions

ActionDescriptionUse Case
syncCopy files, no restartSource code changes
rebuildRebuild imageDependency changes
sync+restartCopy and restartConfig file changes

Debugging Containers

Node.js Debugging

YAML
services:
  app:
    build: .
    ports:
      - "3000:3000"
      - "9229:9229" # Debug port
    command: node --inspect=0.0.0.0:9229 src/index.js
JSON
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Docker: Attach to Node",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "address": "localhost",
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/app",
      "restart": true,
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

Python Debugging with debugpy

YAML
services:
  api:
    build: .
    ports:
      - "8000:8000"
      - "5678:5678" # Debug port
    command: python -m debugpy --listen 0.0.0.0:5678 --wait-for-client -m flask run --host=0.0.0.0
JSON
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python: Remote Attach",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
      "pathMappings": [
        {
          "localRoot": "${workspaceFolder}",
          "remoteRoot": "/app"
        }
      ]
    }
  ]
}

Go Debugging with Delve

DOCKERFILE
# Development Dockerfile
FROM golang:1.22 AS development
 
RUN go install github.com/go-delve/delve/cmd/dlv@latest
 
WORKDIR /app
COPY go.* ./
RUN go mod download
 
# No source copy - mount instead
CMD ["dlv", "debug", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient"]
JSON
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Connect to Delve",
      "type": "go",
      "request": "attach",
      "mode": "remote",
      "remotePath": "/app",
      "port": 2345,
      "host": "127.0.0.1"
    }
  ]
}

VS Code Dev Containers

Dev Container Configuration

JSON
// .devcontainer/devcontainer.json
{
  "name": "Node.js Development",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/app",
 
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "ms-azuretools.vscode-docker"
      ],
      "settings": {
        "editor.formatOnSave": true
      }
    }
  },
 
  "forwardPorts": [3000, 9229],
 
  "postCreateCommand": "npm install",
 
  "remoteUser": "node"
}

Note

Dev Containers run your development environment inside a container while providing the full VS Code experience. This ensures every developer has an identical setup.

Dockerfile-based Dev Container

JSON
// .devcontainer/devcontainer.json
{
  "name": "Python Dev Container",
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  },
 
  "features": {
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },
 
  "customizations": {
    "vscode": {
      "extensions": ["ms-python.python", "ms-python.vscode-pylance"]
    }
  },
 
  "postCreateCommand": "pip install -r requirements.txt"
}

Database Development

Local Database Setup

YAML
services:
  app:
    depends_on:
      postgres:
        condition: service_healthy
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: myapp_dev
    ports:
      - "5432:5432" # Expose for local tools
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 5s
      timeout: 5s
      retries: 5
 
volumes:
  postgres-data:

Database GUI Tools

YAML
services:
  # PostgreSQL admin
  pgadmin:
    image: dpage/pgadmin4
    profiles: ["tools"]
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@local.dev
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"
 
  # Generic SQL admin
  adminer:
    image: adminer
    profiles: ["tools"]
    ports:
      - "8080:8080"
BASH
# Start with database tools
docker compose --profile tools up

Database Migrations

YAML
services:
  migrate:
    build:
      context: .
      target: development
    profiles: ["migrate"]
    depends_on:
      postgres:
        condition: service_healthy
    command: npm run db:migrate
 
  seed:
    build:
      context: .
      target: development
    profiles: ["seed"]
    depends_on:
      postgres:
        condition: service_healthy
    command: npm run db:seed
BASH
# Run migrations
docker compose --profile migrate up migrate
 
# Seed database
docker compose --profile seed up seed

Testing Workflows

Test Configuration

YAML
# docker-compose.test.yml
services:
  test:
    build:
      context: .
      target: test
    volumes:
      - ./src:/app/src
      - ./tests:/app/tests
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgres://test:test@postgres-test:5432/test
    depends_on:
      postgres-test:
        condition: service_healthy
    command: npm test
 
  postgres-test:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 2s
      timeout: 2s
      retries: 10
    # No volume - fresh database each time
    tmpfs:
      - /var/lib/postgresql/data
BASH
# Run tests
docker compose -f docker-compose.test.yml up --exit-code-from test
docker compose -f docker-compose.test.yml down -v

Watch Mode Tests

YAML
services:
  test-watch:
    build:
      context: .
      target: test
    volumes:
      - ./src:/app/src
      - ./tests:/app/tests
    profiles: ["test"]
    command: npm run test:watch

Performance Optimization

Faster Builds with Cache

YAML
services:
  app:
    build:
      context: .
      cache_from:
        - myapp:cache
    image: myapp:latest
BASH
# Build with cache
docker compose build --pull
 
# Push cache layer
docker tag myapp:latest myapp:cache
docker push myapp:cache

Volume Performance (macOS/Windows)

YAML
services:
  app:
    volumes:
      # Cached - faster reads (good for source code)
      - ./src:/app/src:cached
 
      # Delegated - faster writes (good for logs)
      - ./logs:/app/logs:delegated
 
      # Consistent - default, slowest but safest
      - ./data:/app/data:consistent

Warning

On macOS and Windows, bind mount performance is significantly slower than on Linux. Use named volumes for directories with many files (like node_modules).

Exclude Heavy Directories

YAML
services:
  app:
    volumes:
      - .:/app
      # Use anonymous volume to exclude node_modules
      - /app/node_modules
      - /app/.next
      - /app/dist

Development Utilities

Make Commands

MAKEFILE
# Makefile
.PHONY: dev up down logs shell test clean
 
dev:
	docker compose up --build
 
up:
	docker compose up -d
 
down:
	docker compose down
 
logs:
	docker compose logs -f
 
shell:
	docker compose exec app sh
 
test:
	docker compose -f docker-compose.test.yml up --exit-code-from test
	docker compose -f docker-compose.test.yml down -v
 
clean:
	docker compose down -v --remove-orphans
	docker system prune -f

Shell Aliases

BASH
# ~/.bashrc or ~/.zshrc
alias dc='docker compose'
alias dcup='docker compose up -d'
alias dcdown='docker compose down'
alias dclogs='docker compose logs -f'
alias dcexec='docker compose exec'
alias dcbuild='docker compose build'
alias dcps='docker compose ps'

Just Commands

JUST
# justfile
default:
    @just --list
 
dev:
    docker compose up --build
 
up:
    docker compose up -d
 
down:
    docker compose down
 
logs service="":
    docker compose logs -f {{service}}
 
shell service="app":
    docker compose exec {{service}} sh
 
psql:
    docker compose exec postgres psql -U dev -d myapp_dev
 
test:
    docker compose -f docker-compose.test.yml up --exit-code-from test
    docker compose -f docker-compose.test.yml down -v

Complete Development Setup

Project Structure

PLAINTEXT
project/
├── .devcontainer/
│   └── devcontainer.json
├── .vscode/
│   ├── launch.json
│   └── tasks.json
├── docker/
│   ├── Dockerfile
│   └── Dockerfile.dev
├── database/
│   ├── init.sql
│   └── migrations/
├── src/
├── tests/
├── docker-compose.yml
├── docker-compose.override.yml  # Dev defaults
├── docker-compose.test.yml
├── .dockerignore
├── .env.example
├── Makefile
└── README.md

Complete docker-compose.yml

YAML
services:
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: ${BUILD_TARGET:-development}
    volumes:
      - ./src:/app/src
      - ./tests:/app/tests
      - /app/node_modules
    ports:
      - "3000:3000"
      - "9229:9229"
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      - DATABASE_URL=postgres://dev:dev@postgres:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./package.json
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
 
  adminer:
    image: adminer
    profiles: ["tools"]
    ports:
      - "8080:8080"
 
volumes:
  postgres-data:
  redis-data:

Quick Reference

Development Commands

CommandPurpose
docker compose upStart development
docker compose up --buildRebuild and start
docker compose watchStart with file sync
docker compose exec app shShell into container
docker compose logs -f appFollow logs

Debugging Ports

LanguageDefault PortFlag/Config
Node.js9229--inspect=0.0.0.0:9229
Python5678debugpy
Go2345Delve
Ruby1234ruby-debug-ide
PHP9003Xdebug

In the next chapter, we'll explore Docker registries and how to distribute your images.