Docker Compose · Lesson 3 of 5
Volumes and Persistent Data in Docker Compose
Volume Types
Named volumes (recommended for data persistence):
→ Created and managed by Docker
→ Survive container restarts and recreations
→ Not tied to a specific host directory — portable
→ Use for: database data, Redis persistence, uploaded files
Bind mounts (for development):
→ Maps a host directory into the container
→ Changes on the host are immediately visible in the container
→ Use for: development hot-reload, sharing config files
→ DO NOT use in production — host path must exist
tmpfs mounts (in-memory):
→ Stored in host memory, not on disk
→ Cleared when container stops
→ Use for: temporary data, secrets that should never touch diskNamed Volumes for Database Persistence
# Without a volume: SQL Server data is lost when the container is recreated
# With a named volume: data persists across docker compose down / up
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=${DB_PASSWORD}
volumes:
- sqlserver-data:/var/opt/mssql # named volume → SQL Server data directory
healthcheck:
test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${DB_PASSWORD} -Q "SELECT 1"
interval: 10s
retries: 10
start_period: 30s
redis:
image: redis:7-alpine
command: redis-server --appendonly yes # enable Redis AOF persistence
volumes:
- redis-data:/data # named volume → Redis data directory
volumes:
sqlserver-data: # Docker manages this directory
redis-data:Bind Mounts for Development
# Development compose override — hot-reload with volume-mounted source code
# File: docker-compose.override.yml (applied on top of docker-compose.yml in development)
services:
prescription-service:
build:
context: ./src/Services/PrescriptionService
dockerfile: Dockerfile
volumes:
# Mount source code for hot-reload (dotnet watch)
- ./src/Services/PrescriptionService:/app/src:ro
environment:
- ASPNETCORE_ENVIRONMENT=Development
- DOTNET_USE_POLLING_FILE_WATCHER=true
command: ["dotnet", "watch", "run", "--project", "src/PrescriptionService.csproj"]
# docker-compose.override.yml is loaded automatically alongside docker-compose.yml
# For production: docker compose -f docker-compose.yml up (ignores override)Read-Only Configuration Mounts
# Mount configuration files into containers as read-only
# Useful for: Nginx config, certificates, appsettings overrides
services:
nginx:
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # :ro = read-only
- ./nginx/ssl:/etc/nginx/ssl:ro
prescription-service:
image: clinical/prescription-service:latest
volumes:
# Override appsettings with environment-specific file (read-only)
- ./config/appsettings.Integration.json:/app/appsettings.Integration.json:roSharing Volumes Between Services
# Use case: prescription-service writes PDFs, nginx serves them
# Both share the same volume
services:
prescription-service:
image: clinical/prescription-service:latest
volumes:
- prescription-pdfs:/app/generated-pdfs # writes PDFs here
nginx:
image: nginx:alpine
volumes:
- prescription-pdfs:/usr/share/nginx/html/pdfs:ro # reads from same volume (read-only)
# nginx serves /pdfs/prescription-{id}.pdf directly without routing through the API
volumes:
prescription-pdfs:Volume Management Commands
# List all volumes
docker volume ls
# Inspect a specific volume (shows mount point on host)
docker volume inspect clinical_sqlserver-data
# Remove a specific volume (WARNING: deletes data)
docker volume rm clinical_sqlserver-data
# Remove all unused volumes (dangerous — removes orphaned volumes)
docker volume prune
# Remove everything (containers, networks, volumes) — full reset
docker compose down -v
# -v removes named volumes — data is DELETED
# Without -v: containers stop but volumes persist (data preserved)
# Backup a named volume
docker run --rm \
-v clinical_sqlserver-data:/data \
-v $(pwd)/backup:/backup \
alpine tar czf /backup/sqlserver-backup.tar.gz -C /data .
# Restore from backup
docker run --rm \
-v clinical_sqlserver-data:/data \
-v $(pwd)/backup:/backup \
alpine tar xzf /backup/sqlserver-backup.tar.gz -C /dataVolume Strategy for Different Environments
# Development: use bind mounts + named volumes (convenience over isolation)
# Testing: use tmpfs (fast, clean slate every run)
# Production: named volumes with backup strategy (or external managed storage)
# Testing compose (fastest startup, no persistence between test runs):
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
tmpfs:
- /var/opt/mssql/data # in-memory — test data lost when container stops
# Fast startup, clean state for each test run
redis:
image: redis:7-alpine
tmpfs:
- /data # in-memory Redis — no persistence needed for testsProduction issue I've seen: A team ran
docker compose downto restart their environment and then randocker compose up— but their SQL Server container showed no data after restart. They had been using the default (no volume), so all data lived inside the container's writable layer. Everydocker compose downdestroyed the data. They had been seeding the database manually on each restart. Adding a named volume (sqlserver-data:/var/opt/mssql) took 30 seconds to configure. After that,docker compose downpreserved the data.docker compose down -vwas reserved for "full reset" when they needed a clean slate. Understanding the difference betweendown(keeps volumes) anddown -v(removes volumes) is essential.
Key Takeaway
Named volumes persist data across container restarts and recreations — always use them for SQL Server, Redis, and uploaded files. Bind mounts (
./src:/app/src) are for development hot-reload and config file injection only — not for production. Use:ro(read-only) for config file mounts to prevent accidental container writes.docker compose downpreserves named volumes;docker compose down -vremoves them (data deleted). Never usedown -vwithout understanding what data will be destroyed.