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

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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 `sudo` rights and SSH key login, prepared as in [Secure a new Linux server](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- A domain such as `example.com` with A (and AAAA) records for `example.com` and `www.example.com` pointing 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:

```bash
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-imagick
```

Check the versions and the PHP-FPM socket:

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

```bash
sudo mariadb-secure-installation
```

Generate a strong password for the WordPress database user and note it:

```bash
openssl rand -hex 24
```

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

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

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

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

```bash
sudo tar -xzf /tmp/latest.tar.gz -C /var/www/
ls /var/www/wordpress
```

## Step 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:

```bash
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.php
```

WordPress signs cookies and sessions with eight secret keys and salts. Fetch a fresh set from the official generator:

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

```bash
php -l /var/www/wordpress/wp-config.php
```

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

```bash
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.php
```

> **Note**
>
> The hardening guide prefers core files that only your own account can write. That is stricter, because a compromised plugin cannot modify WordPress core, but the dashboard can then no longer update WordPress, and you update with WP-CLI as shown in the update section. Pick one model and apply it consistently.

## Step 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:

```nginx
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:

```bash
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-fpm
```

Enable the site, disable the default page, test the configuration and reload Nginx:

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

`nginx -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](/guides/nginx-reverse-proxy-certbot) explains Certbot and its options in more detail:

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

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

```bash
sudo certbot renew --dry-run
```

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

> **Note**
>
> Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

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

```bash
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.phar
```

`sha512sum` 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:

```bash
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 version
```

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

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

```bash
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.php
```

Further 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`:

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

```bash
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.php
```

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

```bash
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 --all
```

Nginx, 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](/guides/nginx-reverse-proxy-certbot).
- Prefer containers? See [WordPress with Docker](/guides/install-wordpress-docker).
- Looking for a publishing platform with built-in newsletters? Try [Ghost](/guides/install-ghost).
- Compare plans for WordPress sites on the [WordPress hosting](/wordpress-web-hosting) page.
- Read WordPress's [advanced administration handbook](https://developer.wordpress.org/advanced-administration/) 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.

---

Source: <https://hyperdc.com/guides/tutorials/install-wordpress-lemp>\
Updated: 2026-10-09
