← All posts
DevOps7 min readProject: Docker setups across my projects

Where the data lives: Docker volumes, dev vs. prod databases, dumps and a preconfigured pgAdmin

Containers are meant to be thrown away. Databases are not. Here is how I keep PostgreSQL, Redis and pgAdmin data safe across rebuilds with named volumes, why development and production never share a volume, which Docker command really deletes data, and how a committed dump plus a preconfigured pgAdmin gives every new developer a working database in one command.

#Docker#Docker Volumes#PostgreSQL#Redis#pgAdmin#Backups

The problem

Rebuilding containers risked losing database data, dev and prod data could mix, and every new developer started with an empty database and a manual pgAdmin setup.

The result

Data lives in named volumes that survive rebuilds, dev and prod are fully separate, and a new developer gets sample data and a connected pgAdmin with one command.

Stack

DockerDocker ComposePostgreSQLRedispgAdmin

The problem

Docker containers are disposable by design. You rebuild them, recreate them, delete them, and that's a good thing: a fresh container is a clean container.

But a database container holds something that is not disposable: the data. If PostgreSQL writes its files inside the container, every rebuild starts from an empty database. The first time that happens in development, it's annoying. In production, it's a disaster.

On top of that, I had three recurring needs:

  • development and production data must never mix, even on the same machine,
  • a new developer should get realistic data, not an empty schema,
  • looking inside the database shouldn't require installing and configuring tools by hand.

The details

Where PostgreSQL writes

PostgreSQL stores everything in its data directory: /var/lib/postgresql/data in the official image. Redis writes to /data. pgAdmin keeps its settings in /var/lib/pgadmin. Without any configuration, those paths live in the container's writable layer, which disappears with the container.

Two ways to keep files outside a container

  • Bind mounts: a folder from your machine mounted into the container (./data:/var/lib/postgresql/data).
  • Named volumes: storage managed by Docker, referenced by name (postgres_data:/var/lib/postgresql/data).

For database files, I always use named volumes:

Named volumeBind mount
Managed byDockeryour file system
File permissionshandled by Dockeroften wrong (database refuses to start)
Performance on macOS/Windowsgoodcan be slow
Survives docker compose up --force-recreate✔✔
Accidentally edited or deleted by handunlikelyeasy

Bind mounts are great for source code you want to edit live. For database files, a named volume is simpler and safer.

The challenges

Knowing which commands delete data

Docker has many commands that sound destructive. Only some of them actually are. Misunderstanding them either makes people afraid to rebuild anything, or makes them delete data without realizing it.

Dev and prod side by side

On a server, I sometimes run a development or staging stack next to production. If both stacks used a volume called postgres_data in the same Compose project, they would literally be the same files.

Empty databases slow everyone down

An empty database means every developer creates test data by hand, differently, and bugs that only appear with real-looking data stay hidden.

pgAdmin setup is repetitive

Every time pgAdmin starts fresh, someone has to register the server: host, port, database, user. It's five minutes of clicking, done again and again, and usually done wrong once ("localhost" from inside a container…).

The solution

1. A named volume for every stateful service

services:
  postgres:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  pgadmin:
    image: dpage/pgadmin4:latest
    volumes:
      - pgadmin_data:/var/lib/pgadmin

volumes:
  postgres_data:
  redis_data:
  pgadmin_data:

Now the containers can be recreated, upgraded or rebuilt at will. The data stays in the volumes and is mounted again into the new containers.

2. Know exactly which command deletes what

CommandContainersNamed volumes (data)
docker compose restartrestartedkept
docker compose up -d --build --force-recreaterebuilt and recreatedkept
docker compose downremovedkept
docker compose down -vremovedDELETED
docker volume rm <name>—DELETED
docker system prune --volumes—unused volumes DELETED

The rule I teach everyone: -v and --volumes are the dangerous flags. Everything else keeps the data. That's also why automatic deploys can safely run up -d --build --force-recreate on every push: the code changes, the database files don't.

3. Separate volumes for dev and prod

Each environment gets its own volume names and, more importantly, its own Compose project:

# docker-compose.dev.yml
volumes:
  postgres_data_dev:
  redis_data_dev:
  pgadmin_data_dev:
# docker-compose.prod.yml
name: myapp_prod
volumes:
  myapp_postgres_data_prod:
  myapp_redis_data_prod:

Volume names are prefixed with the project name by Docker, so separate projects already keep volumes apart. Explicit _dev / _prod names add a second, human-readable layer: when I list volumes on a server, I can see immediately which one is production. (The full story of why the project name matters is in "One command, a whole stack".)

A dev-only Compose file can also be much smaller. For one service, the dev file is just PostgreSQL on its own port, with its own volume, while the app runs on my machine with hot reload:

services:
  postgres-dev:
    image: postgres:16-alpine
    ports:
      - '5437:5432'
    volumes:
      - postgres_dev_data:/var/lib/postgresql/data

4. Never point a local stack at production by accident

Production databases are protected by more than volume names:

  • connection strings come from environment files that differ per environment, never hard-coded in the image,
  • dangerous flags (like allowing a local app to connect to a production database) are explicit, named variables that someone has to set on purpose,
  • secrets (.env.production) are never committed; Compose reads them with env_file:.

5. A committed sample dump

For a new project, I commit a SQL dump with sample data in a backup/ folder, with a short README explaining how to load it:

# Load the sample data into the running database container
docker compose exec -T postgres \
  psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" < backup/sample_dump.sql
# Create a fresh dump from the running container
docker compose exec -T postgres \
  pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" > backup/sample_dump.sql

A new developer runs docker compose up -d, loads the dump, and has realistic data in a minute. Bugs that depend on real-looking data show up in development, not after release.

(The sample dump contains test data only. Real production data never goes into a repository.)

6. pgAdmin that is already connected

pgAdmin can read a servers.json file at startup. I commit one and mount it read-only:

{
  "Servers": {
    "1": {
      "Name": "My project",
      "Group": "Servers",
      "Host": "postgres",
      "Port": 5432,
      "MaintenanceDB": "app",
      "Username": "app",
      "SSLMode": "prefer"
    }
  }
}
pgadmin:
  image: dpage/pgadmin4:latest
  environment:
    PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
    PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
    PGADMIN_CONFIG_SERVER_MODE: 'False'
    PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
  ports:
    - '${PGADMIN_PORT:-5060}:80'
  volumes:
    - pgadmin_data:/var/lib/pgadmin
    - ./docker/pgadmin/servers.json:/pgadmin4/servers.json:ro
  depends_on:
    postgres:
      condition: service_healthy

Key details:

  • Host: postgres, the service name. pgAdmin runs inside the Compose network, so localhost would point at pgAdmin itself.
  • Server mode off and no master password for local development: open the browser, and the database is already in the sidebar. (For anything exposed beyond a developer machine, keep authentication on.)
  • :ro: the container can read the config but never modify the committed file.
  • depends_on: service_healthy: pgAdmin starts after the database is ready.

7. Defaults that work out of the box

Every value in the compose file has a default with ${VAR:-default}, and .env.example documents the variables:

ports:
  - '${POSTGRES_PORT:-5440}:5432'

The stack runs with zero configuration on a developer machine, and every value can still be overridden per environment.

Lessons learned

  • Containers are disposable; data isn't. Every stateful service gets a named volume.
  • Use named volumes for databases, bind mounts for code. Fewer permission problems, better performance.
  • Learn the destructive flags. down -v, volume rm and prune --volumes delete data; rebuilds don't.
  • Separate dev and prod completely. Different projects, different volume names, different env files.
  • Ship sample data. A committed test dump makes every developer productive in minutes.
  • Preconfigure your tools. A read-only servers.json turns pgAdmin setup into nothing.
  • Use service names everywhere inside Docker. Including in tool configs like pgAdmin.
  • Default everything, override anything. ${VAR:-default} keeps local setup at zero effort.

FAQ

Does docker compose down delete my database?

No. docker compose down removes containers and networks but keeps named volumes. Only docker compose down -v (or removing the volume explicitly) deletes the data.

Should I use a bind mount or a named volume for PostgreSQL data?

A named volume. It avoids file permission issues, performs better on macOS and Windows, and is harder to delete or modify by accident. Use bind mounts for source code you edit.

How do I import a SQL dump into a PostgreSQL Docker container?

Pipe the file into psql inside the container, for example docker compose exec -T postgres psql -U user -d db < dump.sql. The -T flag disables the TTY so input redirection works.

How can pgAdmin connect automatically to my Docker PostgreSQL?

Mount a servers.json file into the pgAdmin container at /pgadmin4/servers.json. Use the PostgreSQL service name as the host, since both containers share the Compose network.

Facing a similar problem?

I help teams fix performance, backend and reliability issues in production.

Let's talk
Welcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have fun

Componentity

Thanks for visiting my portfolio. Every project here reflects my passion for building and learning. If you’re interested in working together, have a question, or just want to connect—don’t hesitate to reach out. I'm always open to meaningful conversations and new ideas.

© 2026 Zakariya Zouazou. All Rights Reserved.

Contact Us