ToolsOps

Docker Compose for PostgreSQL step by step

A working PostgreSQL compose.yaml with a persistent volume, pg_isready healthcheck, .env.example and the key commands, plus why changing the password fails if you keep the old volume.

This guide sets up a PostgreSQL database with Docker Compose for local development: the database your app uses while you code, without installing Postgres on your machine. It is aimed at people starting with Docker and at those who already use it but want a clean compose that does not lose data or leak passwords.

Minimal, working compose.yaml

Save this as compose.yaml. It has no version: key because that is obsolete in Compose v2.

services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

volumes:
  postgres_data:

And here is the matching .env.example. Copy it to .env, set a real password and add .env to your .gitignore.

POSTGRES_DB=app
POSTGRES_USER=app
POSTGRES_PASSWORD=CHANGE_ME

Why each part is there

  • image: postgres:16-alpine. A pinned version, not :latest. That way a docker compose pull tomorrow does not change the major version on you without warning.
  • environment with ${VARIABLE}. Credentials are interpolated from the .env, not written in plaintext in the compose. The real secret never enters git.
  • volumes: postgres_data. Data lives in a named volume mounted at /var/lib/postgresql/data. Without it, a down would lose the whole database.
  • ports: 127.0.0.1:5432:5432. Publishes Postgres on your loopback only, so you can connect with psql or a GUI client without opening it to the network.
  • healthcheck with pg_isready. Checks that Postgres accepts connections. The command uses $$POSTGRES_USER: the $$ makes Compose pass a single $ to the container, where the variable does exist at runtime.

Day-to-day commands

docker compose up -d            # start in the background
docker compose ps               # service status and health
docker compose logs -f postgres # follow Postgres logs
docker compose exec postgres \
  psql -U app -d app            # open a psql session inside
docker compose down             # stop and remove containers (data safe)
docker compose down -v          # ALSO remove the volume (data deleted)

The difference between down and down -v is exactly the one that scares people: -v removes the volume and, with it, the entire database. Use it on purpose when you want to start fresh, never out of habit.

The classic mistake: I changed the password and it doesn't work

This is the number one question with Postgres in Docker. You change POSTGRES_PASSWORD in the .env, restart, and it still asks for the old password. Nothing is broken: the Postgres image only initializes the user and database the first time, when the volume is empty. On later starts the data already exists and the POSTGRES_* variables are ignored.

You have two paths, depending on whether you want to keep the data:

  • Start fresh (development): docker compose down -v to remove the volume, then docker compose up -d. Postgres reinitializes with the new password.
  • Keep the data: change the password via SQL: docker compose exec postgres psql -U app -d app -c "ALTER USER app PASSWORD 'new';", and update the .env to match.

Local and development, not production

This compose is honest about its scope: it is for coding against a real database on your machine. It is not production hardening. Before putting Postgres in front of users you will need, at a minimum, automated and tested backups, a strong password kept out of git, usually no port published to the host, and a deliberate decision about version upgrades. The healthchecks and secrets guide goes into more detail on those decisions.

Next steps

Generate this stack (with or without a local port, with or without a healthcheck) in the Docker Compose generator. If you are going to connect your own app to this database, continue with Node.js + PostgreSQL or add a web interface with PostgreSQL + pgAdmin.

Frequently asked questions

I changed POSTGRES_PASSWORD in .env but the new one doesn't work, why?
Because the Postgres image only creates the user and database the first time, when the data directory is empty. If you already started the container once, the postgres_data volume keeps the original password and the environment variables are ignored on later starts. To start fresh in development: docker compose down -v (removes the volume) and bring it up again. If you need to keep the data, change the password with SQL via ALTER USER, not via the variable.
Why do I use 127.0.0.1:5432:5432 and not 5432:5432?
With 5432:5432 you publish Postgres on all of your machine's interfaces, so anyone on your network could try to connect. Binding it to 127.0.0.1 means only your own machine reaches it (psql, a GUI client), which is usually what you want in development. If another container in the same compose needs the database, you do not need to publish a port: it connects by the service name (postgres:5432) on the internal network.
Do I need the healthcheck if this is just local development?
It is not mandatory, but it is cheap and useful. pg_isready tells you when Postgres actually accepts connections, not just when the container started. As soon as you add an app that depends on the database, that healthcheck is what lets depends_on with condition: service_healthy wait correctly and avoids connection errors on startup.
How do I back up the database?
A volume is not a backup. For a logical dump use pg_dump inside the container: docker compose exec postgres pg_dump -U app app > backup.sql. Store that file off the host and, seriously, try restoring it now and then: a backup you have never restored is not a backup you know works.
Is this compose good for production?
It is a good starting point for development, not a production deployment. For production you need automated, tested backups, a strong password managed outside git, usually no published port to the host, resource limits and a strategy for upgrading Postgres major versions. Treat it as the base to build on, not the finished result.