If you've stood up a web server on Linux, you've probably reached for the LAMP stack by reflex: Linux, Apache, MySQL, PHP. It's well-documented, widely supported, and the default recommendation in most hosting tutorials. But there's a quieter alternative that has been running production workloads for decades and is gaining serious traction in 2026 — the LAPP stack, which swaps MySQL for PostgreSQL.
That single substitution carries more weight than it might appear. The choice of database shapes how your application handles concurrent writes, schema changes, complex queries, and data integrity failures. PostgreSQL and MySQL are both capable, but they solve these problems differently, and the differences matter at scale. This guide covers what the LAPP stack is, how to build one from scratch on Ubuntu, how to secure it for production, and — crucially — why PostgreSQL earns its place in place of MySQL when the requirements get demanding.
Apache does not execute PHP. It forwards requests matching
\.php$ through mod_proxy_fcgi to the PHP-FPM Unix socket at /run/php/php8.4-fpm.sock.
/etc/php/8.4/fpm/pool.d/www.conf — the pm = dynamic mode adjusts the active worker count based on load.OPcache compiles PHP source to bytecode on first execution and caches the result in shared memory, eliminating the parse-and-compile cost on subsequent requests. JIT (enabled via
opcache.jit = tracing) compiles hot bytecode paths to native machine code at runtime.
127.0.0.1:5432. The connection string lives in a DSN; credentials come from environment variables. PostgreSQL validates the connection against pg_hba.conf before accepting it.The new async I/O subsystem in PostgreSQL 18 improves throughput for queries that read large amounts of data from disk — sequential scans, bitmap heap scans, and bulk COPY operations all benefit. For applications using
pgvector, this directly reduces latency on approximate nearest-neighbor queries over large embedding tables.
apache2, php8.4-fpm, and postgresql. systemd handles service restarts on failure and integrates with journald for centralized logging.
What Is the LAPP Stack?
LAPP stands for Linux, Apache, PHP (or Python/Perl), and PostgreSQL. The PostgreSQL documentation has acknowledged LAPP for years alongside MAPP and WAPP (Mac and Windows variants) as officially recognized deployment configurations. As the PostgreSQL project notes on its downloads page, these stacks are distributed by providers like BitNami and TurnKey Linux as ready-to-deploy appliances.
The acronym mirrors LAMP almost exactly. The difference is purely in the database layer. Where LAMP uses MySQL (or its community fork MariaDB), LAPP uses PostgreSQL — an object-relational database system with roots in the POSTGRES research project at UC Berkeley, which began in 1986. PostgreSQL has been continuously developed since then. PostgreSQL 18 — the current major version as of 2026 — was released in September 2025 and introduced a new asynchronous I/O subsystem, virtual generated columns, and the uuidv7() function for timestamp-ordered UUID generation.
PostgreSQL 18.3, 17.9, 16.13, 15.17, and 14.22 were released on February 26, 2026. PostgreSQL 18 is the current major version. PHP 8.5 (released November 20, 2025) is the current stable branch and the recommended choice for new deployments; PHP 8.4 is actively maintained with bug-fix and security support through December 2026. PHP 8.3 is in security-fix-only mode as of December 2025. Ubuntu 24.04 ships PHP 8.3 by default, but PHP 8.4 and 8.5 are available via the Ondrej Sury PPA (ppa:ondrej/php). Apache 2.4 remains the current stable server branch.
The LAPP stack is entirely free and open-source software. Every component is available under licenses that permit commercial use, modification, and redistribution. For organizations concerned about vendor lock-in, this matters: PostgreSQL is developed by the PostgreSQL Global Development Group, a non-profit community. MySQL is owned and maintained by Oracle Corporation.
PostgreSQL vs. MySQL: Why the Swap Matters
Before getting into installation, it's worth understanding concretely why someone chooses LAPP over LAMP. The differences between PostgreSQL and MySQL are architectural, not cosmetic.
ACID Compliance
Both databases claim ACID compliance, but the claim is stronger for PostgreSQL. As the PostgreSQL project documents, PostgreSQL is fully ACID-compliant in all configurations and enforces atomicity, consistency, isolation, and durability for all database transactions. MySQL's ACID compliance depends on which storage engine is in use. With InnoDB, it is ACID-compliant; with MyISAM (still encountered in legacy databases), it is not. MySQL's default configurations also tend to lean toward speed over strict isolation, meaning that some edge cases around transaction boundaries behave differently than a developer expecting strict SQL semantics might expect.
"PostgreSQL fully supports ACID properties: atomicity, consistency, isolation, and durability." — PostgreSQL Project, postgresql.org/about
This is not an academic concern. It surfaces in practice during schema migrations, bulk inserts, and concurrent updates. PostgreSQL supports atomic DDL (Data Definition Language) operations, meaning that ALTER TABLE runs inside a transaction and can be rolled back on failure. If a migration script fails halfway through, you don't end up with a partially modified schema. MySQL's DDL handling is less consistent across versions and configurations.
MVCC and Concurrency
PostgreSQL uses Multi-Version Concurrency Control (MVCC) to manage concurrent access to data. Instead of acquiring row-level locks that block readers while writers are active, MVCC maintains multiple versions of a row simultaneously, allowing readers and writers to operate without blocking each other. Readers never block writers, and writers never block readers. This architecture means that PostgreSQL handles write-heavy workloads with high concurrency especially well — a characteristic that makes it the preferred database in financial applications, analytics pipelines, and any system where transactions overlap heavily.
SQL Standards Conformance and Data Types
PostgreSQL follows the SQL standard more strictly than MySQL. It enforces data types rigidly: inserting a string into an integer column fails immediately and explicitly. MySQL historically performed silent type coercions that could introduce data corruption silently. Strict mode (enabled by default in MySQL 8.0) improves this, but a significant number of production MySQL installations still run with older, more permissive settings inherited from prior versions.
PostgreSQL also supports a significantly richer set of native data types, including arrays, range types, JSONB (binary JSON with full indexing), network address types (inet, cidr), geometric types, and full-text search vectors. Crucially, PostgreSQL's extension system allows you to add new data types, index methods, and functions without modifying the server binary. The PostGIS extension adds production-grade spatial data support. The pgvector extension, which gained widespread adoption in 2024 and 2025, adds vector similarity search directly inside the database — the foundation for AI-adjacent applications that need to store and query high-dimensional embeddings alongside relational data.
Query Planner and Index Types
PostgreSQL's query planner is widely regarded as more sophisticated than MySQL's for complex queries involving multiple joins, subqueries, window functions, and CTEs (Common Table Expressions). It supports a wider range of index types as well: B-tree, Hash, GiST, GIN, BRIN, and partial indexes. MySQL supports B-tree and full-text indexes with some engine-dependent limitations. For applications that run complex analytical queries alongside transactional workloads, PostgreSQL's planner advantage is measurable.
CREATE INDEX with no explicit type specified. Handles equality, range, and pattern-prefix queries against sortable data.tsvector columnstsrange, int4range) queried for overlap or containmentdeleted_at IS NULLstatus = 'pending' rows in a jobs queuePostgreSQL 18 extended this with "skip scan" support, allowing multicolumn B-tree indexes to be used in more query patterns, and improved the performance of sequential scans and bitmap heap scans through its new asynchronous I/O subsystem. The PostgreSQL project's official announcement states that the new I/O subsystem demonstrated up to 3x performance improvements when reading from storage.
MySQL excels at read-heavy workloads with simple query patterns: content management systems, e-commerce platforms, and high-traffic sites that do mostly primary key lookups. WordPress, Drupal, and Magento are built around MySQL. If you're hosting one of those applications, LAMP is the documented and supported path. Use LAPP when your application has complex query requirements, needs strong transactional guarantees, or benefits from PostgreSQL's advanced data types.
Installing the LAPP Stack on Ubuntu
These instructions target Ubuntu 24.04 LTS, the current long-term support release. The commands are similar for Debian 12 and Rocky Linux 9, with package manager differences. For more on Ubuntu server administration basics, see the Ubuntu Server Management guide.
Step 1: Update the System
$ sudo apt update && sudo apt upgrade -y
Step 2: Install Apache
$ sudo apt install -y apache2 $ sudo systemctl enable --now apache2
Apache 2.4 on Ubuntu ships with the mpm_prefork module enabled by default. For production use with PHP-FPM, you should switch to mpm_event, which handles concurrent connections more efficiently through an event-driven architecture.
# Disable prefork, enable event MPM $ sudo a2dismod mpm_prefork $ sudo a2enmod mpm_event # Enable proxy modules for PHP-FPM $ sudo a2enmod proxy proxy_fcgi setenvif $ sudo systemctl restart apache2
Step 3: Install PHP and the PostgreSQL Extension
PHP communicates with PostgreSQL through two mechanisms: the native pgsql extension and the pdo_pgsql driver for PDO. Install both. PDO is the recommended interface for application code because it provides consistent prepared statement handling across database backends.
# PHP 8.4 is recommended for new deployments in 2026 (active support through Dec 2026) $ sudo apt install -y software-properties-common $ sudo add-apt-repository ppa:ondrej/php -y && sudo apt update $ sudo apt install -y php8.4-fpm php8.4-pgsql php8.4-common php8.4-cli php8.4-opcache $ sudo systemctl enable --now php8.4-fpm
Then wire Apache to hand PHP files off to PHP-FPM via a Unix socket rather than processing them inline. This provides process isolation and allows PHP to be restarted independently of Apache.
<FilesMatch "\.php$"> SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost/" </FilesMatch> DirectoryIndex index.php index.html # Deny direct access to sensitive file types <Files "\.(env|ini|log|sh|sql|yml)$"> Require all denied </Files>
$ sudo a2enconf php8.4-fpm $ sudo apachectl -t && sudo systemctl restart apache2
Step 4: Install PostgreSQL
Ubuntu 24.04's default repositories include PostgreSQL 16. To install PostgreSQL 17 or 18, use the official PGDG (PostgreSQL Global Development Group) repository.
# Add the PGDG signing key and repository $ sudo apt install -y curl ca-certificates $ sudo install -d /usr/share/postgresql-common/pgdg $ sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \ --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc # Source the OS release file to get VERSION_CODENAME (more reliable than lsb_release) $ . /etc/os-release $ sudo sh -c "echo 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \ https://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main' \ > /etc/apt/sources.list.d/pgdg.list" # Install PostgreSQL 18 $ sudo apt update && sudo apt install -y postgresql-18 $ sudo systemctl enable --now postgresql
Step 5: Create the Application Database and User
PostgreSQL ships with a system user named postgres and a superuser database account of the same name. Never use the superuser account for application connections. Create a dedicated database user with only the privileges it needs.
# Switch to the postgres system user $ sudo -u postgres psql -- Create the application database user postgres=# CREATE USER appuser WITH PASSWORD 'use-a-strong-password-here'; -- Create the application database postgres=# CREATE DATABASE appdb OWNER appuser; -- Grant database-level privileges postgres=# GRANT ALL PRIVILEGES ON DATABASE appdb TO appuser; -- Connect to the new database, then grant schema privileges -- Required since PostgreSQL 15: CREATE on public schema is no longer granted by default postgres=# \c appdb appdb=# GRANT ALL ON SCHEMA public TO appuser; -- Exit psql appdb=# \q
Since PostgreSQL 15, the CREATE privilege on the public schema is no longer granted to non-owner users by default. GRANT ALL PRIVILEGES ON DATABASE alone is not sufficient — without the subsequent GRANT ALL ON SCHEMA public TO appuser, your application user will receive permission denied for schema public when attempting to create or access tables. PostgreSQL 18 also uses scram-sha-256 as the default password authentication method, which is significantly stronger than the older md5 method. Verify that password_encryption = scram-sha-256 is set in /etc/postgresql/18/main/postgresql.conf.
Step 6: Connecting PHP to PostgreSQL
Use PDO with a PostgreSQL DSN. The connection string keeps credentials out of your query code and allows you to use prepared statements consistently.
<?php // Database connection using PDO + pdo_pgsql $dsn = 'pgsql:host=localhost;port=5432;dbname=appdb'; $username = 'appuser'; $password = getenv('DB_PASSWORD'); // never hardcode credentials $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements ]; try { $pdo = new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { // Log the error -- never echo it to the browser in production error_log($e->getMessage()); http_response_code(500); exit('Database connection failed.'); } // Example: parameterized query (protects against SQL injection) $stmt = $pdo->prepare('SELECT id, username FROM users WHERE email = :email'); $stmt->execute([':email' => $email]); $user = $stmt->fetch();
The ATTR_EMULATE_PREPARES => false setting is important. With it disabled, PDO sends prepared statements to PostgreSQL as actual server-side prepared statements rather than emulating them in the library. This means PostgreSQL validates the query structure once and executes it with different parameters efficiently, which matters for performance under load and provides stronger SQL injection protection.
pgsql Extension vs. pdo_pgsql: When to Use Which
PHP ships with two ways to talk to PostgreSQL: the native pgsql extension (functions like pg_connect(), pg_query(), pg_fetch_assoc()) and the pdo_pgsql driver for PDO. The install command above installs both via the php8.4-pgsql package, which provides both.
For most application code, PDO is the correct choice. It provides a consistent interface if you ever need to switch databases, it enforces prepared statement discipline, and frameworks like Laravel and Symfony expect it. However, the native pgsql extension gives you direct access to PostgreSQL-specific features that PDO does not expose: pg_copy_from() and pg_copy_to() for bulk COPY operations, pg_lo_create() for large object (binary data) handling, and pg_get_notify() for reading messages from PostgreSQL LISTEN/NOTIFY channels. If your application needs any of those — bulk imports, file storage in PostgreSQL, or real-time event channels — use the native extension directly for those operations and PDO for everything else.
Hardening the LAPP Stack for Production
A default LAPP installation is functional but not production-ready. Apache, PHP, and PostgreSQL all ship with settings that prioritize compatibility and ease of setup over security. The following hardening steps should be applied before exposing a LAPP server to the internet. For a broader view of server security on Linux, the Zero-Trust Security on Linux guide and the SSH hardening guide are useful companion reading.
Hardening Apache
Apache's default configuration exposes version information in HTTP response headers. An attacker who knows you are running Apache 2.4.58 on Ubuntu can search known vulnerability databases for that exact version. Suppress it.
# Suppress version disclosure from response headers ServerTokens Prod ServerSignature Off # Disable directory listing <Directory /var/www/> Options -Indexes </Directory> # Add security response headers <IfModule mod_headers.c> 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=(), camera=(), microphone=()" </IfModule> # Restrict HTTP methods to only what your application uses <LimitExcept GET POST HEAD> Require all denied </LimitExcept>
The Apache documentation notes that setting ServerTokens to Prod limits the server header to "Apache" with no version number or module details. This is the CIS Benchmark recommendation for Apache 2.4. The <LimitExcept> block restricts HTTP methods globally — remove or adjust it if your application uses PUT, DELETE, or PATCH for a REST API. After editing the security configuration, enable it and reload Apache:
Hardening PHP
PHP's php.ini file is located at /etc/php/{version}/fpm/php.ini — for example, /etc/php/8.4/fpm/php.ini when using PHP 8.4. The path changes with the PHP version you install. The following settings reduce the attack surface significantly and enable OPcache with JIT compilation, which can cut response times by 50% or more on CPU-bound PHP workloads.
; Do not advertise the PHP version in response headers expose_php = Off ; Never display errors to users in production display_errors = Off display_startup_errors = Off ; Log errors to a file instead ; Create this directory first: sudo mkdir -p /var/log/php && sudo chown www-data:www-data /var/log/php log_errors = On error_log = /var/log/php/error.log ; Limit upload and execution footprint post_max_size = 16M upload_max_filesize = 16M max_execution_time = 30 memory_limit = 256M ; Disable shell execution functions if your app doesn't need them disable_functions = exec,shell_exec,system,passthru,proc_open,popen ; Enable OPcache for performance opcache.enable = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 ; validate_timestamps=0 disables file-change checks for maximum performance. ; You must reload php8.4-fpm after every deploy: sudo systemctl reload php8.4-fpm ; Set to 1 in development so code changes take effect without a reload. opcache.validate_timestamps = 0 opcache.revalidate_freq = 0 ; JIT compilation (PHP 8.0+) — significant CPU-bound performance gains ; tracing mode is recommended; jit_buffer_size enables JIT (0 = disabled) opcache.jit = tracing opcache.jit_buffer_size = 64M
expose_php is On, PHP adds an X-Powered-By: PHP/8.4.x header to every response. An attacker scanning your server can read the exact PHP version in seconds, then cross-reference it against CVE databases. Version disclosure is one of the simplest fingerprinting techniques in automated reconnaissance. Setting expose_php = Off eliminates this signal. Apache's ServerTokens Prod does the same for the Apache version — both directives together prevent the HTTP response headers from revealing what software or versions you are running.
Leaving display_errors = On in production is a serious security risk. PHP error messages can expose file paths, database schema details, credentials stored in environment variables, and the presence of specific PHP extensions. Errors should go to a log file that only administrators can read, never to the HTTP response.
Hardening PostgreSQL
PostgreSQL's access control is governed by the pg_hba.conf file, which defines which hosts and users can connect to which databases using which authentication methods. The default configuration on Ubuntu allows the postgres superuser to connect over local Unix sockets without a password (using peer authentication), which is appropriate for administrative tasks. Application connections should use scram-sha-256 password authentication and should be restricted to localhost.
# TYPE DATABASE USER ADDRESS METHOD # Local Unix socket for the postgres admin user local all postgres peer # Application user connects from localhost only host appdb appuser 127.0.0.1/32 scram-sha-256 host appdb appuser ::1/128 scram-sha-256 # Reject all other connections host all all 0.0.0.0/0 reject
md5 method hashes the password with the username as a salt, producing a hash that is vulnerable to offline dictionary attacks if an attacker obtains the pg_shadow system table (e.g., via a SQL injection that reaches the superuser). With scram-sha-256, the server stores a salted, iterated hash that cannot be replayed or reversed into the original password even if stolen. PostgreSQL 18 uses scram-sha-256 as the default; verify it in postgresql.conf with password_encryption = scram-sha-256.
Also verify that PostgreSQL is not listening on a public interface. By default it binds to localhost, but check postgresql.conf:
The value should be listen_addresses = 'localhost'. If your application runs on the same server as the database, there is no reason to expose PostgreSQL on a network interface. The Linux permissions audit guide covers how to verify file and process-level permissions across services like this.
TLS with Let's Encrypt
No production web server should accept unencrypted HTTP in 2026. Install certbot and obtain a certificate from Let's Encrypt. Certbot will configure Apache to redirect port 80 to HTTPS and install the certificate automatically.
$ sudo apt install -y certbot python3-certbot-apache $ sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
Certbot on Ubuntu installs a systemd timer that runs certificate renewal checks twice daily. After setup, verify the timer is active:
For details on systemd timers and how they work as a replacement for cron, see the systemd comprehensive guide. For production Apache, configure your SSL VirtualHost to allow TLS 1.2 and 1.3 only, and disable older protocol versions explicitly in the Apache SSL configuration.
Configuring a Virtual Host
A virtual host tells Apache how to serve a specific domain. Create one for your application in /etc/apache2/sites-available/.
<VirtualHost *:80> ServerName yourdomain.com ServerAlias www.yourdomain.com # Redirect all HTTP to HTTPS (certbot adds this automatically) Redirect permanent / https://yourdomain.com/ </VirtualHost> <VirtualHost *:443> ServerName yourdomain.com DocumentRoot /var/www/yourdomain.com/public <Directory /var/www/yourdomain.com/public> Options -Indexes +FollowSymLinks AllowOverride All Require all granted </Directory> # TLS certificate paths (filled in by certbot) SSLEngine on SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem ErrorLog ${APACHE_LOG_DIR}/yourdomain_error.log CustomLog ${APACHE_LOG_DIR}/yourdomain_access.log combined </VirtualHost>
$ sudo a2ensite yourdomain.com.conf $ sudo a2dissite 000-default.conf $ sudo apachectl -t && sudo systemctl reload apache2
What PostgreSQL 18 Brings to LAPP
PostgreSQL 18 was released on September 25, 2025, and it is worth discussing specifically because its changes are relevant to anyone choosing PostgreSQL as a backend in 2026.
The headline feature is a new asynchronous I/O subsystem. According to the PostgreSQL Global Development Group's official press release, this subsystem demonstrated up to 3x performance improvements when reading from storage, benefiting sequential scans, bitmap heap scans, vacuums, and bulk operations. The improvement is especially visible on NVMe storage and cloud block devices that perform well with concurrent I/O requests.
PostgreSQL 18 also made virtual generated columns the default behavior for generated columns. These compute their values at query time rather than being stored on disk, so they carry no storage cost for the derived data. Earlier PostgreSQL versions only supported stored generated columns; virtual is now the default unless you specify STORED explicitly. The uuidv7() function generates timestamp-ordered UUIDs that sort predictably in B-tree indexes, providing better index locality than random UUID generation (UUIDv4). For tables where uuid is the primary key — a common pattern in distributed systems — this can meaningfully reduce index fragmentation and improve insertion performance.
PostgreSQL 18 also added OAuth 2.0 authentication support for integration with single sign-on systems, temporal constraints for PRIMARY KEY, UNIQUE, and FOREIGN KEY (allowing constraints to apply over a range of time), and improved handling of RETURNING clauses in INSERT, UPDATE, DELETE, and MERGE statements via OLD and NEW row references. The RETURNING clause improvement is particularly useful for audit logging — you can capture before and after values in a single statement rather than issuing two queries. These are features that put PostgreSQL comfortably ahead of MySQL for complex application requirements.
For teams building AI-adjacent applications on the LAPP stack, the PostgreSQL 18 AIO subsystem has direct consequences for pgvector workloads. The pgvector extension, which adds vector similarity search to PostgreSQL, became widely adopted in 2024 and 2025 for storing and querying high-dimensional embeddings alongside relational data — a common pattern in retrieval-augmented generation (RAG) pipelines and semantic search applications. Approximate nearest-neighbor queries over large vector tables involve substantial sequential and indexed reads; the new I/O subsystem reduces the latency of those operations. Running pgvector on PostgreSQL 18 inside a LAPP stack means your AI-adjacent application features and your relational application data sit in the same database, under the same backup and access-control policies, without a separate vector database infrastructure layer.
The PostgreSQL Global Development Group's release announcement states that the new I/O subsystem delivered storage-read performance gains of up to 3x across workloads of all sizes. — PostgreSQL Global Development Group, September 25, 2025
One operational improvement in PostgreSQL 18 that is often overlooked but matters significantly for production teams is planner statistics preservation across major upgrades. Before PostgreSQL 18, running pg_upgrade to move from one major version to another discarded optimizer statistics, meaning the database had to rebuild them from scratch after the upgrade completed. On busy systems, this could cause serious query plan degradation until the ANALYZE operations finished. PostgreSQL 18 carries statistics through a major version upgrade, so an upgraded cluster reaches its expected performance immediately rather than degrading during a potentially lengthy post-upgrade window.
For teams running LAPP in production and planning a PostgreSQL version upgrade, this is one of the strongest operational arguments for moving to PostgreSQL 18 sooner rather than later. It turns a disruptive maintenance event into a near-seamless transition.
Setting Up the LAPP Stack: Step-by-Step Summary
Step 1: Install Apache, PHP-FPM, and PostgreSQL
On Ubuntu 24.04, add the Ondrej Sury PPA (ppa:ondrej/php) and run sudo apt update, then install apache2, php8.4-fpm, php8.4-pgsql, and postgresql-18 (from the PGDG repository). Enable the MPM Event module and disable the prefork module, then enable the proxy and proxy_fcgi modules for PHP-FPM integration. Start and enable all three services with systemctl enable --now.
Step 2: Create and Configure the PostgreSQL Database
Switch to the postgres system user and open psql. Create a dedicated database user with a strong password using CREATE USER appuser WITH PASSWORD '...', create the application database with CREATE DATABASE appdb OWNER appuser, and grant the user all privileges on that database. Never use the postgres superuser account for application connections. Verify that pg_hba.conf restricts the application user to localhost using scram-sha-256 authentication.
Step 3: Harden Apache and PHP for Production
Set ServerTokens Prod and ServerSignature Off in the Apache security configuration to suppress version disclosure. In /etc/php/8.4/fpm/php.ini, set expose_php = Off, display_errors = Off, and log_errors = On. Disable shell execution functions such as exec and shell_exec if your application does not need them. Configure TLS with a certificate from Let's Encrypt using certbot --apache. Enable opcache for production PHP performance.
Frequently Asked Questions
What is the difference between LAPP and LAMP?
Both stacks use Linux, Apache, and PHP. The only difference is the database: LAMP uses MySQL (or MariaDB), while LAPP uses PostgreSQL. PostgreSQL is fully ACID-compliant in all configurations, supports a richer set of data types and index types, and enforces SQL standards more strictly. LAMP is often the default for CMS platforms like WordPress, while LAPP is preferred for applications that demand strong data integrity, complex queries, or advanced features like PostGIS or pgvector.
Is the LAPP stack good for production use in 2026?
Yes. The LAPP stack is a mature, production-ready platform. PostgreSQL 18, released on September 25, 2025, introduced a new asynchronous I/O subsystem that has demonstrated up to 3x performance improvements when reading from storage, along with virtual generated columns and the uuidv7() function for better indexing. Apache 2.4 with the MPM Event module and PHP-FPM is the recommended production configuration. PHP 8.4 is the actively maintained stable choice for new deployments as of 2026; PHP 8.5 (released November 2025) is also available via the Ondrej Sury PPA. Combined with TLS 1.3 and a properly hardened php.ini, LAPP competes directly with any open-source web stack.
Does PHP work well with PostgreSQL?
Yes. PHP has native PostgreSQL support through the pgsql extension and also supports PDO (PHP Data Objects) with a PostgreSQL driver. PDO is the recommended approach because it provides a consistent interface and prepared statements that protect against SQL injection. Major PHP frameworks including Laravel, Symfony, and CodeIgniter all support PostgreSQL as a first-class database backend.
What PHP version should I use with the LAPP stack in 2026?
PHP 8.4 is the actively maintained stable choice for new LAPP deployments in 2026, with bug-fix and security support through December 2026. PHP 8.5 (released November 20, 2025) is also available and is the current stable branch. PHP 8.3 entered security-fix-only mode in December 2025. Ubuntu 24.04 ships PHP 8.3 by default; PHP 8.4 and 8.5 are available via the Ondrej Sury PPA (ppa:ondrej/php). Install php8.4-fpm and php8.4-pgsql to connect PHP to PostgreSQL via PHP-FPM.
Should I use PDO or the pgsql extension to connect PHP to PostgreSQL?
For most application code, PDO with the pdo_pgsql driver is the correct choice. It provides a consistent interface, enforces prepared statement discipline, and is expected by frameworks like Laravel and Symfony. The native pgsql extension should be used when you need PostgreSQL-specific features that PDO does not expose: pg_copy_from() and pg_copy_to() for bulk COPY operations, pg_lo_create() for large object handling, and pg_get_notify() for reading messages from PostgreSQL LISTEN/NOTIFY channels. Both are installed by the php8.4-pgsql package.
Sources and Further Reading
- PostgreSQL Global Development Group — PostgreSQL 18 Released (September 25, 2025)
- PostgreSQL 18 Release Notes — official feature list and migration notes
- PostgreSQL 18 Press Kit — I/O subsystem performance benchmarks
- PostgreSQL Project — About PostgreSQL (ACID compliance, MVCC, history)
- PostgreSQL Global Development Group — PGDG apt repository setup for Ubuntu
- PHP Project — Official PHP releases and version lifecycle
- Apache Software Foundation — MPM Event module documentation
- PHP Manual — pgsql extension reference
- PHP Manual — PDO_PGSQL driver reference
- Electronic Frontier Foundation — Certbot for Apache (Let's Encrypt)
sudo apt update && sudo apt upgrade -y to update all existing packagesapache2 with systemctl enable --now apache2mpm_prefork, enable mpm_event, enable proxy and proxy_fcgi modulessudo add-apt-repository ppa:ondrej/phpphp8.4-fpm php8.4-pgsql php8.4-common php8.4-cli php8.4-opcache\.php$ to PHP-FPM via Unix socket and reload Apachepostgresql-18 and enable the servicepostgres superuser for app connectionsGRANT ALL ON SCHEMA public TO appuser (required in PG 15+)pdo_pgsql, set ATTR_EMULATE_PREPARES => false, load credentials from env varsServerTokens Prod and ServerSignature Off in Apache security configexpose_php = Off, display_errors = Off, log_errors = On in php.inipg_hba.conf: application user connects to localhost only with scram-sha-256listen_addresses = 'localhost' in postgresql.confphp.ini and reload PHP-FPMcertbot.timer is active