Development Workflows and Best Practices
LEVEL 0
The Problem
You’re developing locally. You make code changes. You want to see the result. But your code is inside a container. Do you:
- Rebuild the image every time?
- Restart the container every time?
- Somehow reload code without restarting?
You have a database with test data. You stop Compose with docker compose down. When you start again, the data is gone.
You want to run tests. You want to run migrations. You want to open a shell in a container.
Development workflows with Compose require specific patterns.
LEVEL 1
The Concept — The Workshop
The Concept
Imagine a woodworking workshop.
Production mode: You build furniture, varnish it, deliver it. Everything is finished, sealed, permanent.
Development mode: You’re prototyping. You need to:
- Make changes and see them immediately (no waiting for varnish to dry)
- Test different configurations (swap parts easily)
- Preserve your work-in-progress (don’t throw away the prototype when you take a break)
Development with Docker is the same. You need:
- Live reloading — see code changes without rebuilding
- Easy testing — run tests, access shells, inspect state
- Data persistence — keep database data between restarts
LEVEL 2
The Mechanics — Development Setup
The Mechanics
Live code reloading with bind mounts:
services:
app:
build: ./app
volumes:
- ./app:/app # Mount source code
environment:
- DEBUG=true
- FLASK_ENV=development
command: flask run --reload --host=0.0.0.0
Now when you edit code in ./app, changes are immediately reflected in the container. Most frameworks (Flask, Django, Node.js with nodemon, etc.) support auto-reload.
Preserving database data:
services:
db:
image: postgres:15
volumes:
- db-data:/var/lib/postgresql/data # Named volume persists
environment:
POSTGRES_PASSWORD: devpass
volumes:
db-data:
Even if you run docker compose down, the db-data volume persists. Data survives restarts.
To wipe data:
docker compose down -v # Remove volumes too
LEVEL 3
Common Development Commands
Start services in foreground (see logs):
docker compose up
Start in background:
docker compose up -d
Rebuild and start (after changing Dockerfile):
docker compose up --build
Restart a single service:
docker compose restart app
View logs:
docker compose logs -f # All services
docker compose logs -f app # One service
docker compose logs --tail=50 app # Last 50 lines
Execute commands in running container:
docker compose exec app sh # Open shell
docker compose exec app python manage.py migrate # Run migration
docker compose exec db psql -U postgres # Open database shell
Run one-off commands (creates new container):
docker compose run app pytest # Run tests
docker compose run app python manage.py createsuperuser
docker compose run --rm app npm install # --rm removes container after
Stop without removing:
docker compose stop
Stop and remove containers (but keep volumes):
docker compose down
Complete cleanup:
docker compose down -v --rmi all # Remove containers, volumes, images
LEVEL 4
Override Files for Different Environments
Base configuration: docker-compose.yml
version: '3.9'
services:
app:
build: ./app
environment:
- DATABASE_URL=postgresql://db:5432/myapp
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Development override: docker-compose.override.yml
version: '3.9'
services:
app:
volumes:
- ./app:/app # Bind mount for live reload
environment:
- DEBUG=true
command: flask run --reload --host=0.0.0.0
db:
ports:
- "5432:5432" # Expose DB port for local access
By default, Compose reads both docker-compose.yml AND docker-compose.override.yml and merges them.
For development:
docker compose up # Uses both files
Production override: docker-compose.prod.yml
version: '3.9'
services:
app:
restart: always
environment:
- DEBUG=false
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
For production:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
The -f flag specifies which files to use.
LEVEL 5
Best Practices
1. Use .dockerignore
Prevent unnecessary files from being copied during build:
.dockerignore
node_modules
.git
.env
*.md
__pycache__
.pytest_cache
2. Use .gitignore
Don’t commit generated or secret files:
.gitignore
.env
.env.local
docker-compose.override.yml # If it contains local-specific config
3. Document setup in README
## Getting Started
1. Copy environment template:
cp .env.example .env
2. Start services:
docker compose up
3. Run migrations:
docker compose exec app python manage.py migrate
4. Access application at http://localhost:8000
4. Use healthchecks
services:
app:
image: myapp
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 3s
retries: 3
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
timeout: 3s
retries: 5
5. Name volumes explicitly
volumes:
postgres-data:
redis-data:
app-logs:
Better than anonymous volumes—easier to find and manage.