How to Install PostgreSQL with Docker
Table of contents
- Key takeaways
- Why run PostgreSQL in Docker?
- What does the docker-compose.yml with a persistent volume look like?
- What environment variables does the image need?
- How do you connect to PostgreSQL from other containers?
- How do you make backups with pg_dump?
- How do you tune performance and the healthcheck?
- Frequently asked questions
- Why should I not use the latest tag in production?
- Is the data lost if I delete the container?
- Should I expose port 5432 to the Internet?
- Conclusion
- Sources
PostgreSQL is the most advanced open-source relational database, and in Docker you bring it up with a single docker-compose.yml file. You define a persistent volume, the superuser password and a pg_isready healthcheck, and within seconds you have a server on port 5432 ready for other containers to connect to it by the service name.
PostgreSQL is the most widely used relational database in self-hosted projects, and with Docker you have it running in under a minute. In this guide you bring up a PostgreSQL server with a single docker-compose.yml file, store the data in a persistent volume, set the essential environment variables, connect other containers over the internal network, make backups with pg_dump and add a healthcheck so the database starts up reliably. The same explanation is available in Spanish.
Key takeaways
- PostgreSQL is the most advanced open-source relational database; its latest major version, PostgreSQL 18, was released on 25 September 2025.
- The official
postgresimage on Docker Hub ships two variants: the Debian-based one (default) and the Alpine one (postgres:18-alpine), much lighter, with an idle footprint of about 130 MB of RAM. - Pin a specific version such as
postgres:18-alpineinstead of:latest, because a floating tag can jump to a new major version without warning during a simple restart. - In PostgreSQL 18 the data path changed: the volume is now mounted at
/var/lib/postgresql(previously/var/lib/postgresql/data), a detail that breaks many older guides. - The server listens on port 5432, and other containers in the same stack connect to it by the service name (
db), without exposing the port to the Internet.
Why run PostgreSQL in Docker?
PostgreSQL is an object-relational database management system with more than 35 years of development behind it. It is the database under a huge part of the self-hosted ecosystem: Nextcloud, Paperless-ngx, Wiki.js, Gitea and Outline all use it as their main store. Running it in a container has a clear advantage: instead of installing packages on the host, managing repositories and dragging in dependencies, you start a ready-made, isolated image, and all the configuration lives in a file you can version.
The official image handles the heavy lifting for you. The first time it starts, it runs initdb, creates the superuser and the initial database from the environment variables, and leaves the server listening on port 5432. If one day you want to store embeddings and do semantic search, there is a variant with the pgvector extension, which we cover in the PostgreSQL with pgvector guide; here we focus on plain PostgreSQL as a general-purpose relational database.
What does the docker-compose.yml with a persistent volume look like?
Create a folder for the project and save this docker-compose.yml inside it. Change the cambia_esta_clave_segura value to your own strong password before starting:
services:
db:
image: postgres:18-alpine
restart: unless-stopped
shm_size: 128mb
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: cambia_esta_clave_segura
POSTGRES_DB: appdb
volumes:
- pg_data:/var/lib/postgresql
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pg_data:
A few details are worth pausing on. The named volume pg_data is mounted at /var/lib/postgresql, the correct path as of PostgreSQL 18; if you copy an old guide that mounts /var/lib/postgresql/data, the container will store the data in the wrong place and you will lose it when you recreate it. To understand the difference between a named volume and a bind mount, see the guide on Docker volumes and bind mounts. The shm_size: 128mb option widens the shared memory, because Docker limits /dev/shm to 64 MB by default and parallel queries or large indexes can run short. And by publishing the port as 127.0.0.1:5432:5432 you keep it reachable only from the machine itself, never exposed to the Internet.
With the file ready, start the stack in the background and check the status:
docker compose up -d
docker compose ps
docker compose logs -f db
The first time, PostgreSQL initialises the data directory and creates the appdb database, so the boot takes a few seconds. When the log shows database system is ready to accept connections, the server is up.
What environment variables does the image need?
The image is configured entirely through environment variables on the first boot. Only one is mandatory:
POSTGRES_PASSWORD: the superuser password. It has no default, so without it the container refuses to start.POSTGRES_USER: the superuser name. If you omit it,postgresis used.POSTGRES_DB: the database created at startup. If you do not set it, it takes the same name asPOSTGRES_USER.POSTGRES_INITDB_ARGS: extra arguments forinitdb, for example--data-checksumsto enable checksums that detect on-disk corruption.
One important detail: these variables only take effect the first time, when the volume is empty and initdb runs. If you change POSTGRES_PASSWORD with data already present, the password will not update by itself; you will have to change it from inside with ALTER USER. That is why you should not put passwords in plain text inside a production docker-compose.yml: it is better to load them from an .env file or, even better, with environment variables and secrets in Docker.
How do you connect to PostgreSQL from other containers?
This is where Docker shines. All the services defined in the same docker-compose.yml share an internal network and discover each other by their service name. An application in the same stack connects to the database using db as the host and 5432 as the port, with no need to publish anything externally. The typical connection string would be:
postgres://app:cambia_esta_clave_segura@db:5432/appdb
Notice that the host is db, not localhost or an IP address: inside the Compose network, Docker’s internal DNS resolves db to the database container’s IP. For an app that depends on PostgreSQL, ideally it should not start until the database is healthy; you achieve that by combining the earlier healthcheck with depends_on, as explained in the guide on healthchecks and restart policies.
If you would rather manage the database with a web interface instead of the command line, you can add a client such as Adminer or pgweb to the same network; we cover it in how to manage databases with Adminer and pgweb. And for a quick query from the container itself, psql is at hand:
docker compose exec db psql -U app -d appdb
How do you make backups with pg_dump?
The golden rule is that your data is worth far more than the container. The pg_data volume holds the whole database, but a logical dump with pg_dump is more portable and lets you restore on any version. Generate a dump in the custom format (compressed and suitable for selective restore) straight from the container:
docker compose exec db pg_dump -U app -Fc appdb > appdb-$(date +%F).dump
To restore it on a clean server, use pg_restore against the already-created database:
cat appdb-2026-07-16.dump | docker compose exec -T db pg_restore -U app -d appdb --clean
If you manage several databases and want to include the global roles and permissions too, use pg_dumpall instead of pg_dump. Automate the dump with a cron job and keep the copies off the server; a copy that lives on the same machine as the database is not a real backup.
How do you tune performance and the healthcheck?
With the default configuration, PostgreSQL reserves a shared_buffers of 128 MB, a conservative value meant to start anywhere. On a server dedicated to the database, the classic recommendation is to raise it to around 25% of the system’s RAM. You can pass startup parameters without touching postgresql.conf by adding a command to the service:
command: >
postgres
-c shared_buffers=256MB
-c max_connections=100
-c work_mem=16MB
The healthcheck we already included uses pg_isready, the official tool that checks whether the server accepts connections. With interval: 10s and retries: 5, Docker marks the container as healthy as soon as the database responds, and as unhealthy if it stops. That status is exactly what depends_on: condition: service_healthy needs so your applications wait for a genuinely ready database, rather than failing to connect during startup.
Frequently asked questions
Why should I not use the latest tag in production?
Because :latest always points to the newest published version and can drag you into a major version jump during a routine restart, with incompatible changes to the data format. By pinning postgres:18-alpine you stay on the 18 branch (the latest major, from September 2025) and decide when you upgrade. A jump between PostgreSQL major versions requires pg_upgrade or a dump and restore; it is never automatic.
Is the data lost if I delete the container?
No, as long as you use a named volume like in this guide. The container is ephemeral, but the pg_data volume survives docker compose down and is mounted again when you recreate the service. You would only lose the data if you run docker compose down -v, which also removes volumes, or if you mount the wrong path. Remember that in PostgreSQL 18 the volume goes at /var/lib/postgresql, not /var/lib/postgresql/data.
Should I expose port 5432 to the Internet?
No. Publishing it as 127.0.0.1:5432:5432 keeps it reachable only from the machine itself, which is the usual case when the app and the database live on the same server. If you really need remote access, do it through a VPN or an SSH tunnel and protect it with a strong password; exposing PostgreSQL directly on the public network is one of the most common causes of data compromise.
Conclusion
With a single docker-compose.yml you have a persistent PostgreSQL server, with its volume, its environment variables and a healthcheck that guarantees reliable startups. From here, the database is ready to serve the rest of your self-hosted infrastructure: connect your applications by the service name, add a web manager with Adminer or pgweb and, above all, schedule regular backups with pg_dump and keep them off the server. That is the difference between having a database and having a database you can recover.