How to install WordPress with Nginx, PHP-FPM and MariaDB (LEMP)
Install WordPress on Ubuntu or Debian with Nginx, PHP-FPM and MariaDB: verified download, official Nginx rules, Let’s Encrypt TLS, WP-CLI, backups and updates.
- Intermediate
- 45 min read
- Updated
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13
This guide is not available in your language yet, so it is shown in English.
On this page
- Prerequisites
- Step 1 — Install Nginx, MariaDB and PHP-FPM
- Step 2 — Create the database and user
- Step 3 — Download WordPress and verify the checksum
- Step 4 — Configure wp-config.php
- Step 5 — Set ownership and permissions
- Step 6 — Configure Nginx and PHP
- Step 7 — Enable HTTPS with Certbot
- Step 8 — Finish the installation in the browser
- Step 9 — Install WP-CLI
- Step 10 — Apply hardening basics
- Back up and restore
- Update WordPress
- Troubleshooting
- 502 Bad Gateway
- 413 Request Entity Too Large or upload size exceeded
- Pretty permalinks return 404
- Error establishing a database connection
- WordPress asks for FTP credentials when installing plugins
- Next steps
WordPress is an open-source content management system for blogs, company sites and online shops, extended by a large catalogue of themes and plugins. A LEMP stack (Linux, Nginx, MariaDB, PHP-FPM) is a lean and fast way to host it on your own server, with full control over PHP settings, caching and security.
This guide builds the stack from your distribution's own packages on Ubuntu 24.04, Ubuntu 26.04, Debian 12 or Debian 13. You create a database, download WordPress from wordpress.org and verify the checksum it publishes, generate secret keys with the official API, apply the Nginx rules from WordPress's documentation, add a free Let's Encrypt certificate with Certbot and install a verified WP-CLI. The guide ends with hardening basics, backups, updates and fixes for common errors.
Prerequisites
- A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13, without another web server listening on ports 80 and 443.
- A non-root user with
sudorights and SSH key login, prepared as in Secure a new Linux server and Set up SSH keys. - A domain such as
example.comwith A (and AAAA) records forexample.comandwww.example.compointing at the server, so that Certbot can validate it.
WordPress recommends PHP 8.3 or newer and MariaDB 10.11 or newer (or MySQL 8.0+), and still runs on PHP 7.4 and MySQL 5.5.5 as end-of-life legacy minimums. The distribution packages used in this guide give you these versions, all of which current WordPress releases support:
| Release | PHP | MariaDB |
|---|---|---|
| Ubuntu 24.04 LTS | 8.3 | 10.11 |
| Ubuntu 26.04 LTS | 8.5 | 11.8 |
| Debian 12 | 8.2 | 10.11 |
| Debian 13 | 8.4 | 11.8 |
Debian 12's PHP 8.2 works but is below WordPress's recommendation; for a new server, prefer Debian 13 or one of the Ubuntu releases. WordPress does not publish CPU or memory minimums. The WordPress hosting handbook recommends a PHP memory_limit of at least 128 MB and 256 MB as a default, so treat the figures below as a conservative starting point for one site.
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 1 vCPU |
| RAM | Not published (PHP memory_limit 128 MB or more) | 2 GB |
| Disk | Not published | 20 GB SSD plus your media library |
Step 1 — Install Nginx, MariaDB and PHP-FPM
Install the web server, the database server, PHP-FPM and the PHP extensions that the WordPress hosting handbook lists as required or highly recommended. Extensions such as json, hash, exif, fileinfo and openssl are already part of the base PHP packages:
sudo apt update
sudo apt install nginx mariadb-server php-fpm php-mysql php-curl php-xml php-mbstring php-intl php-zip php-gd php-imagickCheck the versions and the PHP-FPM socket:
php -v
mariadb --version
ls -l /run/php//run/php/ should contain a version-specific socket such as php8.3-fpm.sock and a php-fpm.sock link to it. The Nginx configuration in Step 6 uses that link, so it works on every release in this guide.
Step 2 — Create the database and user
Run the MariaDB hardening script first. On Ubuntu and Debian the MariaDB root account already logs in through the Unix socket, so press Enter when asked for the current root password, keep socket authentication, and answer Y to removing anonymous users, disallowing remote root login, removing the test database and reloading the privilege tables:
sudo mariadb-secure-installationGenerate a strong password for the WordPress database user and note it:
openssl rand -hex 24Open the MariaDB shell with sudo mariadb and create the database and a user that can only access it. Replace change-me with the generated password:
CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'change-me';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;Test the login as the new user. You should see a single row with the value 1:
mariadb -u wpuser -p wordpress -e "SELECT 1;"Step 3 — Download WordPress and verify the checksum
Download the current release from wordpress.org together with the SHA-1 checksum that wordpress.org publishes for it, and compare the two:
cd /tmp
curl -fsSLO https://wordpress.org/latest.tar.gz
curl -fsSL https://wordpress.org/latest.tar.gz.sha1 -o latest.tar.gz.sha1
echo "$(cat latest.tar.gz.sha1) latest.tar.gz" | sha1sum -c -The output must be latest.tar.gz: OK. If it reports a mismatch, delete both files and download them again; a new release may have been published between the two downloads. Then unpack WordPress into /var/www/wordpress:
sudo tar -xzf /tmp/latest.tar.gz -C /var/www/
ls /var/www/wordpressStep 4 — Configure wp-config.php
Create wp-config.php from the sample file and fill in the database settings. Replace change-me with the password from Step 2; the hexadecimal password contains no characters that would break the sed command:
cd /var/www/wordpress
sudo cp wp-config-sample.php wp-config.php
sudo sed -i "s/database_name_here/wordpress/; s/username_here/wpuser/; s/password_here/change-me/" wp-config.phpWordPress signs cookies and sessions with eight secret keys and salts. Fetch a fresh set from the official generator:
curl -fsSL https://api.wordpress.org/secret-key/1.1/salt/Open the file with sudo nano wp-config.php, delete the eight define lines that contain put your unique phrase here and paste the eight lines from the generator in their place. Every request to the generator returns new random values, so never reuse keys from another site. Save the file and check the syntax:
php -l /var/www/wordpress/wp-config.phpYou should see No syntax errors detected.
Step 5 — Set ownership and permissions
WordPress's hardening guide recommends 755 for directories and 644 for files, and making wp-config.php readable only by you and the web server. This guide gives the files to the www-data user that PHP-FPM runs as, so that WordPress can install plugins and apply automatic updates on its own:
sudo chown -R www-data:www-data /var/www/wordpress
sudo find /var/www/wordpress -type d -exec chmod 755 {} \;
sudo find /var/www/wordpress -type f -exec chmod 644 {} \;
sudo chmod 440 /var/www/wordpress/wp-config.phpStep 6 — Configure Nginx and PHP
Create the server block with sudo nano /etc/nginx/sites-available/wordpress. It is based on the single-site configuration and the global restrictions in WordPress's Nginx documentation, adapted to the Debian and Ubuntu package layout. The deny rules come before the PHP location so that PHP files inside uploads can never run, and hidden files except .well-known are blocked:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/wordpress;
index index.php;
client_max_body_size 64M;
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location ~ /\.(?!well-known) {
deny all;
}
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_intercept_errors on;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp)$ {
expires max;
log_not_found off;
}
}Raise PHP's upload and memory limits to match client_max_body_size. The drop-in file goes into the PHP-FPM configuration directory of your PHP version, which the first command detects:
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
sudo tee /etc/php/$PHPV/fpm/conf.d/99-wordpress.ini > /dev/null <<'EOF'
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 120
EOF
sudo systemctl restart php$PHPV-fpmEnable the site, disable the default page, test the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
curl -I http://example.comnginx -t must report syntax is ok and test is successful. The curl request returns a redirect to /wp-admin/install.php, which shows that PHP and the database connection work.
Step 7 — Enable HTTPS with Certbot
Open the firewall for SSH and web traffic, then let Certbot's Nginx plugin obtain a Let's Encrypt certificate and add the HTTPS configuration and the HTTP-to-HTTPS redirect to your server block. Nginx reverse proxy with Certbot explains Certbot and its options in more detail:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comThis uses the Certbot packages from your distribution; the Certbot team recommends its snap instead, as the Nginx guide shows. Either works, but install only one of them. Certbot asks for an email address for expiry notices and for agreement to the Let's Encrypt terms. Its packages install a systemd timer that renews certificates automatically; test renewal once:
sudo certbot renew --dry-runStep 8 — Finish the installation in the browser
Open https://example.com. WordPress asks for the language, then for the site title, an administrator username, a password and an email address. Do not use admin or another obvious username, keep the strong generated password or use your password manager, and only tick "Discourage search engines" for a staging site.
After logging in at https://example.com/wp-admin, open Settings → Permalinks, choose Post name and save. Open any post: if it loads under its pretty URL, the try_files rule works.
WordPress emails password resets, new-user details and comment notifications through PHP's mail function, which needs a local mail server that this stack does not include, so install an SMTP plugin and connect it to your relay with host smtp.example.com, port 587 and STARTTLS.
Step 9 — Install WP-CLI
WP-CLI manages WordPress from the shell: updates, plugins, users, search-and-replace and database exports. Download the official Phar build with its SHA-512 checksum and GPG signature, verify both, then install it:
cd /tmp
curl -fsSLO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
curl -fsSLO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.sha512
curl -fsSLO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.asc
echo "$(cat wp-cli.phar.sha512) wp-cli.phar" | sha512sum -c -
curl -fsSL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/wp-cli.pgp | gpg --import
gpg --verify wp-cli.phar.asc wp-cli.pharsha512sum must print wp-cli.phar: OK, and gpg must report a good signature from the WP-CLI release key with the fingerprint 63AF 7AA1 5067 C056 16FD DD88 A3A2 E8F2 26F0 BC06, as published in the WP-CLI handbook. A warning that the key is not certified with a trusted signature is normal for a freshly imported key. Then install the Phar and run it as www-data, the owner of the files:
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
sudo -u www-data wp --path=/var/www/wordpress core versionThe last command prints the installed WordPress version.
Step 10 — Apply hardening basics
Add two constants to wp-config.php with sudo nano /var/www/wordpress/wp-config.php, above the line that says to stop editing. The first removes the built-in plugin and theme file editor, which WordPress's hardening guide calls often the first tool an attacker uses after logging in; the second forces logins and the admin area over HTTPS:
define( 'DISALLOW_FILE_EDIT', true );
define( 'FORCE_SSL_ADMIN', true );Then check that PHP files in the uploads folder really cannot run, as WordPress's Nginx documentation recommends. The request must return 403 Forbidden:
sudo -u www-data mkdir -p /var/www/wordpress/wp-content/uploads
echo '<?php echo "test";' | sudo -u www-data tee /var/www/wordpress/wp-content/uploads/test.php > /dev/null
curl -I https://example.com/wp-content/uploads/test.php
sudo rm /var/www/wordpress/wp-content/uploads/test.phpFurther basics from the hardening guide: install plugins and themes only from wordpress.org or other trusted sources and delete the ones you do not use, enable two-factor authentication for administrators with a plugin, and keep the server itself patched, for example with Ubuntu's or Debian's unattended-upgrades.
Back up and restore
A WordPress site consists of the database, the wp-content folder (themes, plugins, uploads), wp-config.php and your Nginx server block. The core files can always be downloaded again. Create the backups with MariaDB's own dump tool and tar:
sudo mkdir -p /opt/backups
sudo sh -c 'mariadb-dump --single-transaction --default-character-set=utf8mb4 wordpress | gzip > /opt/backups/wordpress-db-$(date +%F).sql.gz'
sudo tar czf /opt/backups/wordpress-files-$(date +%F).tar.gz -C /var/www/wordpress wp-content wp-config.php
sudo cp /etc/nginx/sites-available/wordpress /opt/backups/nginx-wordpress-$(date +%F).conf
sudo chmod 600 /opt/backups/wordpress-* /opt/backups/nginx-wordpress-*--single-transaction takes a consistent snapshot of the InnoDB tables without locking the site. Copy /opt/backups to another machine or object storage regularly; a backup on the same server is lost together with it.
To restore on a new server, repeat Steps 1 to 3 and 6 to 7. In Step 2, create the database user with the same password that DB_PASSWORD in the backed-up wp-config.php contains. Then put the files back and import the database:
sudo tar xzf /opt/backups/wordpress-files-2026-10-09.tar.gz -C /var/www/wordpress
gunzip -c /opt/backups/wordpress-db-2026-10-09.sql.gz | sudo mariadb wordpress
sudo chown -R www-data:www-data /var/www/wordpress
sudo chmod 440 /var/www/wordpress/wp-config.phpReplace the dates with those in your file names. If the site moves to a new domain, run sudo -u www-data wp --path=/var/www/wordpress search-replace 'https://old.example.com' 'https://example.com' after the import.
Update WordPress
By default WordPress installs minor and security releases automatically in the background; Dashboard → Updates shows whether automatic updates for major versions are enabled too, and plugins and themes have their own auto-update switches. Read the release notes before a major update, back up first and update manually with WP-CLI if you prefer to control the timing:
sudo -u www-data wp --path=/var/www/wordpress core check-update
sudo -u www-data wp --path=/var/www/wordpress core update
sudo -u www-data wp --path=/var/www/wordpress core update-db
sudo -u www-data wp --path=/var/www/wordpress plugin update --all
sudo -u www-data wp --path=/var/www/wordpress theme update --allNginx, MariaDB and PHP come from your distribution and are updated with sudo apt update && sudo apt upgrade. WP-CLI updates itself with sudo wp cli update. When you upgrade the operating system to a new release with a different PHP version, check that your plugins support it and repeat the PHP drop-in file from Step 6 for the new version.
Troubleshooting
502 Bad Gateway
Nginx cannot reach PHP-FPM. Check that the service runs with systemctl status 'php*-fpm' --no-pager and that /run/php/php-fpm.sock exists. If your system has no php-fpm.sock link, set fastcgi_pass to the version-specific socket, for example unix:/run/php/php8.3-fpm.sock, then run sudo nginx -t and reload Nginx. The Nginx error log at /var/log/nginx/error.log names the exact cause.
413 Request Entity Too Large or upload size exceeded
Nginx rejects requests larger than client_max_body_size, and PHP rejects files larger than upload_max_filesize or post_max_size. Raise all three to the same value, restart PHP-FPM and reload Nginx. Media → Add New shows the effective maximum upload size.
Pretty permalinks return 404
The location / block is missing its try_files $uri $uri/ /index.php?$args; line, or another server block answers for the domain. Run sudo nginx -T | grep -n server_name to see all server blocks, fix the configuration, test it with sudo nginx -t and reload.
Error establishing a database connection
The values in wp-config.php do not match the database user, or MariaDB is not running. Test the credentials with mariadb -u wpuser -p wordpress -e "SELECT 1;", check DB_HOST (it must be localhost) and run systemctl status mariadb --no-pager.
WordPress asks for FTP credentials when installing plugins
PHP-FPM cannot write to the WordPress folder, usually after files were copied in as root. Restore the ownership with sudo chown -R www-data:www-data /var/www/wordpress, or, if you chose the stricter ownership model, install plugins with WP-CLI instead.
Next steps
- Learn more about Certbot and Nginx in Nginx reverse proxy with Certbot.
- Prefer containers? See WordPress with Docker.
- Looking for a publishing platform with built-in newsletters? Try Ghost.
- Compare plans for WordPress sites on the WordPress hosting page.
- Read WordPress's advanced administration handbook for performance and security settings.
Frequently asked questions
Which PHP version does each release install, and does WordPress support it?
Ubuntu 24.04 ships PHP 8.3, Ubuntu 26.04 PHP 8.5, Debian 12 PHP 8.2 and Debian 13 PHP 8.4. Current WordPress releases support all four. WordPress recommends PHP 8.3 or newer, so Debian 12 works but sits below the recommendation.
Do I need an .htaccess file with Nginx?
No. Nginx ignores .htaccess files. Pretty permalinks work through the try_files line in the server block, which sends requests for missing files to index.php, and the deny rules in the same block replace the usual .htaccess protections.
Should the web server own the WordPress files?
It is a trade-off. Giving www-data ownership lets WordPress install plugins and apply automatic updates itself. WordPress's hardening guide prefers core files owned by your own user; then you update through WP-CLI or SSH instead of the dashboard.
How do I back up WordPress on a LEMP server?
Dump the database with mariadb-dump, archive wp-content and wp-config.php, and keep a copy of the Nginx server block. Store the archives off the server. The WordPress core files can always be downloaded again.
Can I run this on a HyperDC server?
Yes. The steps work on a HyperDC Linux VPS, VDS or dedicated server with root access running Ubuntu 24.04, Ubuntu 26.04, Debian 12 or Debian 13. Size the server for your traffic, plugins and media library.
Sources
- wordpress.org/about/requirements
- wordpress.org/download
- wordpress.org/download/releases
- make.wordpress.org/core/handbook/references/php-compatibility-and-w…
- make.wordpress.org/hosting/handbook/server-environment
- developer.wordpress.org/advanced-administration/before-install/howt…
- developer.wordpress.org/advanced-administration/server/web-server/n…
- developer.wordpress.org/advanced-administration/wordpress/wp-config
- developer.wordpress.org/advanced-administration/security/hardening
- developer.wordpress.org/advanced-administration/security/https
- api.wordpress.org/secret-key/1.1/salt
- make.wordpress.org/cli/handbook/guides/installing