Four databases have been running on our lab server since Part V began: PostgreSQL in Chapter 18, MySQL in Chapter 19, MariaDB in Chapter 20, and MongoDB in Chapter 21. All of them are fully managed via the command line, ranging from psql, mysql/mariadb, to mongosh. This approach is sufficient for Sysadmins who are used to working in the terminal, but Developers who only need to check the contents of a single table or add a small index often feel that the command line is too slow for such a simple task, especially if that Developer has just joined and has not memorized the complete SQL syntax or MongoDB queries. That need is met by a web UI for database management, which is a browser-based application that wraps CRUD operations, user management, and raw query execution into a visual interface that is friendlier for daily use.
This chapter installs three web UIs simultaneously, each for a different database: phpMyAdmin for MySQL/MariaDB, pgAdmin 4 for PostgreSQL which this time runs in web mode instead of a desktop application, and mongo-express for MongoDB installed via npm following the Node.js conventions from Chapter 15. All three will be placed behind separate Nginx virtual hosts with their respective subdomains, following the pattern of adding subdomains to the example.local zone that we have practiced repeatedly since Chapter 12. This chapter concludes with a section that must not be missed, namely adding a reverse proxy layer and additional authentication. This is important because these three tools are essentially direct entry points to all data on the server, making them very attractive targets for Attackers if left open without layered security.
22.1 phpMyAdmin for MySQL/MariaDB
phpMyAdmin is a PHP-based web UI for MySQL and MariaDB that has existed since the late 1990s and remains one of the most widely used database administration tools in the field, especially in shared hosting environments and control panels like cPanel. Because phpMyAdmin runs on top of PHP-FPM, which we prepared in Chapter 14, its installation does not require heavy new components; we simply reuse the existing infrastructure.
22.1.1 Installing phpMyAdmin from Ubuntu Repository
Ubuntu Server provides phpMyAdmin directly from its official repository as the phpmyadmin package, without needing to add third-party repositories. This package uses dbconfig-common to set up its own internal database, which stores additional features such as query bookmarks and search history.
Practical Steps
- Update the package list, then install phpMyAdmin.
sudo apt update sudo apt install phpmyadmin - The installation process displays an interactive debconf dialog. On the first question, "Web server to reconfigure automatically", do not select anything because the available choices are only
apache2andlighttpd, whereas our main web server since Chapter 12 is Nginx, which is not listed. PressTabthenEnterto proceed without selecting any. - On the next question, "Configure database for phpmyadmin with dbconfig-common?", agree by selecting
yes. Sincerootfor both MySQL and MariaDB has been locked viaauth_socket/unix_socket since Section 19.2.2 and 20.1.2,dbconfig-commonrunning with system root permissions can connect directly without needing any password, then automatically creates thephpmyadmindatabase along with a dedicated internal account to store this configuration in/etc/phpmyadmin/config-inc.php. - Ensure the required PHP extensions are active. The
php-mbstringextension andphp-mysqldriver should already be installed since Section 14.1.2, so this step is merely a confirmation.php -m | grep -Ei 'mbstring|mysqli'
Verification and Troubleshooting
- The phpMyAdmin application files should already be available in
/usr/share/phpmyadmin. Confirm withls /usr/share/phpmyadmin; this directory will later serve as the document root for the Nginx virtual host in Section 22.1.2. - If the debconf dialog in step two was missed because the installation ran non-interactively, re-run
sudo dpkg-reconfigure phpmyadminto trigger all questions again. - The warning "The configuration file now needs a secret passphrase (blowfish_secret)" that sometimes appears on the login page means the line
$cfg['blowfish_secret']in/etc/phpmyadmin/config.inc.phpis still empty. Fill it with a random string of at least 32 characters, then save it again.
22.1.2 Nginx Virtual Host and PHP-FPM Integration
This section connects phpMyAdmin to Nginx via the FastCGI pattern identical to Section 14.3.1, except that this time the document root directly points to the phpMyAdmin application folder instead of the existing example.local folder.
Practical Steps
- Add a new DNS record for the subdomain
phpmyadmin.example.local, following the same pattern as theblogsubdomain in Section 12.2.2 andappin Section 16.2.2.sudo nano /etc/bind/db.example.local
Increment the Serial number inphpmyadmin IN A 192.168.1.20SOA, then validate and reload.sudo named-checkzone example.local /etc/bind/db.example.local sudo systemctl reload bind9 - Create a new virtual host file.
sudo nano /etc/nginx/sites-available/phpmyadmin.example.local - Fill it with the following configuration, reusing the exact same PHP-FPM socket from Section 14.1.1.
server { listen 80; server_name phpmyadmin.example.local; root /usr/share/phpmyadmin; index index.php; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.4-fpm.sock; } } - Enable this virtual host, test the syntax, and apply changes.
sudo ln -s /etc/nginx/sites-available/phpmyadmin.example.local /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Verification and Troubleshooting
- Test reachability first via curl from another computer on the lab network, sufficient to ensure Nginx is responding and forwarding to PHP-FPM correctly.
A healthy output displays the linecurl -s http://phpmyadmin.example.local | grep -i title<title>phpMyAdmin</title>. - Actual login testing still requires a real browser, because phpMyAdmin uses sessions and JavaScript that cannot be represented by curl. Open
http://phpmyadmin.example.localfrom a browser on another computer within the same lab network. - A
502 Bad Gatewayerror here has the same cause as noted in Section 14.3.1: PHP-FPM is not active or the socket path is misspelled. Check withsystemctl status php8.4-fpm.service.
22.1.3 Login and Access Verification to MySQL/MariaDB
In accordance with field notes in Section 19.2.2, accounts relying on auth_socket/unix_socket such as root cannot be used to log in through phpMyAdmin because its connection always uses TCP, not a Unix socket. The login process must use an account with a password, such as webapp_user created in Section 19.3.2 for MySQL or Section 20.3.1 for MariaDB.
Practical Steps
- Open
http://phpmyadmin.example.localin a browser. - Log in using the username
webapp_useralong with the password created in the previous chapter, depending on which instance is active on this lab server, whether MySQL from Chapter 19 or MariaDB from Chapter 20. - After logging in successfully, select the
webapp_dbdatabase in the left panel, open the SQL tab, and run a simple query to prove the connection works properly.SELECT DATABASE(), CURRENT_USER();
Verification and Troubleshooting
- The query result must display
webapp_dbandwebapp_user@localhost, identical to the command-line test in Section 19.3.2. - The message
#1045 Cannot log in to the MySQL serveron the login page usually means the password was mistyped, not a phpMyAdmin configuration issue itself, as phpMyAdmin simply passes credentials as-is to the database server. - Field note: never allow the
rootaccount to have a TCP password that can be used for phpMyAdmin login merely for convenience. A separate account with limited privileges likewebapp_userremains the safest choice for routine daily access, following the least privilege principle applied since Chapter 18.
22.2 pgAdmin 4 for PostgreSQL (Web Mode)
pgAdmin 4 is the official administration tool recommended by the PostgreSQL community itself, available in two modes: desktop mode running as an Electron application on a Developer's laptop, and web mode installed on a server and accessed via browser by multiple users simultaneously. This section focuses on web mode, as the goal is to provide centralized access to the lab PostgreSQL server without requiring every Developer to install a separate desktop application.
22.2.1 Installing pgAdmin 4 Web Mode from Official Repository
pgAdmin 4 is not available in the official Ubuntu repository, so its installation requires adding the official repository owned by the pgAdmin project itself, following the same pattern as adding the MongoDB repository in Section 21.2.1.
Practical Steps
- Download and import the official pgAdmin public key.
curl -fsS https://www.pgadmin.org/static/packages_pgadmin_org.pub | \ sudo gpg -o /usr/share/keyrings/packages-pgadmin-org.gpg --dearmor - Add the repository to
/etc/apt/sources.list.d/. The line below intentionally uses the codenamenoble(Ubuntu 24.04 LTS) rather thanresolutefor Ubuntu 26.04 that we are using, for the exact same reason as the MongoDB compatibility note in Section 21.5: third-party repositories take time to provide builds for every new Ubuntu release. Checkhttps://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/dists/first to verify ifresoluteis available before following this example as-is.echo "deb [signed-by=/usr/share/keyrings/packages-pgadmin-org.gpg] https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/noble pgadmin4 main" | \ sudo tee /etc/apt/sources.list.d/pgadmin4.list - Update the package list, then install only its web variant.
This package pullssudo apt update sudo apt install pgadmin4-weblibapache2-mod-wsgi-py3as a dependency, because pgAdmin 4 web mode runs as a Python WSGI application executed directly by Apache, not by Nginx or PHP-FPM.
Verification and Troubleshooting
- A
404 Not Founderror duringapt updatepointing to pathdists/noble/on theftp.postgresql.orgdomain means builds for that codename have been discontinued or mistyped, not a release compatibility issue. For such cases, recheck the supported codenames list at the URL in step two. - The
apache2package which was intentionally disabled (disabled, not removed) since the end of Chapter 14 will automatically be pulled as a dependency if it is not yet installed on this server.
22.2.2 Running setup-web.sh and Nginx Reverse Proxy
pgAdmin 4 web mode requires Apache to function properly, while Nginx still holds ports 80 and 443 as the main web server since Chapter 12. The solution is to run Apache on a local port unused by other applications, then let Nginx forward traffic to that port via reverse proxy, following the exact same upstream concept pattern from Section 16.2.2.
Practical Steps
- Change the port Apache listens on to avoid conflicts with Nginx. Change
Listen 80to a new local port.sudo nano /etc/apache2/ports.confListen 127.0.0.1:8082 - Match Apache's default virtual host to fit the new port.
Change the opening line fromsudo nano /etc/apache2/sites-available/000-default.conf<VirtualHost *:80>to the following.<VirtualHost 127.0.0.1:8082> - Test configuration, then enable and start Apache.
sudo apache2ctl configtest sudo systemctl enable --now apache2 - Run the official pgAdmin 4 setup script for web mode.
This script is interactive, asking for an email address and password for the initial pgAdmin administrator account, then offering automatic Apache configuration. Accept the Apache configuration offer, as we have prepared Apache specifically for this purpose.sudo /usr/pgadmin4/bin/setup-web.sh - Create a new Nginx virtual host file to forward traffic to Apache.
sudo nano /etc/nginx/sites-available/pgadmin.example.localserver { listen 80; server_name pgadmin.example.local; location / { proxy_pass http://127.0.0.1:8082; 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; } } - Add a DNS record for
pgadmin.example.local, following the same pattern as Section 22.1.2.sudo nano /etc/bind/db.example.localpgadmin IN A 192.168.1.20sudo named-checkzone example.local /etc/bind/db.example.local sudo systemctl reload bind9 - Enable virtual host, test syntax, and apply changes.
sudo ln -s /etc/nginx/sites-available/pgadmin.example.local /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Verification and Troubleshooting
- Ensure Apache is truly listening on loopback only, not all interfaces.
Correct output showsss -tlnp | grep 8082127.0.0.1:8082, not0.0.0.0:8082, because Apache here should only be accessed through Nginx, not directly from external networks. - Access the application via
http://pgadmin.example.local/pgadmin4/, not the domain without suffix, becausesetup-web.shinstalls the application by default at path/pgadmin4, not at the domain root. - A
502 Bad Gatewayerror from Nginx means Apache is not running or still listening on the old port. Confirm withsystemctl status apache2and repeat step one if necessary.
22.2.3 Login and Connecting PostgreSQL Server
pgAdmin becomes useful only after being registered to the PostgreSQL instance you wish to manage, as this application is essentially just an interface, not the database itself. Server registration is done once and saved permanently in the administrator account created in Section 22.2.2.
Practical Steps
- Open
http://pgadmin.example.local/pgadmin4/in a browser, then log in using the administrator email and password created duringsetup-web.shin Section 22.2.2. - Right-click on Servers in the left panel, select Register > Server, and fill the General tab with any name, for example
Lab PostgreSQL. - Switch to the Connection tab, fill Host name/address with
127.0.0.1, Port with5432, Maintenance database withwebapp_db, Username withwebapp_user, and the password created in Section 18.3.1. This connection is automatically allowed by the rulehost all all 127.0.0.1/32 scram-sha-256present since initial installation in Section 18.2.1, without needing new rules inpg_hba.conf. - Save, then navigate to Lab PostgreSQL > Databases > webapp_db > Schemas > public > Tables to ensure the
notestable from Section 18.3.2 is visible with its data.
Verification and Troubleshooting
- A
connection refusedmessage when saving a new server usually means PostgreSQL is not running on the same server as Apache/pgAdmin, check again withpg_lsclustersfrom Section 18.1.2. - The message
password authentication failed for user "webapp_user"indicates a mistyped password when filling the connection form, not an issue with pgAdmin itself. - The built-in pgAdmin query tool (lightning icon in toolbar) can be used directly to run raw SQL such as
SELECT * FROM notes;, useful for quick troubleshooting without switching to thepsqlterminal.
22.3 mongo-express for MongoDB
mongo-express is a Node.js and Express-based web UI for MongoDB, installed via npm instead of APT packages because it is distributed as a standard Node.js package. Unlike phpMyAdmin and pgAdmin 4 which are actively developed by large communities, mongo-express release cadence has been much slower in recent years. Check the latest available version before installing, and consider that this tool is best suited for internal or lab needs like this series, rather than being the sole MongoDB administration reliance in large-scale production environments.
22.3.1 Installation via npm and Environment Variable Configuration
This section installs mongo-express as a global package, following the habit of installing command-line tools via npm install -g introduced in Chapter 15, then sets up a dedicated system user to run it as a service.
Practical Steps
- Check the latest available version of mongo-express in the npm registry before installation, given its slow release cadence mentioned earlier.
npm view mongo-express version - Install mongo-express globally, using Node.js from NodeSource installed since Chapter 15.
If installation fails due to peer dependency conflicts with newer Node.js versions, retry with the following additional flag.sudo npm install -g mongo-expresssudo npm install -g mongo-express --legacy-peer-deps - Note the binary location of this global installation because systemd requires a full path, just like the important note in Section 15.4.3 regarding
ExecStart.which mongo-express - Create a dedicated system user for this service, following the same least privilege principle as Section 5.2.2 and Section 15.4.3.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin mongoexpress
Verification and Troubleshooting
- A healthy
which mongo-expresscommand returns a path like/usr/bin/mongo-express, matching the npm global bin location from NodeSource installation. Save this path as it will be used directly in Section 22.3.2. - An
EACCES: permission deniederror duringnpm install -gusually means the command was run withoutsudo, as the npm global directory from NodeSource installation is owned byroot.
22.3.2 systemd Service and Nginx Reverse Proxy
mongo-express is configured purely via environment variables without separate configuration files needing edits, so all MongoDB connection settings and access credentials can be written directly into the systemd unit file.
Practical Steps
- Create a new unit file.
sudo nano /etc/systemd/system/mongo-express.service - Fill it with the following configuration. Replace the values of
ME_CONFIG_MONGODB_ADMINPASSWORDandME_CONFIG_BASICAUTH_PASSWORDwith strong passwords of your own before practicing on a real server.
The values[Unit] Description=mongo-express Web UI for MongoDB After=network-online.target mongod.service Wants=network-online.target [Service] Type=simple User=mongoexpress Group=mongoexpress Environment=ME_CONFIG_MONGODB_SERVER=127.0.0.1 Environment=ME_CONFIG_MONGODB_PORT=27017 Environment=ME_CONFIG_MONGODB_ADMINUSERNAME=admin Environment=ME_CONFIG_MONGODB_ADMINPASSWORD=AdminMongoAman!2026 Environment=ME_CONFIG_MONGODB_AUTH_DATABASE=admin Environment=ME_CONFIG_MONGODB_ENABLE_ADMIN=true Environment=ME_CONFIG_SITE_HOST=127.0.0.1 Environment=ME_CONFIG_SITE_PORT=8081 Environment=ME_CONFIG_BASICAUTH_USERNAME=sysadmin Environment=ME_CONFIG_BASICAUTH_PASSWORD=UbahDariDefault!2026 ExecStart=/usr/bin/mongo-express Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetME_CONFIG_MONGODB_ADMINUSERNAMEandME_CONFIG_MONGODB_ADMINPASSWORDrefer directly to theadminaccount created in Section 21.4.1. The twoME_CONFIG_BASICAUTH_*variables set mongo-express's built-in HTTP Basic Authentication, the first layer before the additional layer we will install in Section 22.4. A critical field note: older mongo-express versions used default basic auth credentialsadmin/passwhich are widely known and frequently targeted automatically by Attackers scanning the internet for unmanaged instances, so never leave these default values unchanged. - Adjust the path in
ExecStartif the output ofwhich mongo-expressin Section 22.3.1 was not exactly/usr/bin/mongo-express. - Reload systemd, then enable and start the service.
sudo systemctl daemon-reload sudo systemctl enable --now mongo-express.service - Create an Nginx virtual host file to forward traffic to mongo-express.
sudo nano /etc/nginx/sites-available/mongoexpress.example.localserver { listen 80; server_name mongoexpress.example.local; location / { proxy_pass http://127.0.0.1:8081; 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; } } - Add a DNS record for
mongoexpress.example.local.sudo nano /etc/bind/db.example.localmongoexpress IN A 192.168.1.20sudo named-checkzone example.local /etc/bind/db.example.local sudo systemctl reload bind9 - Enable virtual host, test syntax, and apply changes.
sudo ln -s /etc/nginx/sites-available/mongoexpress.example.local /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Verification and Troubleshooting
- Check service logs if mongo-express fails to start.
sudo journalctl -u mongo-express -n 50 - Open
http://mongoexpress.example.localin a browser. The browser will first prompt mongo-express's built-in HTTP Basic Auth login dialog, asking for username and password according toME_CONFIG_BASICAUTH_USERNAME/ME_CONFIG_BASICAUTH_PASSWORDset in step two, before accessing the main view containingwebapp_catalogdatabase from Section 21.3.2. - The error
MongoServerError: Authentication faileddisplayed injournalctlindicates that the MongoDB admin username and password combination is incorrect; verify against those created in Section 21.4.1.
22.4 Access Security: Reverse Proxy and Additional Authentication
All three web UIs above can now be accessed by anyone who knows the subdomain and is on a network that can reach port 80 of this server. This is a serious problem. phpMyAdmin, pgAdmin 4, and mongo-express each represent direct entry points to all data on the server. Furthermore, real-world internet security incident histories are filled with cases of database administration panels exposed without protection and exploited by Attackers within hours. The defense in depth principle requires adding a protection layer at the Nginx level, separate from each application's authentication, so that a single failed or bypassed layer does not grant full access immediately.
22.4.1 HTTP Basic Auth Layer with htpasswd
Adding HTTP Basic Authentication at the Nginx level means an Attacker must pass two sets of credentials before reaching the actual application login page: one from Nginx and another from the application itself (phpMyAdmin, pgAdmin 4, or mongo-express's built-in basic auth from Section 22.3.2).
Practical Steps
- Install
apache2-utilsto obtain thehtpasswdcommand. This package is safe to install even if Apache itself is disabled, becausehtpasswdis a standalone utility that does not rely on a running Apache service.sudo apt install apache2-utils - Create a new password file for the first Sysadmin, using the
-cflag only once to create a new file.sudo htpasswd -c /etc/nginx/.htpasswd-dbadmin sysadmin - Create a separate snippet file so the same rules can be reused across all three virtual hosts without rewriting identical lines, following the
includeconvention used forsnippets/fastcgi-php.confsince Section 14.3.1.sudo nano /etc/nginx/snippets/dbadmin-restrict.confauth_basic "Restricted Area - Database Admin"; auth_basic_user_file /etc/nginx/.htpasswd-dbadmin; - Include this snippet in all three virtual host files created in Sections 22.1.2, 22.2.2, and 22.3.2. Add the following line inside the
server { }block of each file, for example forphpmyadmin.example.local.sudo nano /etc/nginx/sites-available/phpmyadmin.example.local
Repeat the exact sameserver { listen 80; server_name phpmyadmin.example.local; include snippets/dbadmin-restrict.conf; root /usr/share/phpmyadmin; index index.php; ... }includeline forpgadmin.example.localandmongoexpress.example.local. - Test syntax, then apply to all three virtual hosts at once.
sudo nginx -t sudo systemctl reload nginx
Verification and Troubleshooting
- Access without credentials must be rejected with status
401 Unauthorized.curl -I http://phpmyadmin.example.local - Access with correct credentials must successfully pass this layer.
curl -I -u sysadmin http://phpmyadmin.example.local - Add another user to the same password file without the
-cflag, as that flag overwrites the entire existing file contents.sudo htpasswd /etc/nginx/.htpasswd-dbadmin developer
22.4.2 Restricting Access Based on Source IP
The second layer restricts access based on source IP address by evaluating rules sequentially from top to bottom, exactly like how pg_hba.conf works as discussed in Section 18.2.1. Note that this restriction cannot be done via UFW as in Section 18.4.2 or 19.4.1, because UFW filters by port, whereas these three subdomains share the exact same port 80 as example.local and other public domains that must remain open for general Visitors. Nginx, which knows which domain is requested via the Host header, is the only layer capable of distinguishing access per subdomain like this.
Practical Steps
- Add
allowanddenyrules to the same snippet file from Section 22.4.1, placed before theauth_basicdirective.sudo nano /etc/nginx/snippets/dbadmin-restrict.confallow 192.168.1.0/24; deny all; auth_basic "Restricted Area - Database Admin"; auth_basic_user_file /etc/nginx/.htpasswd-dbadmin; - Test syntax, then apply changes.
sudo nginx -t sudo systemctl reload nginx
Verification and Troubleshooting
- Access from IPs outside
192.168.1.0/24must be directly rejected with status403 Forbidden, even before being prompted for HTTP Basic Auth credentials, becauseallow/denydirectives are evaluated prior toauth_basic. - The combination of
allow/denyandauth_basicforms two complementary layers rather than replacements for each other. Allowed IPs are still required to input correct credentials, while valid credentials alone will be useless if the source IP is not included in theallowrule. - For comprehensive security, these three subdomains should also be protected by TLS using the same SAN self-signed certificate as Section 17.3.1, because HTTP Basic Auth traffic is merely Base64 encoded, not encrypted, making it readable by Attackers sniffing network traffic if not running over HTTPS.
At this point, the server runs three database web UIs side by side: phpMyAdmin for MySQL/MariaDB, pgAdmin 4 web mode for PostgreSQL, and mongo-express for MongoDB, each on separate subdomains protected by two simultaneous layers: HTTP Basic Auth and IP restriction, on top of each application's built-in authentication. Chapter 23 continues Part V with an equally crucial topic: database backup and restore, covering all four databases installed from Chapter 18 through Chapter 21.

