Ubuntu ships PostgreSQL in its default package repositories, but those packages lag behind the upstream release schedule by design. On Ubuntu 24.04, you get PostgreSQL 16. On Ubuntu 22.04, you get PostgreSQL 14. Ubuntu snapshots a version at release time and maintains it for the lifetime of that LTS - useful for predictability, but it means you can't easily get the latest PostgreSQL (currently version 18) or specific older versions not shipped by your Ubuntu release without adding an external source.
The PostgreSQL Global Development Group (PGDG) maintains its own apt repository at apt.postgresql.org that tracks upstream releases across all supported Ubuntu versions. This is the installation path most production environments should use. It supports multiple major versions side by side, and the packages are built from the same source as the Debian official packages - there's no quality tradeoff.
Adding the PGDG Repository and Installing PostgreSQL
The PGDG repository uses a signed apt source. The setup involves downloading the signing key, creating the source list entry, then installing the package. These steps work on Ubuntu 22.04 (Jammy), 24.04 (Noble), and later releases.
sudo apt install -y curl ca-certificates
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
--fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
Next, add the repository source file. The command reads the OS codename from /etc/os-release so you don't need to hard-code it:
. /etc/os-release
sudo sh -c "echo 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \
https://apt.postgresql.org/pub/repos/apt $VERSION_CODENAME-pgdg main' \
> /etc/apt/sources.list.d/pgdg.list"
sudo apt update
Now install a specific major version. Replace 18 with whatever version you're targeting:
sudo apt install -y postgresql-18
The package installs the server, client tools, and a default cluster called main. Configuration files land under /etc/postgresql/18/main/, data under /var/lib/postgresql/18/main/. The postgresql meta-package always tracks the latest available major version - useful for fresh installs where you want whatever is newest, less appropriate when you need version pinning.
Simpler alternative: PostgreSQL's official download page now recommends an automated script that handles the repository setup steps above in one go:
sudo apt install -y postgresql-common ca-certificates
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
The script detects your Ubuntu release, downloads the signing key, adds the source list entry, and runs apt update. The manual steps above do the same thing and are useful when you need to understand or script each step individually.
Service Management with systemd
The installer registers a systemd unit automatically and starts the service. On Ubuntu, the service name is version-qualified:
sudo systemctl status postgresql@18-main
sudo systemctl start postgresql@18-main
sudo systemctl stop postgresql@18-main
sudo systemctl restart postgresql@18-main
The generic postgresql unit is a target that starts all clusters on the machine. To control a specific cluster - which matters when you're running multiple major versions - use the versioned unit name above. To make the service start on boot (it usually is by default after install):
sudo systemctl enable postgresql@18-main
Ubuntu also ships pg_ctlcluster, a wrapper around pg_ctl that's cluster-aware. sudo pg_ctlcluster 18 main status does the same job, but systemctl is the right tool for anything integrated into system startup and monitoring.
Configuration Basics: postgresql.conf and pg_hba.conf
Two files govern the majority of operational configuration. postgresql.conf controls server behavior - memory, connections, WAL settings, logging. pg_hba.conf controls client authentication: who can connect, from where, and how they prove identity. Both are in /etc/postgresql/18/main/.
The defaults in postgresql.conf are conservative. max_connections is 100, shared_buffers is 128MB, listen_addresses is localhost. For any non-trivial workload, you'll raise shared_buffers to 25% of available RAM and tune work_mem, effective_cache_size, and wal_buffers to match your workload and hardware. At minimum, change listen_addresses if you need remote connectivity:
# postgresql.conf
listen_addresses = '*' # or a specific IP
max_connections = 200
shared_buffers = 2GB # scale to your server RAM
After editing postgresql.conf, reload with sudo systemctl reload postgresql@18-main. Settings marked with # requires restart in the docs need a full restart.
pg_hba.conf uses a top-down rule match - PostgreSQL applies the first line that matches the connection. The default local rules use peer authentication, which maps the OS user to the database user. That works for the postgres system user connecting to the postgres database role, but breaks for application connections that arrive from a different OS user or over TCP.
Creating a Superuser and Setting Passwords
After install, the only role that exists is postgres. Connect to it via the OS-level peer auth:
sudo -u postgres psql
From there, set a password on the postgres role and create application-specific roles:
ALTER USER postgres WITH PASSWORD 'strongpassword';
CREATE ROLE appuser WITH LOGIN PASSWORD 'apppassword';
GRANT ALL PRIVILEGES ON DATABASE myapp TO appuser;
Note that GRANT ALL PRIVILEGES ON DATABASE only grants database-level privileges (CONNECT, CREATE, TEMP) - it does not grant access to existing tables or sequences inside the database. To give appuser full access to objects in the public schema, connect to the target database and run:
GRANT ALL ON ALL TABLES IN SCHEMA public TO appuser;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO appuser;
-- For tables created in the future:
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO appuser;
Security Hardening: Authentication and Network Exposure
The default pg_hba.conf uses peer for local Unix socket connections and scram-sha-256 (or md5 on older installs) for TCP connections. For remote access, you need a host record that allows connections from specific IP ranges. scram-sha-256 is the right authentication method - it replaced MD5 as the default in PostgreSQL 14 and uses a challenge-response protocol that avoids sending the password in cleartext or as a simple hash.
A minimal pg_hba.conf that allows password auth over the local socket and controlled remote access looks like this:
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local all all scram-sha-256
host all all 10.0.0.0/24 scram-sha-256
Avoid trust for anything beyond local development. Avoid 0.0.0.0/0 in production - scope remote access to known CIDRs or put PostgreSQL behind a private network and connect through a bastion or VPN. The hostssl record type enforces TLS for TCP connections. Note that PostgreSQL PGDG packages on Ubuntu have SSL enabled by default (ssl = on) using a self-signed "snakeoil" certificate generated at install time, so you do not need to explicitly enable it. For production, replace the certificate with one from a recognized CA - set ssl_cert_file and ssl_key_file in postgresql.conf to point to your certificate and private key. Using hostssl with a valid certificate is the minimum for any externally accessible instance.
After editing pg_hba.conf, reload the server - changes take effect without a restart:
sudo systemctl reload postgresql@18-main
One more thing worth noting: PostgreSQL listens only on localhost by default. Even with the right pg_hba.conf rules, remote connections fail until you change listen_addresses and reload. These two settings work together, and misconfiguring one while having the other correct is a common source of connection errors during initial setup.