“Works on my machine” is a classic developer problem. One developer runs Python 3.11, another runs 3.13. One uses PostgreSQL 15, another uses 16. The app behaves differently on every machine.
Docker solves this. With Docker, your entire development environment — app, database, cache, message queue — is defined in a YAML file. Every developer on your team runs identical environments with one command.
Why Use Docker for Development?
- Consistent environments — everyone runs the same versions of everything
- No installation required — no need to install PostgreSQL, Redis, or Node locally
- Easy onboarding — new team members run
docker compose upand have everything - Clean isolation — each project has its own versions, no conflicts between projects
- Production parity — dev environment matches production as closely as possible
The Development Compose Pattern
A common pattern is to have two Compose files:
docker-compose.yml— base configuration (works for both dev and prod)docker-compose.override.yml— development overrides (automatically applied when you rundocker compose up)
Docker Compose automatically merges docker-compose.override.yml on top of docker-compose.yml.
docker-compose.yml (base):
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_USER: alex
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U alex -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data:
docker-compose.override.yml (dev overrides):
services:
api:
build:
dockerfile: Dockerfile.dev # use the dev Dockerfile
environment:
APP_ENV: development
LOG_LEVEL: debug
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: requirements.txt
db:
ports:
- "5432:5432" # expose db port locally (for GUI tools)
In production, you deploy only docker-compose.yml and do not apply the override.
Compose Watch Mode: The Best Dev Experience
Compose Watch Mode (now stable, requires Docker Compose 2.22+) is the modern way to do hot reload with Docker. It watches your files and syncs changes into running containers automatically.
Start with watch mode:
docker compose watch
Or:
docker compose up --watch
Three Watch Actions
sync — copy changed files into the running container without restarting. Best for interpreted languages:
develop:
watch:
- action: sync
path: ./src/python
target: /app/src
Save a Python file → it is instantly available inside the container. The running server (Flask/FastAPI with --reload) picks it up automatically.
rebuild — rebuild the image and restart the container when a file changes. Best for compiled languages:
develop:
watch:
- action: rebuild
path: ./src/go
Save a Go file → Docker rebuilds the image and restarts the container. Slower than sync, but necessary for compiled code.
sync+restart — sync the file then restart the container process. Best for config files:
develop:
watch:
- action: sync+restart
path: ./config/nginx.conf
target: /etc/nginx/conf.d/default.conf
Hot Reload for Python/FastAPI
# docker-compose.override.yml
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
command: uvicorn main:app --reload --host 0.0.0.0 --port 8080
develop:
watch:
- action: sync
path: ./app
target: /app/app
- action: rebuild
path: requirements.txt
Dockerfile.dev (for Python development):
FROM python:3.13-slim
WORKDIR /app
# Install dev dependencies too
COPY requirements.txt requirements-dev.txt ./
RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt
COPY . .
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]
The --reload flag makes uvicorn watch for Python file changes and reload automatically. Combined with Compose sync, changes appear instantly in the running container.
Hot Reload for Node.js
services:
api:
build: .
command: node --watch src/index.js
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
Node.js 18+ has built-in --watch mode. No need for nodemon.
Rebuild Workflow for Go
Go must be compiled. Use rebuild action:
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
develop:
watch:
- action: rebuild
path: .
ignore:
- "*.md"
- ".git"
Dockerfile.dev (Go):
FROM golang:1.24-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o myapp .
CMD ["./myapp"]
Database Setup for Development
Here is a development setup with PostgreSQL and Redis, including GUI tools:
services:
api:
build: .
environment:
DATABASE_URL: postgresql://alex:secret@db:5432/myapp
REDIS_URL: redis://cache:6379
db:
image: postgres:17
environment:
POSTGRES_USER: alex
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
ports:
- "5432:5432" # Access from host with psql or GUI
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U alex -d myapp"]
interval: 10s
retries: 5
pgadmin:
image: dpage/pgadmin4
profiles:
- debug
environment:
PGADMIN_DEFAULT_EMAIL: alex@example.com
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
depends_on:
- db
cache:
image: redis:7-alpine
ports:
- "6379:6379"
redis-insight:
image: redis/redisinsight:latest
profiles:
- debug
ports:
- "5540:5540"
volumes:
db_data:
Start with debug tools:
docker compose --profile debug up
Start without debug tools:
docker compose up
Devcontainers: Full Dev Environment in a Container
Devcontainers go one step further. Instead of just running your services in Docker, you run your entire development environment — including your code editor’s tools, extensions, and terminal — inside a container.
VS Code has first-class support via the Dev Containers extension.
Create .devcontainer/devcontainer.json:
{
"name": "My App Dev",
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.dev.yml"],
"service": "api",
"workspaceFolder": "/workspace",
"extensions": [
"ms-python.python",
"ms-python.pylance",
"ms-azuretools.vscode-docker"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python"
},
"postCreateCommand": "pip install -r requirements-dev.txt"
}
When you open the project in VS Code, it asks if you want to “Reopen in Container”. Say yes, and VS Code runs inside your Docker container. Your terminal, Python interpreter, linter — everything runs inside Docker.
Benefits:
- Zero local installation needed (just VS Code + Docker)
- Everyone on the team has identical development tools
- CI and local environments match exactly
Environment Variable Management
Use a layered approach:
.env— default values committed to git (no secrets).env.local— local overrides, not committed to git
.env (committed, no secrets):
APP_ENV=development
LOG_LEVEL=debug
DB_NAME=myapp
DB_USER=alex
API_PORT=8080
.env.local (not committed, has secrets):
DB_PASSWORD=my_local_password
API_SECRET_KEY=dev_secret_key_123
In docker-compose.yml, use env_file::
services:
api:
env_file:
- .env
- .env.local # override with local values
Add to .gitignore:
.env.local
.env.*.local
Common Mistakes
Using sync watch mode for compiled languages
If you use action: sync for a Go or Rust project, Docker copies your source files into the container — but the binary does not get rebuilt. The container keeps running the old binary. Use action: rebuild for compiled languages.
Mounting the entire project as a bind mount in production
In development, bind mounting the code is fine. In production, copy the code into the image with COPY and use named volumes only for data. Bind mounting in production creates a dependency on the host filesystem.
Committing .env files with secrets to git
Keep secrets out of git. Use separate .env.local files for local secrets and a secrets manager (AWS Secrets Manager, HashiCorp Vault) for production. The .env file committed to git should only contain non-sensitive defaults.
What’s Next?
Next: Docker Tutorial #9: Docker Security Best Practices