Databases

Install pgAdmin 4 on Debian 13 / 12

pgAdmin 4 runs on Debian as a web application behind Apache. The official APT repository ships the same build for Debian 13 (Trixie) and Debian 12 (Bookworm), so you add one repository, install one package, and let a bundled script wire it into Apache with mod_wsgi.

Original content from computingforgeeks.com - post 12843

This guide installs pgAdmin 4 in server (web) mode, puts Nginx with a Let’s Encrypt certificate in front of it, and connects it to a local PostgreSQL server. It also covers the change most upgrade guides skip, because free-text password exec commands no longer run in server mode. Every command below was run in October 2026 on Debian 13 and Debian 12 virtual machines with pgAdmin 4 9.19. If you still need the database itself, set it up with our PostgreSQL on Debian guide first.

Prerequisites

The pgAdmin repository only publishes x86_64 packages for Debian, so an ARM server (Raspberry Pi, Graviton, Ampere) needs the pgAdmin container image instead. Here is what the steps assume:

  • Debian 13 (Trixie) or Debian 12 (Bookworm), amd64
  • PostgreSQL installed and running on the same host or reachable over the network
  • A user with sudo rights
  • A DNS name pointing at the server, for the Let’s Encrypt certificate

The test VMs had 2 GB of RAM and 2 vCPUs each, and that was enough for PostgreSQL, Apache, Nginx and pgAdmin together on one box. pgAdmin itself is light because it only holds web sessions and a small SQLite configuration database. The PostgreSQL server you point it at sets the real sizing, so plan that host around its data and connection count, and treat the numbers here as a floor for a single admin.

Add the pgAdmin Repository

Debian’s own archive does not carry pgAdmin 4, so the packages come from the pgAdmin team’s APT repository. Start with the tools the repository setup needs. Minimal and container images sometimes lack lsb-release, and the repository line below depends on it, so install it even if you think it’s already there.

sudo apt update
sudo apt install -y curl ca-certificates gnupg lsb-release

Next, import the signing key into /etc/apt/keyrings, the path the pgAdmin download page now uses. The deprecated apt-key method is gone from both releases, so a dedicated keyring file that the repository line references by path is the only route that works.

curl -fsS https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo gpg --dearmor -o /etc/apt/keyrings/packages-pgadmin-org.gpg

Now add the repository. $(lsb_release -cs) expands to trixie on Debian 13 and bookworm on Debian 12, so the same line works on both:

echo "deb [signed-by=/etc/apt/keyrings/packages-pgadmin-org.gpg] https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" | sudo tee /etc/apt/sources.list.d/pgadmin4.list

Refresh the package index and ask APT which pgAdmin builds it can see:

sudo apt update
apt-cache policy pgadmin4-web

On Debian 13 the candidate came from the trixie suite, with every release back to 9.15 still available if you ever need to pin one:

pgadmin4-web:
  Installed: (none)
  Candidate: 9.19
  Version table:
     9.19 500
        500 https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/trixie pgadmin4/main all Packages
     9.18 500
        500 https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/trixie pgadmin4/main all Packages
     9.17 500
        500 https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/trixie pgadmin4/main all Packages
     9.16 500
        500 https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/trixie pgadmin4/main all Packages
     9.15 500
        500 https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/trixie pgadmin4/main all Packages

Debian 12 shows the same candidate from the bookworm suite, because the pgAdmin team builds both suites from the same release on the same day. If the Candidate line reads (none) instead, the repository line or the keyring path has a typo, and sudo apt update will have printed a NO_PUBKEY or 404 error above it. Fix that before you continue, since the install step below would otherwise fail with “Unable to locate package”.

Install pgAdmin 4

The repository splits pgAdmin into three packages. Pick the one that matches how you’ll use it:

  • pgadmin4-web: web mode under Apache, for servers
  • pgadmin4-desktop: the desktop application only
  • pgadmin4: both of the above

A headless server only needs the web package. It pulls in Apache, mod_wsgi and the pgadmin4-server core package as dependencies:

sudo apt install -y pgadmin4-web

Confirm which version landed:

dpkg -l | grep pgadmin4

Both the web package and the server core it depends on should report the same version, 9.19 at the time of testing:

ii  pgadmin4-server                        9.19                                 amd64        The core server package for pgAdmin. pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world.
ii  pgadmin4-web                           9.19                                 all          The web interface for pgAdmin, hosted under Apache HTTPD. pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world.

The two version columns must match. The web package is only the Apache glue, while pgadmin4-server holds the application and its Python environment, so a mismatch means a partial upgrade and the two halves come from different releases. If you ever see one, run sudo apt install -y pgadmin4-web pgadmin4-server to bring them back in step before going further.

Configure pgAdmin 4 Web Mode

The package installs files, but it does not create the configuration database or your first login. setup-web.sh does both, enables the Apache config and restarts Apache. Passing the email and password as environment variables with --yes keeps it non-interactive, which also makes it safe to script:

sudo PGADMIN_SETUP_EMAIL='[email protected]' PGADMIN_SETUP_PASSWORD='YourStrongPassword' /usr/pgadmin4/bin/setup-web.sh --yes

Swap in your own email and a strong password, since these become the pgAdmin administrator login. On Debian 13 the script finished like this:

Setting up pgAdmin 4 in web mode on a Debian based platform...
Running in non-interactive mode...
Creating configuration database...
/usr/pgadmin4/venv/lib/python3.13/site-packages/alembic/operations/batch.py:298: UserWarning: Unnamed CHECK constraint on reflected table 'sharedserver' is being omitted from the table recreate; pass the constraint explicitly using batch_alter_table.table_args.  Naming CHECK constraints avoids this going forward.
  util.warn(
NOTE: Configuring authentication for SERVER mode.

pgAdmin 4 - Application Initialisation
======================================

Creating storage and log directories...
Apache successfully restarted. You can now start using pgAdmin 4 in web mode at http://127.0.0.1/pgadmin4

The Alembic UserWarning comes from a schema migration while the configuration database is created, and it is harmless, since your login and every later step worked with it in place. The last line is the one that matters. At this point pgAdmin answers on plain HTTP port 80, which is fine for a quick check from the server itself but not for a login form you reach over a network.

Set Up Nginx Reverse Proxy with SSL

Rather than bolting TLS onto the Apache vhost that setup-web.sh created, we keep Apache untouched on a local port and let Nginx own ports 80 and 443. The pgAdmin package owns the Apache side of the config, so leaving it alone means a package upgrade never fights your TLS settings. Nginx then holds the certificate, the redirect and the HTTP/2 listener, and you only have one web server to harden.

Move Apache to Port 8080

Nginx cannot bind port 80 while Apache holds it, so move Apache first. The two sed edits change the listener and the default vhost, and the pgAdmin config that setup-web.sh enabled follows along because /etc/apache2/conf-enabled/pgadmin4.conf is a server-wide WSGIScriptAlias, not tied to a port.

sudo sed -i 's/Listen 80/Listen 8080/' /etc/apache2/ports.conf
sudo sed -i 's/<VirtualHost \*:80>/<VirtualHost *:8080>/' /etc/apache2/sites-enabled/000-default.conf
sudo systemctl restart apache2

Check that Apache moved before you install Nginx. The Debian nginx package starts its service as soon as it installs, so Apache still holding port 80 at that point leaves you with a failed unit to clean up.

sudo ss -tlnp | grep 8080

Apache’s worker processes should now hold port 8080.

LISTEN 0      511                *:8080             *:*    users:(("apache2",pid=5290,fd=4),("apache2",pid=5289,fd=4),("apache2",pid=5286,fd=4))

If port 80 still shows up for Apache instead, the Listen line in ports.conf had different spacing and the first sed missed it, so fix that line by hand.

Install Nginx and Certbot

Both packages come from the Debian archive. Our Nginx guide for Debian and Ubuntu covers the server itself in more depth:

sudo apt install -y nginx certbot

Request the certificate with certbot’s standalone authenticator, replacing pgadmin.example.com and the email with your own. The two hooks matter more than they look. Standalone mode needs port 80 to itself, and the original version of this guide stopped Nginx by hand once, which works for the first certificate but leaves every automatic renewal failing while Nginx is running. With --pre-hook and --post-hook, certbot stores the stop and start commands in the renewal config and runs them on its own:

sudo certbot certonly --standalone -d pgadmin.example.com --non-interactive --agree-tos -m [email protected] --pre-hook 'systemctl stop nginx' --post-hook 'systemctl start nginx'

The lab VMs had no public DNS name, so we pointed certbot at a local Pebble ACME test server with an extra --server flag. You leave that flag off and Let’s Encrypt issues the certificate. Below is our output without the donation banner and Pebble’s five-year test expiry line; a Let’s Encrypt certificate shows its much shorter expiry date there instead:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for pgadmin.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/pgadmin.example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/pgadmin.example.com/privkey.pem
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

The hooks are now part of the renewal configuration, which you can confirm before trusting the timer with it:

sudo grep hook /etc/letsencrypt/renewal/pgadmin.example.com.conf

Both lines should be there:

pre_hook = systemctl stop nginx
post_hook = systemctl start nginx

If the grep prints nothing, renewals will fail with “Could not bind TCP port 80” (covered under troubleshooting below). Run the certonly command again with the hooks and certbot updates the stored settings.

Create the Nginx Configuration

Open a new server block file. You’ll keep it under sites-available and link it in, which is the Debian layout the default site already uses.

sudo vim /etc/nginx/sites-available/pgadmin

Paste the configuration below. It redirects HTTP to HTTPS, proxies /pgadmin4/ to Apache on 8080, and passes X-Script-Name so pgAdmin builds its links under the right path. It’s written for Debian 13, whose Nginx takes HTTP/2 as a separate directive. On Debian 12, remove the http2 line and add http2 to the end of the 443 listen line instead, because the older Nginx there rejects the separate directive.

server {
    listen 80;
    server_name pgadmin.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name pgadmin.example.com;

    ssl_certificate /etc/letsencrypt/live/pgadmin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pgadmin.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    location /pgadmin4/ {
        proxy_pass http://127.0.0.1:8080/pgadmin4/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Script-Name /pgadmin4;
    }

    location / {
        return 301 https://$host/pgadmin4/;
    }
}

Enable the site and drop the default one, which would otherwise also claim port 80:

sudo ln -sf /etc/nginx/sites-available/pgadmin /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default

Test the syntax before you reload anything, because a failed restart takes the redirect and the TLS listener down together.

sudo nginx -t

A clean config prints two lines:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Restart Nginx and enable it at boot:

sudo systemctl restart nginx
sudo systemctl enable nginx

From the server, a request to the HTTPS URL should come back as a redirect to the pgAdmin login page over HTTP/2:

curl -s -o /dev/null -w '%{http_code} %{http_version} %{redirect_url}\n' https://pgadmin.example.com/pgadmin4/

Both Debian releases returned the same line, Debian 12 included, once its listen line used the combined syntax:

302 2 https://pgadmin.example.com/pgadmin4/login?next=/pgadmin4/

Finish with a renewal dry run. This is the test the stopped-by-hand setup used to fail, because it goes through the same stop, renew and start sequence the timer will:

sudo certbot renew --dry-run

You want the success line, and Nginx should be back up afterwards (systemctl is-active nginx printed active on both VMs):

Processing /etc/letsencrypt/renewal/pgadmin.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for pgadmin.example.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/pgadmin.example.com/fullchain.pem (success)

The Debian certbot package installs certbot.timer, which runs the real renewal twice a day, so there is no cron job for you to add.

Configure the Firewall

Debian ships without an active firewall, so install ufw and open SSH, HTTP and HTTPS. Allow SSH before you enable it, or you’ll cut off your own session. ufw allow ssh opens port 22 only, so allow your custom port instead if sshd listens elsewhere. Port 8080 stays closed, which keeps Apache reachable only through Nginx:

sudo apt install -y ufw
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Answer y at the prompt, then list the rules:

sudo ufw status

Ports 22, 80 and 443 should be allowed for IPv4 and IPv6:

Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)             

Port 80 has to stay open even though Nginx only redirects it, because certbot’s standalone renewal answers the HTTP-01 challenge on that port. Closing it looks tidy, but it breaks the next automatic renewal, and you only find out from an expiry warning. Apache’s 8080 is the port to keep shut, so nobody can reach pgAdmin over plain HTTP around the proxy.

Access the pgAdmin Web Interface

Browse to https://pgadmin.example.com/pgadmin4/. The login page asks for the email and password you passed to setup-web.sh:

pgAdmin 4 login page served over HTTPS through Nginx on Debian 13

Once you’re in, the welcome dashboard opens with an empty Servers group in the Object Explorer on the left:

pgAdmin 4 welcome dashboard after logging in on Debian 13

pgAdmin does not discover servers on its own, even one on the same host, so the next job is registering one. Each pgAdmin login keeps its own server list, which means a second admin you add later starts with an empty tree as well.

Connect to a PostgreSQL Server

pgAdmin connects over TCP, so the postgres role needs a password. Debian’s default pg_hba.conf uses peer authentication only for local socket connections, and that’s why a fresh install has no password set:

sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'YourSecurePassword';"

Older guides, this one included, told you to edit pg_hba.conf next. Check the file before you touch it, because on both releases TCP connections to 127.0.0.1 already use scram-sha-256. The path is /etc/postgresql/17/main/ on Debian 13 and /etc/postgresql/15/main/ on Debian 12 with the distribution packages, so adjust the version in the command if you run Debian 12 or the PGDG builds.

sudo grep -E '^(local|host)' /etc/postgresql/17/main/pg_hba.conf

The host lines for 127.0.0.1 and ::1 are the ones pgAdmin uses:

local   all             postgres                                peer
local   all             all                                     peer
host    all             all             127.0.0.1/32            scram-sha-256
host    all             all             ::1/128                 scram-sha-256
local   replication     all                                     peer
host    replication     all             127.0.0.1/32            scram-sha-256
host    replication     all             ::1/128                 scram-sha-256

So a local pgAdmin needs no pg_hba.conf change and no PostgreSQL restart. Switching the local postgres line from peer to a password method, as the old steps did, only changes socket logins and makes sudo -u postgres psql ask for a password for no gain. Prove the password works over TCP from the shell before you blame pgAdmin for anything.

psql -h 127.0.0.1 -U postgres -c 'select version();'

After the password prompt, the server version comes back:

                                                       version
---------------------------------------------------------------------------------------------------------------------
 PostgreSQL 17.11 (Debian 17.11-0+deb13u1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row)

A PostgreSQL server on another host is different: it needs listen_addresses and a host line for the pgAdmin server’s IP in its own pg_hba.conf, plus port 5432 open in its firewall.

In pgAdmin, click Add New Server on the dashboard. Give it a name such as “Local PostgreSQL” on the General tab, then fill the Connection tab with host 127.0.0.1, port 5432, maintenance database postgres and username postgres:

pgAdmin 4 Register Server dialog Connection tab with host 127.0.0.1, port 5432 and user postgres

Type the password you set above into the Password field, or leave it empty if you’ll use a password exec command from the next section. Click Save, and pgAdmin connects and opens the server’s activity dashboard:

pgAdmin 4 server dashboard connected to the local PostgreSQL server on Debian 13

Expand the server in the Object Explorer to browse databases, schemas and tables. For a structured way into the database side, our list of PostgreSQL books covers everything from first queries to replication.

Allow a Password Exec Command (Optional)

A password exec command lets pgAdmin fetch a database password from a script (a vault client, a secrets file) instead of storing it. Starting with 9.19, users in server mode can no longer type their own command into the server dialog. The administrator defines named commands in SERVER_PASSEXEC_COMMANDS, users pick a name from a dropdown, and pgAdmin runs it without a shell and with a reduced environment. Until you define at least one, the feature is off. Skip this section if every user types or saves their password.

The setting belongs in /etc/pgadmin/config_system.py. pgAdmin reads that file at startup on Linux, and it lives outside /usr/pgadmin4, so package upgrades leave it alone. As a small example, store the password in a file only the web server can read:

sudo mkdir -p /etc/pgadmin
echo 'YourSecurePassword' | sudo tee /etc/pgadmin/postgres.pass > /dev/null
sudo chown root:www-data /etc/pgadmin/postgres.pass
sudo chmod 0640 /etc/pgadmin/postgres.pass

Create the script pgAdmin will run:

sudo vim /usr/local/bin/pg-pass

Add these two lines. A real setup would call your vault CLI here and check the PGADMIN_PASSEXEC_PGADMIN_USER variable pgAdmin passes in:

#!/bin/sh
exec cat /etc/pgadmin/postgres.pass

Restrict it to root and the www-data group that Apache runs pgAdmin as, then run it as that user to prove the permissions line up:

sudo chown root:www-data /usr/local/bin/pg-pass
sudo chmod 0750 /usr/local/bin/pg-pass
sudo -u www-data /usr/local/bin/pg-pass

It should print the password and nothing else. Now open the pgAdmin system config:

sudo vim /etc/pgadmin/config_system.py

Each entry maps a name to a list holding the executable and its arguments. Arguments are not passed through a shell, so pipes and $VARS you put in them will not expand.

SERVER_PASSEXEC_COMMANDS = {
    'local-postgres': ['/usr/local/bin/pg-pass']
}

Make the file world readable before restarting Apache. In our lab the file was created with mode 0600, and every pgAdmin page then returned HTTP 500 because www-data couldn’t import it:

sudo chmod 0644 /etc/pgadmin/config_system.py
sudo systemctl restart apache2

Open the server’s properties (or the Register Server dialog for a new one) and the name you defined is now offered:

pgAdmin 4 Register Server Advanced tab with the local-postgres password exec command selected
  1. 1Advanced tab
  2. 2Password exec command
  1. Open the Advanced tab of the server dialog.
  2. Pick local-postgres from Password exec command and leave the password on the Connection tab empty.

We saved the server this way with no password typed anywhere, and pgAdmin connected straight to the dashboard shown in the previous section, which proves the script ran as www-data. Anything you don’t list in the config never appears in that dropdown, which is the point of the change: a pgAdmin user can no longer make the server run a command of their choosing.

Upgrade an Existing pgAdmin Install

Upgrades come through APT like any other package. Upgrade both packages together, then restart Apache so the WSGI process loads the new code:

sudo apt update
sudo apt install -y --only-upgrade pgadmin4-web pgadmin4-server
sudo systemctl restart apache2

A plain install with saved passwords needs nothing else. Server-mode installs should still read the release notes, because 9.19 tightened several other server-mode defaults, including these:

  • SSH tunnels ignore the SSH agent and the service account’s ~/.ssh
  • No fallback to the service account’s password file or client certificate
  • Users of a shared server no longer inherit the owner’s password file
  • Identity files, certificates and password files must sit in the user’s storage directory or SHARED_STORAGE

A tunnel that relied on the agent now needs an identity file or password set on the server. The change most likely to bite is password exec commands, which 9.19 stopped running as free text. On the first start where SERVER_PASSEXEC_COMMANDS has at least one entry, pgAdmin converts every stored command that exactly matches an entry into that entry’s name and clears the rest, and the clearing can’t be undone. So build the full allowlist before that start, not one entry at a time.

We tested the upgrade on Debian 12 from 9.18, with a server that had /usr/local/bin/pg-pass stored as its command. Two things show up in /var/log/pgadmin/pgadmin4.log, so check it right after the restart:

sudo grep WARNING /var/log/pgadmin/pgadmin4.log

With ENABLE_SERVER_PASS_EXEC_CMD = True still in the old config_system.py and no allowlist yet, the first start on 9.19 logged both warnings:

2026-10-31 06:10:03,214: WARNING	pgadmin:	ENABLE_SERVER_PASS_EXEC_CMD is deprecated and no longer has any effect; define the permitted commands in SERVER_PASSEXEC_COMMANDS instead.
2026-10-31 06:10:03,233: WARNING	pgadmin:	SERVER_PASSEXEC_COMMANDS is not configured, but 1 row(s) in the server table still hold a password exec command from before the upgrade. They are not run in server mode. Configure SERVER_PASSEXEC_COMMANDS before the next start: commands that match a configured entry are then converted to its name, and the rest are cleared.

The second warning is your safety net. While the allowlist is empty, the old commands stay in the configuration database and pgAdmin does not run them. Add an entry whose arguments, joined by single spaces, match the stored command exactly ('local-postgres': ['/usr/local/bin/pg-pass'] matched /usr/local/bin/pg-pass), drop the deprecated ENABLE_SERVER_PASS_EXEC_CMD line, and restart Apache. On our VM the stored command became local-postgres on that restart and the warning stopped. A command that matches no entry, say one with extra flags, is wiped at that point, so copy each existing command into the allowlist before the restart, not after.

The full list of security fixes behind this change is in the pgAdmin 9.19 release notes. The same release also added SERVER_MODE_ALLOWED_SERVICES, an allowlist for libpq service names that is empty by default, so a server set to use a pg_service.conf entry won’t connect until you list that service in config_system.py. Listing a service shares its credentials with every pgAdmin user, so only list services meant for everyone.

Troubleshooting

These three errors all came up during the lab runs, and each has a one-line cause.

Could not bind TCP port 80 because it is already in use

certbot’s standalone mode tried to answer the challenge while Nginx held port 80. This is what a certificate requested without the hooks hits on every renewal:

Could not bind TCP port 80 because it is already in use by another process on this system (such as a web server). Please stop the program in question and then try again.

The first certificate usually works, because people stop Nginx by hand before requesting it. Renewal is where it breaks, since the timer runs months later with Nginx up and nobody there to stop it. Rerun the certbot certonly command from the SSL section with both hooks, then confirm with sudo certbot renew --dry-run while Nginx is running.

unknown directive “http2” on Debian 12

The Debian 13 config was copied to Debian 12 unchanged. Nginx 1.22 does not know the separate directive, and nginx -t fails like this:

2026/10/31 05:43:58 [emerg] 7406#7406: unknown directive "http2" in /etc/nginx/sites-enabled/pgadmin:9
nginx: configuration file /etc/nginx/nginx.conf test failed

Nginx only learned the separate HTTP/2 directive in the 1.25 line, and Debian 12 ships 1.22. Delete the http2 line and append http2 to the 443 listen line, so it reads listen 443 ssl http2 followed by the usual semicolon. The test then passes and the site still negotiates HTTP/2.

HTTP 500 and PermissionError on config_system.py

Every pgAdmin URL returns 500 after you add /etc/pgadmin/config_system.py, and the Apache error log ends with:

PermissionError: [Errno 13] Permission denied: '/etc/pgadmin/config_system.py'

Apache runs pgAdmin as www-data, which cannot read a 0600 file owned by root, so the config import fails before pgAdmin can even render its error page. Running sudo chmod 0644 /etc/pgadmin/config_system.py and restarting Apache fixed it on our VM. Keep secrets out of that file for the same reason, because once it’s world readable anyone with a shell on the box can read it.

Debian 13 vs Debian 12 Differences

The pgAdmin side is identical on both releases. What changes is the stack around it, mostly the PostgreSQL version and the Nginx HTTP/2 syntax:

ItemDebian 13 (Trixie)Debian 12 (Bookworm)
pgAdmin version tested9.199.19 (fresh and upgraded from 9.18)
Repository suitetrixie (from pgAdmin 9.8)bookworm (from pgAdmin 7.0)
Default PostgreSQL17.1115.19
Apache2.4.692.4.68
Nginx1.26.3, http2 on;1.22.1, listen 443 ssl http2;
certbot4.0.02.1.0
pg_hba.conf path/etc/postgresql/17/main//etc/postgresql/15/main/
Install and setup-web.sh stepsIdenticalIdentical
Versions captured on the lab VMs in October 2026. Repository support dates: pgAdmin APT repository

If you’re starting fresh, take Debian 13. It gets the newer PostgreSQL by default and Nginx’s current HTTP/2 syntax, and both releases receive pgAdmin builds on the same day. Debian 12 remains a fine host until its own end of life, but every config you copy from newer guides will need the HTTP/2 edit above. For the same setup on Ubuntu, see our pgAdmin 4 on Ubuntu guide.

Keep reading

Configure Samba File Share on Debian 13 / 12 Debian Configure Samba File Share on Debian 13 / 12 Backup and Restore Linux Systems with Timeshift Debian Backup and Restore Linux Systems with Timeshift Install KVM on Debian 13 / Debian 12: Complete Guide KVM Install KVM on Debian 13 / Debian 12: Complete Guide Upgrade a Single Package on Ubuntu / Debian with apt Debian Upgrade a Single Package on Ubuntu / Debian with apt APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Cheat Sheets APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Steps of Installing MariaDB or MySQL on Rocky 9|AlmaLinux 9 AlmaLinux Steps of Installing MariaDB or MySQL on Rocky 9|AlmaLinux 9

4 thoughts on “Install pgAdmin 4 on Debian 13 / 12”

Leave a Comment

Press ESC to close