Development Workflows
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 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
# 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 devHot Reloading
Node.js with Nodemon
services:
app:
build:
context: .
target: development
volumes:
- ./src:/app/src
command: npx nodemon --watch src src/index.jsNode.js with Vite/Next.js
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.0Note
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
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 --reloadGo with Air
services:
api:
build:
context: .
target: development
volumes:
- .:/app
ports:
- "8080:8080"
command: air# .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:
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# Start with watch mode
docker compose watchWatch Actions
| Action | Description | Use Case |
|---|---|---|
sync | Copy files, no restart | Source code changes |
rebuild | Rebuild image | Dependency changes |
sync+restart | Copy and restart | Config file changes |
Debugging Containers
Node.js Debugging
services:
app:
build: .
ports:
- "3000:3000"
- "9229:9229" # Debug port
command: node --inspect=0.0.0.0:9229 src/index.js// .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
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// .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
# 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"]// .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
// .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
// .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
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
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"# Start with database tools
docker compose --profile tools upDatabase Migrations
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# Run migrations
docker compose --profile migrate up migrate
# Seed database
docker compose --profile seed up seedTesting Workflows
Test Configuration
# 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# Run tests
docker compose -f docker-compose.test.yml up --exit-code-from test
docker compose -f docker-compose.test.yml down -vWatch Mode Tests
services:
test-watch:
build:
context: .
target: test
volumes:
- ./src:/app/src
- ./tests:/app/tests
profiles: ["test"]
command: npm run test:watchPerformance Optimization
Faster Builds with Cache
services:
app:
build:
context: .
cache_from:
- myapp:cache
image: myapp:latest# Build with cache
docker compose build --pull
# Push cache layer
docker tag myapp:latest myapp:cache
docker push myapp:cacheVolume Performance (macOS/Windows)
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:consistentWarning
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
services:
app:
volumes:
- .:/app
# Use anonymous volume to exclude node_modules
- /app/node_modules
- /app/.next
- /app/distDevelopment Utilities
Make Commands
# 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 -fShell Aliases
# ~/.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
# 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 -vComplete Development Setup
Project Structure
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.mdComplete docker-compose.yml
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
| Command | Purpose |
|---|---|
docker compose up | Start development |
docker compose up --build | Rebuild and start |
docker compose watch | Start with file sync |
docker compose exec app sh | Shell into container |
docker compose logs -f app | Follow logs |
Debugging Ports
| Language | Default Port | Flag/Config |
|---|---|---|
| Node.js | 9229 | --inspect=0.0.0.0:9229 |
| Python | 5678 | debugpy |
| Go | 2345 | Delve |
| Ruby | 1234 | ruby-debug-ide |
| PHP | 9003 | Xdebug |
In the next chapter, we'll explore Docker registries and how to distribute your images.