How to Install PostgreSQL and pgAdmin on a Linux Server (The Database Half Your Apps Want)

PostgreSQL, properly: one shared server or one per stack, a database and user per app, pgAdmin to look inside, pg_dump nightly.

Tutorial 29 set up MariaDB. The other database you keep meeting in compose files — Immich, Gitea, Paperless, Umami, Vikunja, Joplin, Planka — is PostgreSQL. Each of those tutorials ran its own Postgres container, which is fine; but you may want one shared server for apps you install by hand, a web UI to look at the data, and a backup routine that doesn't involve six separate dumps. Here's Postgres as a first-class citizen.

Quick reference

TaskCommand
Shared containerpostgres:16 in ~/docker/postgres, port on 127.0.0.1 or a shared Docker network
On the host insteadsudo apt install postgresql -y
Admin shelldocker compose exec -u postgres db psql (or sudo -u postgres psql)
Database + user per appCREATE USER app WITH PASSWORD '…'; CREATE DATABASE app OWNER app;
Web UIdpage/pgadmin4 on port 5050
Dump / restorepg_dump -Fc app > app.dump · pg_restore -d app app.dump
Everythingpg_dumpall > all.sql

1. One shared server, or one per stack?

One per stack (what the other tutorials did): each app's compose file has its own db: service. Simple, isolated, the app's docs match. Upgrade one without touching the others.

One shared server: a single Postgres on a shared Docker network (proxy-style, tutorial 101) that several apps connect to by name. Less RAM, one backup, one pgAdmin. Better for apps you install by hand or many small ones.

Both are fine. Don't share for Immich — it needs its own build with extensions.

2. A shared Postgres container

mkdir -p ~/docker/postgres && cd ~/docker/postgres
nano compose.yaml

services:
db:
image: postgres:16
container_name: postgres
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: Europe/London
volumes:
- ./data:/var/lib/postgresql/data
- ./backups:/backups
ports:
- "127.0.0.1:5432:5432"
networks: [default, shared]
shm_size: 256mb
restart: unless-stopped

pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: john@example.com
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
volumes:
- ./pgadmin:/var/lib/pgadmin
ports:
- "127.0.0.1:5050:80"
restart: unless-stopped

networks:
shared:
external: true

docker network create shared once; apps that join shared reach the database as postgres:5432. The host port on 127.0.0.1 is for psql from the server and SSH tunnels (62).

docker compose up -d

3. A database and user per app

docker compose exec -u postgres db psql

CREATE USER vikunja WITH PASSWORD 'long-random';
CREATE DATABASE vikunja OWNER vikunja;
\l
\du
\q

In that app's compose file: host postgres, port 5432, those credentials, and networks: [default, shared]. Each app sees only its own database.

4. pgAdmin

Tunnel to it (ssh -L 5050:127.0.0.1:5050 john@server) or proxy it behind Authelia (95). Log in, Add New Server: name, host postgres (or db), user postgres, the password. Browse tables, run SQL, see what Paperless or Immich actually store — and fix the odd row when an app's UI can't.

5. Back up

Logical dumps, never copied data files while running (tutorial 65). Nightly, into the mounted backups folder so restic picks them up:

docker compose exec -T -u postgres db pg_dumpall > /mnt/backup/postgres-$(date +%F).sql

Per-app, in the compressed custom format (faster restore, selective):

docker compose exec -T -u postgres db pg_dump -Fc vikunja > ./backups/vikunja.dump
docker compose exec -T -u postgres db pg_restore -d vikunja --clean ./backups/vikunja.dump

For the per-stack containers from other tutorials, the same pg_dump against each stack's db service.

6. Two settings that matter

Postgres ships tuned for a tiny machine. In the container's environment or postgresql.conf:

And shm_size: 256mb in compose, or parallel queries fail with "could not resize shared memory". pgtune.leopard.in.ua generates the rest from your RAM and workload.

7. Upgrading major versions

Postgres won't start a 16 data directory with the 17 image. Either stay on a major tag (postgres:16) and upgrade deliberately — pg_dumpall on 16, start 17 with an empty data folder, restore — or use the pgautoupgrade image that does it on start. Pin the tag (50).

Troubleshooting

"FATAL: password authentication failed". Wrong user/database pairing, or the app connects as postgres with the app's password. Check \du.

App can't resolve postgres. Not on the shared network, or you used localhost from inside a container (101).

"could not resize shared memory segment". shm_size.

Data folder permission errors after a restore. Must be owned by UID 999 (the postgres user in the image): sudo chown -R 999:999 data.

Wrapping up

Per-stack Postgres for the apps that ship it; a shared one on a Docker network for everything else; a user and database per app; pgAdmin through a tunnel; pg_dumpall nightly; major versions pinned and upgraded on purpose. The database behind half your apps stops being a mystery box.

Call us