Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Lapp Stack Ubuntu
GUIDEInstall Guides

How to Install LAPP Stack on Ubuntu 24.04 VPS: Linux, Apache, PostgreSQL, PHP for Web Apps

30 min read

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?
  • LAPP vs LAMP: Why Choose PostgreSQL?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Install Apache 2.4
  • Step 3: Install PostgreSQL 16
  • Step 4: Create a PostgreSQL Role and Database
  • Step 5: Install PHP 8.3 and PHP-FPM
  • Step 6: Configure Apache with mpm_event and PHP-FPM
  • Step 7: Install phpPgAdmin (Optional)
  • Step 8: Create a VirtualHost and Test the Stack
  • Step 9: Secure with Let's Encrypt TLS (Certbot)
  • Step 10: Harden Security Headers and Firewall
  • PHP + PostgreSQL Connection Example
  • Performance Tuning
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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 .htaccess ecosystem, 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.
    The result: a stack that handles everything from classic PHP apps (Symfony, Laravel, WordPress via the 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:

    FeatureMySQL 8.0 / MariaDB 11PostgreSQL 16
    SQL standards compliancePartial (several quirks)Very high
    Transactional DDL (CREATE/ALTER inside transactions)LimitedFull
    JSON supportJSON (text-backed)JSONB (binary, indexable, GIN indexes)
    Full-text searchBasic built-in, needs pluginsNative tsvector with ranking and dictionaries
    Window functions, CTEs, recursive CTEsYes (MySQL 8+)Yes, richer implementation
    Array, range, enum, UUID, geometric typesNo (UUID via helpers)Native
    Table inheritance, partitioningPartitioning onlyBoth, with declarative partitioning
    Materialized viewsNoYes
    Custom data types, operators, functionsLimitedExtensible (including in C, PL/pgSQL, PL/Python)
    Extensions ecosystemFew pluginsHuge (PostGIS, pg_trgm, pg_stat_statements, TimescaleDB, pgvector for AI embeddings)
    Concurrent writesGood (InnoDB row locks)Excellent (MVCC, no read/write blocking)
    Default licenseGPL / dualPostgreSQL 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. JSONB with 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. pgvector turns 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.
    For a pure CMS with 10,000 simple reads per second, MySQL is fine. For almost anything else — SaaS apps, multi-tenant platforms, analytics dashboards, geospatial services, AI-augmented apps — PostgreSQL is the better default.

    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:

    bash
    ssh root@your-server-ip

    If you prefer working as a non-root user, create one and give it sudo rights first:

    bash
    adduser deploy
    usermod -aG sudo deploy

    Step 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.

    bash
    sudo apt update && sudo apt upgrade -y

    Install a few baseline utilities we will rely on throughout the guide:

    bash
    sudo apt install -y curl wget gnupg lsb-release ca-certificates software-properties-common ufw

    If the kernel was upgraded, reboot before continuing:

    bash
    sudo reboot

    Reconnect after about a minute.

    Step 2: Install Apache 2.4

    Apache is in the default Ubuntu repositories — no third-party PPAs required.

    bash
    sudo apt install -y apache2

    Enable and start the service (it starts automatically on install, but this makes the state explicit):

    bash
    sudo systemctl enable --now apache2

    Verify it is running:

    bash
    sudo systemctl status apache2

    Expected output:

    text
    ● 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 ago

    Check the version to confirm you are on 2.4.x:

    bash
    apache2 -v

    Expected output:

    text
    Server version: Apache/2.4.58 (Ubuntu)
    Server built:   2024-XX-XX

    Open 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:

    bash
    sudo apt install -y postgresql postgresql-contrib

    The 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:

    bash
    sudo systemctl enable --now postgresql

    Verify:

    bash
    sudo systemctl status postgresql

    Confirm the version:

    bash
    psql --version

    Expected output:

    text
    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:

    bash
    sudo -i -u postgres psql

    You will land at the postgres=# prompt. Create a role for your application, set a password, and create a database owned by that role:

    sql
    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;
    \q

    Replace 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:

    bash
    sudo nano /etc/postgresql/16/main/pg_hba.conf

    Find the line for IPv4 local connections and make sure it uses scram-sha-256 (the modern default):

    text
    # TYPE  DATABASE        USER            ADDRESS                 METHOD
    host    all             all             127.0.0.1/32            scram-sha-256
    host    all             all             ::1/128                 scram-sha-256

    If 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:

    bash
    sudo systemctl reload postgresql

    Verify the new role can connect

    bash
    psql -h 127.0.0.1 -U appuser -d appdb -W

    Enter the password you chose. You should see:

    text
    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.

    bash
    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-opcache

    Key packages:

    • php8.3-fpm — the FastCGI Process Manager (PHP-FPM) daemon Apache will talk to.
    • php8.3-pgsql — ships both the pgsql extension and the modern pdo_pgsql driver.
    • 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.
    Enable and start PHP-FPM:

    bash
    sudo systemctl enable --now php8.3-fpm
    sudo systemctl status php8.3-fpm

    Verify PHP sees both PostgreSQL drivers:

    bash
    php -m | grep -i -E 'pgsql|pdo_pgsql'

    Expected output:

    text
    pdo_pgsql
    pgsql

    Both 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

    bash
    sudo a2dismod php8.3 mpm_prefork
    sudo a2enmod mpm_event proxy_fcgi setenvif rewrite headers
    sudo a2enconf php8.3-fpm

    What 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 .php files to the PHP-FPM Unix socket at /run/php/php8.3-fpm.sock.
    Validate the configuration:

    bash
    sudo apache2ctl configtest

    Expected:

    text
    Syntax OK

    Reload Apache:

    bash
    sudo systemctl restart apache2

    Verify the handoff works

    Create a tiny info script (we will delete it again in a moment — never leave phpinfo() on a production server):

    bash
    echo '<?php phpinfo();' | sudo tee /var/www/html/info.php > /dev/null

    Visit http://your-server-ip/info.php. You should see a PHP info page showing:

    • Server API: FPM/FastCGI
    • A pdo_pgsql section
    • A pgsql section
    If instead you see a 404 or the file downloads as plain text, PHP-FPM is not wired up — re-run a2enconf php8.3-fpm and restart Apache.

    Delete the info file immediately:

    bash
    sudo rm /var/www/html/info.php

    Step 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

    bash
    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/phppgadmin

    Configure it

    Edit the config:

    bash
    sudo nano /usr/share/phppgadmin/conf/config.inc.php

    Confirm the following (these are the defaults and usually fine):

    php
    $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:

    bash
    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):

    bash
    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

    bash
    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.com

    Drop in a smoke-test PHP file that will also prove PostgreSQL connectivity:

    bash
    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

    bash
    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:

    bash
    sudo a2dissite 000-default.conf
    sudo a2ensite example.com.conf
    sudo apache2ctl configtest && sudo systemctl reload apache2

    Browse to http://example.com. You should see:

    text
    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.

    bash
    sudo apt install -y certbot python3-certbot-apache

    Run Certbot against your VirtualHost:

    bash
    sudo certbot --apache -d example.com -d www.example.com \
        --agree-tos -m [email protected] --redirect --no-eff-email

    What the flags do:

    • --apache — use the Apache plugin to automatically configure both the certificate and the Redirect from HTTP to HTTPS.
    • -d example.com -d www.example.com — cover both apex and www names with a single cert.
    • --redirect — automatically add the 301 redirect from port 80 to 443.
    • -m ... --agree-tos --no-eff-email — non-interactive registration.
    After Certbot finishes you will have:

    • /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 (or certbot.timer on the apt version) that renews twice a day.
    Test automatic renewal:

    bash
    sudo certbot renew --dry-run

    You 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:

    bash
    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:

    ini
    expose_php = Off

    Then:

    bash
    sudo systemctl restart php8.3-fpm

    You 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:

    bash
    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:

    text
    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:

    bash
    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
    <?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 => false forces real server-side prepared statements, which is what you want with PostgreSQL for both performance and safety.
    • INET is 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_PASSWORD from 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:

    text
    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.9

    Reload:

    bash
    sudo systemctl restart postgresql

    Install pg_stat_statements for per-query metrics:

    sql
    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:

    text
    pm = dynamic
    pm.max_children = 20
    pm.start_servers = 4
    pm.min_spare_servers = 2
    pm.max_spare_servers = 6
    pm.max_requests = 500

    Each PHP-FPM worker typically uses 30-80 MB; 20 workers caps PHP at ~1.5 GB RAM, leaving plenty for PostgreSQL and Apache.

    Restart:

    bash
    sudo systemctl restart php8.3-fpm

    Apache event MPM

    Edit /etc/apache2/mods-available/mpm_event.conf:

    text
    <IfModule mpm_event_module>
        StartServers             2
        MinSpareThreads          25
        MaxSpareThreads          75
        ThreadLimit              64
        ThreadsPerChild          25
        MaxRequestWorkers        150
        MaxConnectionsPerChild   10000
    </IfModule>

    Reload:

    bash
    sudo systemctl reload apache2

    OPcache

    Add to /etc/php/8.3/fpm/conf.d/10-opcache.ini:

    text
    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=tracing

    JIT alone typically gives 10-30% throughput improvement on real-world workloads. Restart PHP-FPM after changes.

    Troubleshooting

    ProblemCauseSolution
    Apache serves .php as plain text / downloads the filephp8.3-fpm conf not enabled, or proxy_fcgi missingsudo a2enconf php8.3-fpm && sudo a2enmod proxy_fcgi && sudo systemctl restart apache2
    503 Service Unavailable with AH01079: failed to make connectionPHP-FPM is down or socket path wrongsudo systemctl status php8.3-fpm; verify /run/php/php8.3-fpm.sock exists
    PDO error SQLSTATE[08006] could not connect to serverBad host/port, or PostgreSQL not listening on 127.0.0.1Check listen_addresses in postgresql.conf; run sudo ss -tlnp</td><td>grep 5432
    PDO error FATAL: password authentication failedRole password wrong, or pg_hba.conf line is peer/ident for the IPv4 rowSet 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 80sudo ss -tlnp \</td><td>grep :80; stop the conflicting service
    Certbot fails DNS-01/HTTP-01 challengeDomain not pointing to the server, or UFW blocking 80Verify dig example.com +short returns your IP; confirm UFW allows "Apache Full"
    phpPgAdmin login rejected for postgresextra_login_security = true blocks superusersUse the dbadmin role you created, not postgres
    Site suddenly slow under loadPHP-FPM saturationWatch sudo tail -f /var/log/php8.3-fpm.log for "server reached max_children"; raise pm.max_children
    High PostgreSQL CPU from one queryMissing indexEnable pg_stat_statements, run SELECT query, total_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

    Key log locations

    bash
    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 -f

    FAQ

    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:

    bash
    sudo -u postgres pg_dump -Fc appdb > /var/backups/appdb-$(date +%F).dump

    For 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 the DATABASE_URL to point at the appuser/appdb you created, and run migrations. Both frameworks detect PostgreSQL automatically.
    • Install a PostgreSQL monitoring stack — pg_stat_statements plus a lightweight exporter like postgres_exporter feeding 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_dump with restic or rclone pushing 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

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket