Databases

Install PostgreSQL 17 on Ubuntu 24.04 / 26.04 LTS

The quickest way to install PostgreSQL Ubuntu users reach for in 2026 is the PGDG apt repository on Ubuntu 24.04 LTS (with identical steps on Ubuntu 26.04 LTS “Resolute Raccoon”), targeting the current 17 LTS line. The URL you landed on used to cover PostgreSQL 12, which reached end of life in November 2024 and no longer receives security patches. Rather than sending you elsewhere, this page now ships you the current install on the same backbone: PGDG apt repository, scram-sha-256 authentication, LAN-reachable listener, a working test database, backup and restore, and a clean upgrade path if you are coming from an older cluster.

Original content from computingforgeeks.com - post 32329

Ubuntu 24.04 ships PostgreSQL 16 in main. That works, but PostgreSQL 17 is the current LTS line and the correct target for new installations in 2026. You get it from the official PostgreSQL Global Development Group (PGDG) apt repository. PostgreSQL 18 is also available and covered in the coexistence section, but pin your production deployments to 17 unless you have a specific reason to move to 18.

Tested April 2026 on Ubuntu 24.04.4 LTS with PostgreSQL 17.9 from PGDG (apt.postgresql.org), scram-sha-256 auth, kernel 6.8

What changed since PostgreSQL 12

PostgreSQL 12 shipped in October 2019 and hit end of life in November 2024. Six major versions have landed since. The upgrades that matter most for day-to-day work: JIT compilation is on by default, SCRAM authentication replaced MD5 as the default, partitioning became production-grade (PG13), logical replication was extended to include DDL (PG16), and PG17 added incremental backup support, better vacuum performance, and materialized view refreshes without locking out readers. None of these require config changes to benefit from, but they do shift the sane defaults.

If you have a running PG12 cluster, the last section of this guide covers the pg_upgradecluster path to 17. You do not need to reinstall the server from scratch.

Prerequisites

  • An Ubuntu 24.04 LTS or Ubuntu 26.04 LTS server with root or sudo access (this guide was tested on 24.04.4)
  • Port 5432/tcp reachable from your database clients (LAN or jump host)
  • At least 1 GB RAM for a small cluster (2 GB or more for anything resembling production)
  • The ufw package if you plan to use the built-in firewall

If you are still provisioning the host, a 2 vCPU / 2 GB droplet on DigitalOcean or a CX22 on Hetzner runs PG17 comfortably for small and medium workloads. For production, size disk IOPS and RAM to working-set size before CPU.

Step 1: Set reusable shell variables

Every command below references shell variables so you change one block and paste the rest. Export these on your SSH session first:

export PG_VERSION="17"
export PG_DB="testdb"
export PG_USER="testuser"
export PG_PASS="ChangeMe#Strong2026"
export CLIENT_LAN="192.168.1.0/24"

Swap the password for something real, adjust CLIENT_LAN to the subnet your application servers live on, and confirm the variables are set before running anything else:

echo "PG:    $PG_VERSION"
echo "DB:    $PG_DB / $PG_USER"
echo "LAN:   $CLIENT_LAN"

Values persist only for the current shell session. Re-run the export block if you reconnect or jump into sudo -i.

Step 2: Add the PGDG apt repository

Ubuntu has PostgreSQL in its main archive, but the versions lag. PGDG ships the current and previous major versions for every supported Ubuntu release on the same day they hit upstream. Install the GPG key first:

sudo install -d /usr/share/postgresql-common/pgdg
sudo wget -O /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
  https://www.postgresql.org/media/keys/ACCC4CF8.asc

Now create the sources list. The repository codename needs to match your Ubuntu release: noble for 24.04 and resolute for 26.04. The /etc/os-release file has the right value in VERSION_CODENAME, so use that directly:

. /etc/os-release
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \
https://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" | \
  sudo tee /etc/apt/sources.list.d/pgdg.list

Refresh the apt cache so the new repo is indexed:

sudo apt update

Confirm the PGDG line shows up and the candidate version of PostgreSQL matches what you expect:

apt-cache policy postgresql-17 | head -5

You should see a candidate version from apt.postgresql.org rather than the default Ubuntu archive:

postgresql-17:
  Installed: (none)
  Candidate: 17.9-1.pgdg24.04+1
  Version table:
     17.9-1.pgdg24.04+1 500
        500 https://apt.postgresql.org/pub/repos/apt noble-pgdg/main amd64 Packages

The noble-pgdg repo reference becomes resolute-pgdg on Ubuntu 26.04 hosts, and the package version string adjusts to pgdg26.04+1 accordingly.

Step 3: Install PostgreSQL

Pull the server, client, and contrib packages in one command:

sudo apt install -y postgresql-${PG_VERSION} postgresql-client-${PG_VERSION} postgresql-contrib-${PG_VERSION}

The install creates a postgres system user, initialises a default cluster at /var/lib/postgresql/17/main, drops configuration files into /etc/postgresql/17/main/, and starts the service. The tail of apt’s output shows the cluster being bootstrapped with UTF-8 encoding and the C.UTF-8 locale:

The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are disabled.
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

Data page checksums default to off for speed. If you want them on, you need to pass --data-checksums to pg_createcluster at create time; they cannot be toggled after the cluster is running without a dump-and-restore or pg_checksums offline run.

Step 4: Verify the service and version

PostgreSQL on Debian-family systems runs under a templated systemd unit: postgresql@<version>-<cluster>.service. The top-level postgresql.service is a wrapper that starts all configured clusters. Check both:

systemctl status postgresql@${PG_VERSION}-main --no-pager

The unit should report active (running) with the postmaster and its worker processes listed:

[email protected] - PostgreSQL Cluster 17-main
     Loaded: loaded (/usr/lib/systemd/system/[email protected]; enabled-runtime; preset: enabled)
     Active: active (running) since Sat 2026-04-18 20:54:06 UTC; 16s ago
   Main PID: 39095 (postgres)
      Tasks: 6 (limit: 2316)
     Memory: 18.9M (peak: 27.2M)
     CGroup: /system.slice/system-postgresql.slice/[email protected]
             ├─39095 /usr/lib/postgresql/17/bin/postgres -D /var/lib/postgresql/17/main
             ├─39096 "postgres: 17/main: checkpointer"
             ├─39097 "postgres: 17/main: background writer"
             ├─39099 "postgres: 17/main: walwriter"
             ├─39100 "postgres: 17/main: autovacuum launcher"
             └─39101 "postgres: 17/main: logical replication launcher"

Confirm the exact server version by asking the database itself. The postgres OS user is the default superuser, so sudo -u postgres gives you a trusted psql session without a password:

sudo -u postgres psql -c "SELECT version();"

The version string includes the upstream PostgreSQL version and the Debian package metadata:

                                                               version
-------------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 17.9 (Ubuntu 17.9-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
(1 row)

A screenshot from the test VM with version, service status, and a psql \dt on the test database:

PostgreSQL 17 psql version and systemctl status on Ubuntu 24.04 LTS

By default the cluster listens only on localhost. Confirm with ss:

sudo ss -tlnp | grep 5432

You will see the postmaster bound to the loopback addresses only:

LISTEN 0  200  127.0.0.1:5432  0.0.0.0:*  users:(("postgres",pid=39095,fd=7))
LISTEN 0  200      [::1]:5432     [::]:*  users:(("postgres",pid=39095,fd=6))

That is safe for single-host deployments where the app and database share a machine, but useless for anything that spans more than one server.

Step 5: Configure remote access

A default install is safe because nothing from outside the host can reach the database. Most real workloads sit on a separate application server, so you need to open the listener and authorise the client subnet. Two files control this: postgresql.conf (network bind) and pg_hba.conf (client authentication rules).

Open the main config first:

sudo vi /etc/postgresql/${PG_VERSION}/main/postgresql.conf

Find #listen_addresses = 'localhost' and replace it with a non-commented line that accepts connections on every interface. You can also bind to a single IP if the host has multiple NICs:

listen_addresses = '*'

Save and close the file. Now edit the host-based authentication file. PGDG ships scram-sha-256 as the default password method on PG14+, which is the right choice. Older articles from the PG11/12 era often show md5; do not copy that, it is weaker:

sudo vi /etc/postgresql/${PG_VERSION}/main/pg_hba.conf

Add a line for your application subnet at the bottom. The structure is type database user address method. A LAN-wide rule with scram-sha-256 looks like this:

host    all             all             192.168.1.0/24          scram-sha-256

Restart the cluster so both changes take effect. systemctl reload is enough for pg_hba.conf, but listen_addresses requires a full restart:

sudo systemctl restart postgresql@${PG_VERSION}-main

Re-check the listening sockets. You should now see 0.0.0.0:5432 instead of 127.0.0.1:5432:

sudo ss -tlnp | grep 5432

The bind address is now wildcard, meaning the postmaster accepts connections from any interface:

LISTEN 0  200  0.0.0.0:5432  0.0.0.0:*  users:(("postgres",pid=39342,fd=6))
LISTEN 0  200     [::]:5432     [::]:*  users:(("postgres",pid=39342,fd=7))

Changes to listen_addresses demand a full restart; a plain systemctl reload will not rebind the socket.

Step 6: Create a database, user, and test the connection

Time to create a real user, a database owned by that user, and connect as that user from outside the postgres superuser session. Open an interactive psql as the postgres superuser:

sudo -u postgres psql

Create the role and database. PostgreSQL uses two SQL statements here: CREATE USER (with password, because the authentication method demands one) and CREATE DATABASE (owned by that user, so they can create and drop tables without superuser privileges):

CREATE USER testuser WITH PASSWORD 'ChangeMe#Strong2026';
CREATE DATABASE testdb OWNER testuser;
\l
\q

The \l meta-command lists all databases, confirming testdb is now present and owned by testuser:

                                                 List of databases
   Name    |  Owner   | Encoding | Locale Provider | Collate |  Ctype
-----------+----------+----------+-----------------+---------+---------
 postgres  | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8
 template0 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8
 template1 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8
 testdb    | testuser | UTF8     | libc            | C.UTF-8 | C.UTF-8
(4 rows)

Now connect as testuser over TCP (not the peer socket). PGPASSWORD is the simplest way to pass credentials non-interactively; for production use a ~/.pgpass file or a secret manager:

PGPASSWORD="${PG_PASS}" psql -h 127.0.0.1 -U ${PG_USER} -d ${PG_DB} -c "\conninfo"

The output confirms the TLS handshake and user context:

You are connected to database "testdb" as user "testuser" on host "127.0.0.1" at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql)

TLS on a loopback connection is a recent default. It comes from the snakeoil certificate the Debian postinst generates at first start, stored in /etc/ssl/certs/ssl-cert-snakeoil.pem. Replace it with a real certificate for production use.

Create a table, insert rows, and read them back

A quick DDL plus DML round-trip proves the user has the right privileges on their own database. Save the SQL to a file so the psql invocation stays one line:

vi /tmp/schema.sql

Paste the table definition and sample inserts:

CREATE TABLE servers (
    id SERIAL PRIMARY KEY,
    hostname VARCHAR(100) NOT NULL,
    ip_address INET,
    os VARCHAR(50),
    created_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO servers (hostname, ip_address, os) VALUES
    ('web01', '10.0.1.50', 'Ubuntu 24.04'),
    ('db01', '10.0.1.51', 'Ubuntu 24.04'),
    ('cache01', '10.0.1.52', 'Debian 13');
SELECT id, hostname, ip_address, os FROM servers ORDER BY id;

Pipe the file into psql as the test user:

PGPASSWORD="${PG_PASS}" psql -h 127.0.0.1 -U ${PG_USER} -d ${PG_DB} -f /tmp/schema.sql

The select returns the three rows with their typed columns intact (notice the INET data type handling the IP):

 id | hostname | ip_address |      os
----+----------+------------+--------------
  1 | web01    | 10.0.1.50  | Ubuntu 24.04
  2 | db01     | 10.0.1.51  | Ubuntu 24.04
  3 | cache01  | 10.0.1.52  | Debian 13
(3 rows)

The INET column type stores IPs with subnet awareness and supports operators like << (contained by) for range queries, which beats a plain varchar every time.

Step 7: Back up and restore with pg_dump

Never run a database in production without a tested restore. pg_dump produces a logical backup (SQL statements) that restores on any compatible or newer PostgreSQL version. It is the right tool for small-to-medium databases and migrations between major versions.

Dump the test database to a SQL file:

sudo -u postgres pg_dump ${PG_DB} > /tmp/${PG_DB}.sql
ls -la /tmp/${PG_DB}.sql
wc -l /tmp/${PG_DB}.sql

A three-row test produces a small file, but the shape of the output (~100 lines) is the same as a full database; pg_dump just scales the COPY blocks:

-rw-r--r-- 1 root root 2392 Apr 18 20:54 /tmp/testdb.sql
101 /tmp/testdb.sql

Now prove the restore works. Drop the database, recreate it empty, and pipe the dump back in:

sudo -u postgres dropdb ${PG_DB}
sudo -u postgres createdb -O ${PG_USER} ${PG_DB}
sudo -u postgres psql ${PG_DB} < /tmp/${PG_DB}.sql

The restore replays DDL and DML in the right order:

SET
SET
CREATE TABLE
ALTER TABLE
CREATE SEQUENCE
ALTER SEQUENCE
ALTER TABLE
COPY 3
 setval
--------
      3

Count rows to confirm the data is back:

PGPASSWORD="${PG_PASS}" psql -h 127.0.0.1 -U ${PG_USER} -d ${PG_DB} -c "SELECT COUNT(*) AS row_count FROM servers;"

Three rows, matching the sample data from Step 6:

 row_count
-----------
         3
(1 row)

For larger databases use the custom format (-Fc), which is compressed and restorable with pg_restore in parallel streams. Schedule daily dumps with cron, rotate with 7 day retention, and ship the files off-box to object storage. The Ubuntu 26.04 PostgreSQL 18 guide walks through physical base backups with pg_basebackup if logical dumps outgrow your RTO window.

Step 8: Open the firewall

If UFW is active, open 5432/tcp from the LAN only. Never open the port to the public internet; put a bastion or a VPN in front for remote access:

sudo ufw allow from ${CLIENT_LAN} to any port 5432 proto tcp
sudo ufw status | grep 5432

The rule shows up scoped to the subnet you set:

5432/tcp                   ALLOW       192.168.1.0/24

On cloud hosts the firewall is upstream (AWS security group, Hetzner Cloud firewall, DigitalOcean firewall). Open 5432/tcp only to the application subnet or the jump host, never 0.0.0.0/0.

Run PostgreSQL 17 and 18 side by side

PGDG lets you install multiple major versions on the same host. They coexist because each cluster lives under its own version directory (/var/lib/postgresql/17/main, /var/lib/postgresql/18/main) with its own config and port. Useful when you want to test application compatibility before upgrading production.

sudo apt install -y postgresql-18 postgresql-client-18

Debian’s postgresql-common picks the next available port for the new cluster (5433 by default). List what you have with pg_lsclusters:

pg_lsclusters

Both clusters show online, each bound to its own port:

Ver Cluster Port Status Owner    Data directory              Log file
17  main    5432 online postgres /var/lib/postgresql/17/main /var/log/postgresql/postgresql-17-main.log
18  main    5433 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log

Connect to the 18 cluster by passing -p 5433 to psql or the --cluster 18/main flag to postgresql-common tooling.

Upgrade an older cluster (PG12/13/14/15/16) to 17

If you came here because the old PG12 guide used to live at this URL, the in-place upgrade path is straightforward. Install PG17 alongside the existing version, drop the freshly created empty 17 cluster, then run pg_upgradecluster to promote the old data into 17. The wrapper calls pg_upgrade internally but handles the data directory and config file plumbing for you.

With PG12 still running and PG17 already installed:

sudo pg_dropcluster --stop 17 main
sudo pg_upgradecluster 12 main

The wrapper stops the 12 cluster, invokes pg_upgrade --link internally (fast, uses hardlinks to avoid copying table data), migrates roles and pg_hba.conf, and starts the 17 cluster on port 5432 so your apps connect transparently. The old 12 cluster is left in place on port 5433 for rollback; drop it once you have verified the app against 17:

sudo pg_dropcluster --stop 12 main
sudo apt purge -y postgresql-12 postgresql-client-12

A few things that bite during major-version upgrades:

  • Extensions must exist on the target version. Before upgrading, check each database for non-default extensions with \dx and install the matching postgresql-17-* packages.
  • Authentication method changes matter. If you came from md5, flip users to scram-sha-256 after the upgrade and have them re-enter passwords.
  • Analyze first. Always run vacuumdb --all --analyze-in-stages on the upgraded cluster before returning it to production traffic. Stats do not carry across majors.

Troubleshooting common issues

psql: FATAL: password authentication failed for user “testuser”

Either the password is wrong or the pg_hba.conf rule does not cover your client. Check which rule matched with:

sudo tail -20 /var/log/postgresql/postgresql-17-main.log

The log prints the line number of pg_hba.conf that matched. If no rule matched you will see no pg_hba.conf entry for host, and you need to add one for your client subnet.

psql: could not connect to server: Connection refused

The cluster is not listening on the address you tried. Confirm the listener:

sudo ss -tlnp | grep 5432

If you see only 127.0.0.1:5432, listen_addresses in postgresql.conf was not changed, or the restart did not happen. A reload is insufficient for that setting.

initdb: could not create directory … Permission denied

Happens when the postgres user cannot write to /var/lib/postgresql, usually after restoring from a backup that reset ownership. Fix with:

sudo chown -R postgres:postgres /var/lib/postgresql

Re-run systemctl start postgresql@${PG_VERSION}-main once ownership is corrected.

Locale warnings during initdb

If the package postinst prints perl: warning: Setting locale failed, your Ubuntu image is missing the C.UTF-8 locale. Generate it:

sudo locale-gen en_US.UTF-8
sudo update-locale LANG=en_US.UTF-8

Then reinstall or re-run pg_createcluster 17 main.

Run the install on DigitalOcean or Hetzner Cloud

PostgreSQL is IO-heavy. For self-hosted production, size disk performance first. Two budget-friendly VPS options:

  • DigitalOcean has $200 free credit for new accounts. A $24/mo Premium Intel droplet (2 vCPU, 4 GB RAM, NVMe SSD) runs PG17 for small production workloads.
  • Hetzner Cloud is roughly half the price for the same specs (CX22, €4.51/mo) if you are happy in European regions.

If you would rather skip the operational overhead of patches, backups, and failover, managed PostgreSQL exists: DigitalOcean Managed Databases starts at $15/mo for 1 GB. Neon.tech is a cheaper serverless option with branching that plays well with CI. Both give you PG17 today.

Secure the credentials you just created

The ChangeMe#Strong2026 password above exists to make the tutorial reproducible. Do not ship with it. Store the real password in a secret manager and inject it at deploy time:

  • 1Password for Teams has a CLI and Kubernetes operator that pulls secrets at runtime, avoiding committed plaintext.
  • HashiCorp Vault is the self-hosted equivalent.
  • For a solo project, an ~/.pgpass file with chmod 600 plus a .env per app is the minimum bar.

Rotate the postgres superuser password after installation with ALTER USER postgres WITH PASSWORD '...'; if you plan to expose psql from outside the box.

Where to go next

With the cluster running, pick the path that matches your workload:

Related Articles

Debian Install Apache Spark 4.1.1 on Debian 13 / Ubuntu 24.04 LTS Ubuntu Configure Postfix as Send-Only SMTP Server on Ubuntu 24.04 / 22.04 Databases Install Latest Redis Server on CentOS 7 / RHEL 7 Networking Install Zimbra Collaboration Suite on Ubuntu 24.04

3 thoughts on “Install PostgreSQL 17 on Ubuntu 24.04 / 26.04 LTS”

Leave a Comment

Press ESC to close