Learnixo
Back to blog
AI Systemsintermediate

Docker Compose Volumes — Persisting Data and Sharing Files

Configure Docker Compose volumes for .NET applications: named volumes for database persistence, bind mounts for development, read-only configuration mounts, and volume management strategies.

Asma Hafeez KhanMay 16, 20265 min read
Docker ComposeDockerVolumes.NETContainers
Share:š•

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 disk

Named Volumes for Database Persistence

YAML
# 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

YAML
# 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

YAML
# 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:ro

Sharing Volumes Between Services

YAML
# 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

Bash
# 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 /data

Volume Strategy for Different Environments

YAML
# 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 tests

Production issue I've seen: A team ran docker compose down to restart their environment and then ran docker 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. Every docker compose down destroyed 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 down preserved the data. docker compose down -v was reserved for "full reset" when they needed a clean slate. Understanding the difference between down (keeps volumes) and down -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 down preserves named volumes; docker compose down -v removes them (data deleted). Never use down -v without understanding what data will be destroyed.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.