How to Install LAPP Stack on Ubuntu 24.04 VPS — Linux, Apache, PostgreSQL, PHP for Web Apps
The LAPP stack (Linux, Apache, PostgreSQL, PHP) is the PostgreSQL-powered cousin of the venerable LAMP stack. Instead of MySQL/MariaDB, you run PostgreSQL — the open-source relational database trusted by companies like Apple, Instagram, Reddit, and Spotify for its strict SQL compliance, advanced data types (JSONB, arrays, ranges, geometric types), and rock-solid ACID guarantees. Paired with Apache 2.4 running the event MPM and PHP 8.3 via PHP-FPM, you get a stack that is as fast as anything written in Python or Node, while remaining familiar to every PHP developer on the planet.
This guide walks you end-to-end: from a bare Ubuntu 24.04 VPS to a hardened production-ready LAPP stack serving a PHP application over HTTPS, with PostgreSQL 16, phpPgAdmin for database management, Let's Encrypt TLS certificates, security headers, and a firewall.
Skip the manual setup? Our CloudCore Starter VPS gives you a clean Ubuntu 24.04 base with 4 vCPU, 8 GB RAM and 100 GB NVMe for EUR 7.99/month — perfect for a LAPP stack that can handle tens of thousands of daily page views.
Table of Contents
What is the LAPP Stack?
The LAPP stack is a bundle of four open-source components that together form a complete web application platform:
- L — Linux: The operating system. Ubuntu 24.04 LTS (Noble Numbat) is supported until April 2029 and ships with modern kernel, systemd, and glibc versions that play nicely with every component below.
- A — Apache HTTP Server 2.4: The web server. Apache httpd accepts HTTP/HTTPS requests, serves static assets, and forwards dynamic PHP requests to PHP-FPM. Its rich
.htaccessecosystem, mature modules (mod_rewrite, mod_ssl, mod_headers), and flexible VirtualHost system make it the default choice for running multiple PHP applications on one server. - P — PostgreSQL 16: The relational database. PostgreSQL is an object-relational database management system known for its SQL standards compliance, transactional DDL, powerful indexing (B-tree, GiST, GIN, BRIN), and native support for JSON/JSONB, full-text search, and window functions.
- P — PHP 8.3: The application language. PHP 8.3 introduces typed class constants, readonly class cloning improvements, a new
json_validate()function, and continued JIT performance gains. Running it through PHP-FPM (FastCGI Process Manager) decouples PHP execution from Apache worker processes, letting Apache's event MPM handle thousands of concurrent keep-alive connections while PHP-FPM manages a right-sized pool of PHP workers.
pg4wp plugin, Drupal, MediaWiki) to modern headless APIs built with Slim or Mezzio — all talking to PostgreSQL instead of MySQL.LAPP vs LAMP: Why Choose PostgreSQL?
If you are familiar with LAMP (Linux, Apache, MySQL, PHP), the only difference in LAPP is swapping MySQL/MariaDB for PostgreSQL. Here is why that swap matters for most modern applications:
| Feature | MySQL 8.0 / MariaDB 11 | PostgreSQL 16 |
|---|---|---|
| SQL standards compliance | Partial (several quirks) | Very high |
| Transactional DDL (CREATE/ALTER inside transactions) | Limited | Full |
| JSON support | JSON (text-backed) | JSONB (binary, indexable, GIN indexes) |
| Full-text search | Basic built-in, needs plugins | Native tsvector with ranking and dictionaries |
| Window functions, CTEs, recursive CTEs | Yes (MySQL 8+) | Yes, richer implementation |
| Array, range, enum, UUID, geometric types | No (UUID via helpers) | Native |
| Table inheritance, partitioning | Partitioning only | Both, with declarative partitioning |
| Materialized views | No | Yes |
| Custom data types, operators, functions | Limited | Extensible (including in C, PL/pgSQL, PL/Python) |
| Extensions ecosystem | Few plugins | Huge (PostGIS, pg_trgm, pg_stat_statements, TimescaleDB, pgvector for AI embeddings) |
| Concurrent writes | Good (InnoDB row locks) | Excellent (MVCC, no read/write blocking) |
| Default license | GPL / dual | PostgreSQL License (permissive BSD-like) |
When LAPP beats LAMP
- Complex queries, analytics, reporting. PostgreSQL's query planner handles multi-join, window-function, and CTE-heavy workloads significantly better out of the box.
- JSON-heavy APIs.
JSONBwith GIN indexes makes NoSQL-style querying fast inside a relational database — no need for a separate document store for most use cases. - Geospatial apps. PostGIS is the industry-standard spatial extension; nothing on the MySQL side comes close.
- AI/ML features.
pgvectorturns PostgreSQL into a vector database for RAG pipelines and semantic search. - Data integrity matters. Check constraints, exclusion constraints, foreign keys with deferrable checks, and per-transaction DDL mean your schema is provably correct.
If you specifically need the LAMP stack instead, see our companion guide: How to Install LAMP Stack on Ubuntu 24.04.
Prerequisites
Before you start, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access.
- SSH access to your server.
- At least 2 GB RAM (4 GB+ recommended for comfortable use with PostgreSQL tuned for production).
- At least 20 GB of free disk space (PostgreSQL data, logs, PHP apps, and Let's Encrypt renewals).
- A domain name pointed at your server's public IPv4 (and ideally IPv6) — required for Let's Encrypt TLS.
Recommended Plan: CloudCore Starter>
The CloudCore Starter plan gives you everything needed for a production LAPP stack:>
- 4 vCPU cores
- 8 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- Ubuntu 24.04 LTS pre-installed
- EUR 7.99/month>
That is enough headroom for PostgreSQL's shared buffers, Apache's event MPM pool, PHP-FPM workers, and your application code — with capacity to handle tens of thousands of daily requests.
Connect via SSH:
ssh root@your-server-ipIf you prefer working as a non-root user, create one and give it sudo rights first:
adduser deploy
usermod -aG sudo deployStep 1: Update System Packages
Refresh the package index and upgrade anything that is already installed. Always start from a fully updated system — mismatched library versions are the #1 cause of installer headaches.
sudo apt update && sudo apt upgrade -yInstall a few baseline utilities we will rely on throughout the guide:
sudo apt install -y curl wget gnupg lsb-release ca-certificates software-properties-common ufwIf the kernel was upgraded, reboot before continuing:
sudo rebootReconnect after about a minute.
Step 2: Install Apache 2.4
Apache is in the default Ubuntu repositories — no third-party PPAs required.
sudo apt install -y apache2Enable and start the service (it starts automatically on install, but this makes the state explicit):
sudo systemctl enable --now apache2Verify it is running:
sudo systemctl status apache2Expected output:
● apache2.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/apache2.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-04-16 10:02:00 UTC; 10s agoCheck the version to confirm you are on 2.4.x:
apache2 -vExpected output:
Server version: Apache/2.4.58 (Ubuntu)
Server built: 2024-XX-XXOpen a browser to http://your-server-ip and you should see the default Ubuntu Apache welcome page. If nothing loads, the firewall may be blocking port 80 — we will configure UFW in Step 10.
For a deep dive on Apache-only setup (extra modules, logging, tuning), see How to Install Apache on Ubuntu 24.04.
Step 3: Install PostgreSQL 16
Ubuntu 24.04 ships with PostgreSQL 16 in its default repositories, so installation is a single command:
sudo apt install -y postgresql postgresql-contribThe postgresql-contrib metapackage pulls in commonly used extensions (pg_trgm, uuid-ossp, pgcrypto, hstore, pg_stat_statements) that most production apps end up needing.
Enable and start the service:
sudo systemctl enable --now postgresqlVerify:
sudo systemctl status postgresqlConfirm the version:
psql --versionExpected output:
psql (PostgreSQL) 16.x (Ubuntu 16.x-0ubuntu0.24.04.1)PostgreSQL on Ubuntu creates a system user named postgres that owns the database cluster. All administrative operations go through that user.
If you want the very latest PostgreSQL (17.x at time of writing) or need specific minor versions, add the official PGDG repository — full details in How to Install PostgreSQL on Ubuntu 24.04.
Step 4: Create a PostgreSQL Role and Database
By default PostgreSQL uses "peer" authentication on Unix sockets, which means the Linux user logging in must match the PostgreSQL role name. For a web application, you want a dedicated role and database.
Create an application role
Switch to the postgres system user and open the psql shell:
sudo -i -u postgres psqlYou will land at the postgres=# prompt. Create a role for your application, set a password, and create a database owned by that role:
CREATE ROLE appuser WITH LOGIN PASSWORD 'ChangeThisStrongP4ssw0rd!';
CREATE DATABASE appdb OWNER appuser ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0;
GRANT ALL PRIVILEGES ON DATABASE appdb TO appuser;
\qReplace appuser, appdb, and the password with values that match your application.
Enable password authentication over local TCP
PHP on the same server will connect over 127.0.0.1:5432. Tell PostgreSQL to accept password auth on that interface.
Edit the host-based authentication file:
sudo nano /etc/postgresql/16/main/pg_hba.confFind the line for IPv4 local connections and make sure it uses scram-sha-256 (the modern default):
# TYPE DATABASE USER ADDRESS METHOD
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256If your PHP app will live on the same box, you do not need to expose PostgreSQL beyond localhost. Leave listen_addresses at its default (localhost) in /etc/postgresql/16/main/postgresql.conf.
Reload the service:
sudo systemctl reload postgresqlVerify the new role can connect
psql -h 127.0.0.1 -U appuser -d appdb -WEnter the password you chose. You should see:
psql (16.x) SSL connection (protocol: TLSv1.3, ...) Type "help" for help.
appdb=>
Type \q to exit. The role works.
Step 5: Install PHP 8.3 and PHP-FPM
Ubuntu 24.04 ships with PHP 8.3 in its default archive, so you do not need the Ondřej Surý PPA unless you need a different version.
sudo apt install -y php8.3 php8.3-fpm php8.3-cli php8.3-common \
php8.3-pgsql php8.3-mbstring php8.3-xml php8.3-curl \
php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath php8.3-opcacheKey packages:
php8.3-fpm— the FastCGI Process Manager (PHP-FPM) daemon Apache will talk to.php8.3-pgsql— ships both thepgsqlextension and the modernpdo_pgsqldriver.php8.3-opcache— opcode cache, essential for production performance.php8.3-mbstring / xml / curl / zip / gd / intl / bcmath— standard extensions that virtually every framework (Symfony, Laravel, Drupal) requires.
sudo systemctl enable --now php8.3-fpm
sudo systemctl status php8.3-fpmVerify PHP sees both PostgreSQL drivers:
php -m | grep -i -E 'pgsql|pdo_pgsql'Expected output:
pdo_pgsql
pgsqlBoth extensions are now available. For a deeper tour of PHP-FPM pool tuning, worker management, and php.ini tweaks, see How to Install PHP-FPM on Ubuntu 24.04.
Step 6: Configure Apache with mpm_event and PHP-FPM
By default Ubuntu's Apache package enables the mpm_prefork module, which forks one process per request — extremely safe with mod_php, but slow and memory-hungry. Combined with PHP-FPM, the right choice is mpm_event, which uses a small pool of worker threads to juggle many keep-alive connections and hands PHP work off over FastCGI.
Switch to mpm_event
sudo a2dismod php8.3 mpm_prefork
sudo a2enmod mpm_event proxy_fcgi setenvif rewrite headers
sudo a2enconf php8.3-fpmWhat each command does:
a2dismod php8.3 mpm_prefork— disables mod_php (which only works with prefork) and the prefork MPM itself.a2enmod mpm_event— enables the event MPM (asynchronous keep-alive handling).a2enmod proxy_fcgi— enables the FastCGI proxy Apache uses to talk to PHP-FPM.a2enmod setenvif— lets the included FPM config conditionally set environment variables per request.a2enmod rewrite headers— mod_rewrite (for clean URLs /.htaccess) and mod_headers (for security headers in Step 10).a2enconf php8.3-fpm— enables/etc/apache2/conf-available/php8.3-fpm.conf, which wires.phpfiles to the PHP-FPM Unix socket at/run/php/php8.3-fpm.sock.
sudo apache2ctl configtestExpected:
Syntax OKReload Apache:
sudo systemctl restart apache2Verify the handoff works
Create a tiny info script (we will delete it again in a moment — never leave phpinfo() on a production server):
echo '<?php phpinfo();' | sudo tee /var/www/html/info.php > /dev/nullVisit http://your-server-ip/info.php. You should see a PHP info page showing:
Server API: FPM/FastCGI- A
pdo_pgsqlsection - A
pgsqlsection
a2enconf php8.3-fpm and restart Apache.Delete the info file immediately:
sudo rm /var/www/html/info.phpStep 7: Install phpPgAdmin (Optional)
phpPgAdmin is the PostgreSQL counterpart to phpMyAdmin — a web UI for managing databases, running queries, editing rows, and exporting data. It is entirely optional; command-line psql handles everything it does. If you want a GUI, the easiest path on Ubuntu 24.04 is a manual install (the old phppgadmin package was dropped from recent Ubuntu releases).
Download and place phpPgAdmin
cd /tmp
wget https://github.com/phppgadmin/phppgadmin/releases/download/REL_7-14-0/phpPgAdmin-7.14.0.tar.gz
tar xzf phpPgAdmin-7.14.0.tar.gz
sudo mv phpPgAdmin-7.14.0 /usr/share/phppgadmin
sudo chown -R www-data:www-data /usr/share/phppgadminConfigure it
Edit the config:
sudo nano /usr/share/phppgadmin/conf/config.inc.phpConfirm the following (these are the defaults and usually fine):
$conf['servers'][0]['desc'] = 'PostgreSQL';
$conf['servers'][0]['host'] = '127.0.0.1';
$conf['servers'][0]['port'] = 5432;
$conf['servers'][0]['sslmode'] = 'allow';
$conf['servers'][0]['defaultdb'] = 'template1';
$conf['extra_login_security'] = true;Leave extra_login_security = true — it prevents superuser logins (including postgres) through the web UI, which is what you want.
Expose it through Apache
Create a dedicated Apache config:
sudo tee /etc/apache2/conf-available/phppgadmin.conf > /dev/null <<'EOF' Alias /phppgadmin /usr/share/phppgadmin<Directory /usr/share/phppgadmin> Options FollowSymLinks DirectoryIndex index.php
<IfModule mod_authz_core.c> # Restrict to localhost plus a single admin IP Require local Require ip 203.0.113.50 </IfModule>
<FilesMatch \.php$> SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/" </FilesMatch> </Directory> EOF
sudo a2enconf phppgadmin sudo systemctl reload apache2
Replace 203.0.113.50 with your real admin IP, or remove that line to restrict to localhost only (in which case, tunnel through SSH with ssh -L 8080:localhost:80 user@server and browse to http://localhost:8080/phppgadmin).
Create a dedicated admin role in PostgreSQL to log in with (do not reuse postgres):
sudo -i -u postgres psql -c "CREATE ROLE dbadmin WITH LOGIN CREATEDB CREATEROLE PASSWORD 'AnotherStrongP4ssw0rd!';"Visit http://your-server-ip/phppgadmin and log in as dbadmin.
Step 8: Create a VirtualHost and Test the Stack
Real applications live under a domain name in their own document root, not /var/www/html. Set one up now.
Replace example.com everywhere below with your own domain. Point an A record (and AAAA if you have IPv6) at your server's IP before going further — Certbot in the next step needs working DNS.
Create the document root
sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com
sudo chmod -R 755 /var/www/example.comDrop in a smoke-test PHP file that will also prove PostgreSQL connectivity:
sudo -u www-data tee /var/www/example.com/public/index.php > /dev/null <<'EOF' <?php declare(strict_types=1);
$dsn = 'pgsql:host=127.0.0.1;port=5432;dbname=appdb'; try { $pdo = new PDO($dsn, 'appuser', 'ChangeThisStrongP4ssw0rd!', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ]); $row = $pdo->query('SELECT version() AS v')->fetch(PDO::FETCH_ASSOC); echo '<h1>LAPP stack is working.</h1>'; echo '<p>PHP ' . PHP_VERSION . ' connected to: ' . htmlspecialchars($row['v']) . '</p>'; } catch (PDOException $e) { http_response_code(500); echo 'Database error: ' . htmlspecialchars($e->getMessage()); } EOF
Define the VirtualHost
sudo tee /etc/apache2/sites-available/example.com.conf > /dev/null <<'EOF' <VirtualHost *:80> ServerName example.com ServerAlias www.example.com ServerAdmin [email protected]DocumentRoot /var/www/example.com/public
<Directory /var/www/example.com/public> Options -Indexes +FollowSymLinks AllowOverride All Require all granted </Directory>
ErrorLog ${APACHE_LOG_DIR}/example.com-error.log CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined </VirtualHost> EOF
Disable the default site, enable the new one, and reload:
sudo a2dissite 000-default.conf
sudo a2ensite example.com.conf
sudo apache2ctl configtest && sudo systemctl reload apache2Browse to http://example.com. You should see:
LAPP stack is working.
PHP 8.3.x connected to: PostgreSQL 16.x on x86_64-pc-linux-gnu, compiled by gcc ...Congratulations — Linux, Apache, PostgreSQL, and PHP are all talking to each other. Now secure it.
Step 9: Secure with Let's Encrypt TLS (Certbot)
Certbot automates issuing and renewing free Let's Encrypt certificates. The python3-certbot-apache plugin edits your VirtualHost, sets up the HTTPS redirect, and schedules renewal.
sudo apt install -y certbot python3-certbot-apacheRun Certbot against your VirtualHost:
sudo certbot --apache -d example.com -d www.example.com \
--agree-tos -m [email protected] --redirect --no-eff-emailWhat the flags do:
--apache— use the Apache plugin to automatically configure both the certificate and theRedirectfrom HTTP to HTTPS.-d example.com -d www.example.com— cover both apex andwwwnames with a single cert.--redirect— automatically add the 301 redirect from port 80 to 443.-m ... --agree-tos --no-eff-email— non-interactive registration.
/etc/apache2/sites-available/example.com-le-ssl.conf— a new VirtualHost on port 443./etc/letsencrypt/live/example.com/{fullchain,privkey}.pem— the actual cert files.- A systemd timer
snap.certbot.renew.timer(orcertbot.timeron the apt version) that renews twice a day.
sudo certbot renew --dry-runYou should see "Congratulations, all simulated renewals succeeded."
Browse to https://example.com — you should now see a padlock and your PHP page served over TLS 1.3.
Step 10: Harden Security Headers and Firewall
A working stack is not a safe stack. Two quick steps lock it down.
Security headers
Add a reusable include with recommended headers:
sudo tee /etc/apache2/conf-available/security-headers.conf > /dev/null <<'EOF'Sent on every response
Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'"Hide Apache / PHP fingerprint
ServerTokens Prod ServerSignature Off Header unset X-Powered-By Header always unset X-Powered-By EOF
sudo a2enconf security-headers sudo systemctl reload apache2
Also tell PHP to stop advertising itself. Edit /etc/php/8.3/fpm/php.ini:
expose_php = OffThen:
sudo systemctl restart php8.3-fpmYou can grade the result at securityheaders.com — you should get an A or A+ with the config above. For a stricter policy, add a Content-Security-Policy header tuned to your application.
UFW firewall
Ubuntu ships with UFW preinstalled but inactive. Lock down everything except SSH and HTTPS:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Apache Full'
sudo ufw enable
sudo ufw status verbose'Apache Full' is an app profile shipped with the apache2 package that opens ports 80 and 443. PostgreSQL on 127.0.0.1:5432 is never exposed to the public network, so no firewall rule is needed for it.
Expected status output:
To Action From
-- ------ ----
OpenSSH ALLOW IN Anywhere
Apache Full ALLOW IN Anywhere
OpenSSH (v6) ALLOW IN Anywhere (v6)
Apache Full (v6) ALLOW IN Anywhere (v6)If you manage the server over a non-standard SSH port, open that port before enabling UFW — otherwise you will lock yourself out.
PHP + PostgreSQL Connection Example
The smoke-test in Step 8 used PDO inline. Real apps should keep credentials out of code and wrap the connection in a reusable helper.
Create a small config file outside the document root:
sudo mkdir -p /var/www/example.com/config sudo tee /var/www/example.com/config/db.php > /dev/null <<'EOF' <?php return [ 'dsn' => 'pgsql:host=127.0.0.1;port=5432;dbname=appdb', 'username' => 'appuser', 'password' => getenv('APP_DB_PASSWORD') ?: 'ChangeThisStrongP4ssw0rd!', ]; EOF
sudo chown -R www-data:www-data /var/www/example.com/config sudo chmod 640 /var/www/example.com/config/db.php
Then use it from the app:
<?php declare(strict_types=1);function db(): PDO { static $pdo = null; if ($pdo !== null) { return $pdo; }
$config = require __DIR__ . '/../config/db.php';
$pdo = new PDO($config['dsn'], $config['username'], $config['password'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_PERSISTENT => false, ]);
$pdo->exec("SET TIME ZONE 'UTC'"); return $pdo; }
// Example: insert + select with a prepared statement $pdo = db(); $pdo->exec('CREATE TABLE IF NOT EXISTS visits ( id BIGSERIAL PRIMARY KEY, ip INET NOT NULL, ua TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() )');
$stmt = $pdo->prepare('INSERT INTO visits (ip, ua) VALUES (:ip, :ua)'); $stmt->execute([ ':ip' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0', ':ua' => $_SERVER['HTTP_USER_AGENT'] ?? null, ]);
$total = $pdo->query('SELECT count(*) AS c FROM visits')->fetchColumn(); echo "Total visits: {$total}";
Notes:
PDO::ATTR_EMULATE_PREPARES => falseforces real server-side prepared statements, which is what you want with PostgreSQL for both performance and safety.INETis a native PostgreSQL type — no need to validate IP format in PHP, the database does it for you. That kind of small win is why you picked LAPP in the first place.- For production, load
APP_DB_PASSWORDfrom a systemd environment file or your secrets manager, not from the literal file.
Performance Tuning
On an 8 GB RAM server, the defaults are conservative. Three quick tweaks pay for themselves immediately.
PostgreSQL: postgresql.conf
Edit /etc/postgresql/16/main/postgresql.conf and set:
shared_buffers = 2GB # ~25% of RAM
effective_cache_size = 6GB # ~75% of RAM
work_mem = 16MB
maintenance_work_mem = 512MB
random_page_cost = 1.1 # NVMe SSD
effective_io_concurrency = 200
wal_buffers = 16MB
checkpoint_completion_target = 0.9Reload:
sudo systemctl restart postgresqlInstall pg_stat_statements for per-query metrics:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;And add shared_preload_libraries = 'pg_stat_statements' to postgresql.conf, then restart.
PHP-FPM pool
Edit /etc/php/8.3/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500Each PHP-FPM worker typically uses 30-80 MB; 20 workers caps PHP at ~1.5 GB RAM, leaving plenty for PostgreSQL and Apache.
Restart:
sudo systemctl restart php8.3-fpmApache event MPM
Edit /etc/apache2/mods-available/mpm_event.conf:
<IfModule mpm_event_module>
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxRequestWorkers 150
MaxConnectionsPerChild 10000
</IfModule>Reload:
sudo systemctl reload apache2OPcache
Add to /etc/php/8.3/fpm/conf.d/10-opcache.ini:
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=2
opcache.save_comments=1
opcache.jit_buffer_size=128M
opcache.jit=tracingJIT alone typically gives 10-30% throughput improvement on real-world workloads. Restart PHP-FPM after changes.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Apache serves .php as plain text / downloads the file | php8.3-fpm conf not enabled, or proxy_fcgi missing | sudo a2enconf php8.3-fpm && sudo a2enmod proxy_fcgi && sudo systemctl restart apache2 |
503 Service Unavailable with AH01079: failed to make connection | PHP-FPM is down or socket path wrong | sudo systemctl status php8.3-fpm; verify /run/php/php8.3-fpm.sock exists |
PDO error SQLSTATE[08006] could not connect to server | Bad host/port, or PostgreSQL not listening on 127.0.0.1 | Check listen_addresses in postgresql.conf; run sudo ss -tlnp</td><td>grep 5432 |
PDO error FATAL: password authentication failed | Role password wrong, or pg_hba.conf line is peer/ident for the IPv4 row | Set row to scram-sha-256, reload PostgreSQL, confirm password via psql -h 127.0.0.1 -U appuser |
| Certbot fails with "Problem binding to port 80" | Something else is on port 80 | sudo ss -tlnp \</td><td>grep :80; stop the conflicting service |
| Certbot fails DNS-01/HTTP-01 challenge | Domain not pointing to the server, or UFW blocking 80 | Verify dig example.com +short returns your IP; confirm UFW allows "Apache Full" |
phpPgAdmin login rejected for postgres | extra_login_security = true blocks superusers | Use the dbadmin role you created, not postgres |
| Site suddenly slow under load | PHP-FPM saturation | Watch sudo tail -f /var/log/php8.3-fpm.log for "server reached max_children"; raise pm.max_children |
| High PostgreSQL CPU from one query | Missing index | Enable pg_stat_statements, run SELECT query, total_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10; |
Key log locations
sudo tail -f /var/log/apache2/error.log
sudo tail -f /var/log/apache2/example.com-error.log
sudo tail -f /var/log/php8.3-fpm.log
sudo tail -f /var/log/postgresql/postgresql-16-main.log
sudo journalctl -u apache2 -u php8.3-fpm -u postgresql -fFAQ
Should I use LAPP or LAMP for WordPress?
WordPress core is MySQL-only. If you want to run WordPress on PostgreSQL you need the pg4wp translation layer, and even then some plugins break. For a pure WordPress site, pick LAMP. For any app where you control the schema — including Laravel, Symfony, Drupal (which has first-class PostgreSQL support), MediaWiki, Nextcloud, or any SaaS you are building yourself — LAPP is the better pick.
Can I run both PostgreSQL and MySQL on the same VPS?
Yes. They listen on different default ports (5432 and 3306) and don't conflict. That said, each one has its own memory footprint — PostgreSQL with reasonable tuning wants 2+ GB, and MySQL/MariaDB the same. On an 8 GB server you can run both, but you are paying RAM for two database engines. It is usually cleaner to pick one per project.
How do I migrate from MySQL to PostgreSQL?
Use pgloader, which in one command reads a live MySQL database and writes it into PostgreSQL, translating data types, indexes, and most constraints automatically. Validate with a diff of row counts per table afterward. Application code changes are usually limited to: LIMIT x, y → LIMIT y OFFSET x, backticks → double quotes for identifiers, and AUTO_INCREMENT → GENERATED ALWAYS AS IDENTITY or BIGSERIAL.
Is Apache slower than Nginx for PHP?
With mpm_event + proxy_fcgi to PHP-FPM, Apache's throughput is in the same ballpark as Nginx on the same hardware. The historical "Nginx is faster" advice came from the mod_php + prefork combination, which is genuinely slow. The configuration in this guide does not use mod_php. Pick Apache if you want .htaccess per-directory overrides and the rich module ecosystem; pick Nginx if you want a smaller binary and simpler reverse-proxy patterns. For LAPP, Apache is the conventional choice.
Do I need php8.3-pgsql or just php8.3-pdo-pgsql?
The Ubuntu package php8.3-pgsql installs both the legacy pgsql extension (pg_connect, pg_query, ...) and the modern pdo_pgsql driver (used by new PDO('pgsql:...')). Most frameworks use PDO. Installing php8.3-pgsql is the catch-all and what we recommend.
How do I back up PostgreSQL?
Use pg_dump for logical backups and a cron job to rotate them:
sudo -u postgres pg_dump -Fc appdb > /var/backups/appdb-$(date +%F).dumpFor large databases, switch to physical base backups via pg_basebackup or set up continuous archiving with pgBackRest. Always test restores — a backup you have never restored is a wish, not a backup.
Can I host multiple sites on one LAPP server?
Yes. Create one VirtualHost per domain (Step 8), each pointing at its own document root. Create one PostgreSQL role and database per app (Step 4). Certbot handles multiple certs with one certbot --apache -d site1.com -d site2.com ... command, or run it separately per site. On an 8 GB VPS, 5-10 small PHP sites co-exist comfortably.
Next Steps
Now that your LAPP stack is running and hardened, here are the things to tackle next:
- Deploy your first framework — Clone a Laravel or Symfony project, run
composer install, set theDATABASE_URLto point at theappuser/appdbyou created, and run migrations. Both frameworks detect PostgreSQL automatically. - Install a PostgreSQL monitoring stack —
pg_stat_statementsplus a lightweight exporter likepostgres_exporterfeeding Prometheus/Grafana gives you query-level visibility within 20 minutes of setup. - Add Redis for caching and sessions — PostgreSQL is your system of record; Redis keeps hot data (sessions, rate-limit counters, OPcache metadata) out of the database and off disk.
- Set up automated backups off-site — Combine
pg_dumpwithresticorrclonepushing encrypted dumps to S3-compatible storage nightly. - Read the PostgreSQL 16 release notes — postgresql.org/docs/16/release-16.html covers the SQL/JSON path improvements, logical replication enhancements, and parallel query gains that ship with this version.
Deploy a CloudCore Starter in 60 seconds>
Skip the provisioning queue and get an Ubuntu 24.04 VPS ready for this guide. The CloudCore Starter gives you 4 vCPU, 8 GB RAM, 100 GB NVMe for EUR 7.99/month — enough to run the full LAPP stack with headroom for traffic spikes.>
Deploy Your VPS Now