How to Install WordPress on Nginx on Debian 12

Install WordPress on Nginx for Debian 12 – DevAnswers.net guide

This guide covers Debian 12. Read the Debian 13 edition.

Installing WordPress on Nginx is a popular self-hosted setup: a few packages, a database, a clean server block, PHP-FPM for executing PHP, and a fresh wp-config.php. This DevAnswers guide walks through a complete, secure install on Debian 12 (Bookworm) with the distro’s stock Nginx 1.22 and MariaDB 10.11 plus PHP 8.2 from the official repositories.

The walkthrough installs Nginx, MariaDB, and PHP-FPM from scratch, then covers the PHP extensions WordPress needs, a dedicated database and user, the file-permission scheme PHP-FPM expects, a clean nginx server block with try_files for pretty permalinks, a Let’s Encrypt certificate obtained before the installer runs, and a hardened wp-config.php.

Prerequisites 

You need a server running Debian 12 (Bookworm) and a domain name. Everything else is installed below.

  • A non-root user with sudo privileges (see our initial server setup guide if you haven’t created one).
  • A domain name with A records for both the apex and the www host, pointed at your server’s public IP. Certbot requests a certificate covering both names later and fails if either does not resolve. Examples below use devanswers.test; substitute your real domain.
Already have Nginx, MariaDB, and PHP-FPM running? Skip straight to Install Required PHP Extensions.

Install the LEMP Stack 

WordPress needs three things before it can run: a web server, a database, and PHP. This section installs all three on a clean Debian 12 (Bookworm) server, along with curl and wget, which later steps use to fetch WordPress and its salt keys. For the reasoning behind each component, our complete LEMP stack guide for Debian 12 (Bookworm) covers the same ground in depth.

Terminal
sudo apt update
sudo apt install -y nginx curl wget

Open the firewall so the site is reachable. The nginx package registers an Nginx Full profile covering ports 80 and 443:

Debian does not ship ufw, so install it first. Allow SSH in the same breath: if you enable the firewall later without that rule, you lock yourself out of the server.

Terminal
sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'

If you followed our initial server setup guide, ufw is active with only SSH allowed, and this command is what lets visitors reach your site. If the firewall is not enabled, the rule is stored and applies whenever you turn it on.

If Nginx fails to start with an address-already-in-use error, another web server holds port 80. Some images ship with Apache installed:

Terminal
sudo systemctl disable --now apache2
sudo systemctl start nginx

Install the database server, then run the hardening script:

Terminal
sudo apt install -y mariadb-server
sudo mariadb-secure-installation

Keep root on unix_socket authentication rather than setting a root password, then accept the prompts to remove anonymous users, disallow remote root login, drop the test database, and reload privileges. Leaving root on unix_socket is deliberate: it is why the sudo mysql command in the Create a Database step works without a password.

Install the PHP-FPM runtime that Nginx hands .php requests to:

Terminal
sudo apt install -y php8.2-fpm
systemctl is-active php8.2-fpm
Output
active

You now have Nginx 1.22, MariaDB 10.11, and PHP 8.2 running. The next section adds the extensions WordPress specifically needs.

Install Required PHP Extensions 

WordPress requires a small set of PHP extensions beyond the core. mysqli is mandatory (database connectivity); gd handles image resizing for uploaded media (with imagick as an optional higher-quality alternative); curl, mbstring, xml, and zip are used by the updater, the REST API, and plugin/theme installers. Without these, the dashboard will display warnings on the Site Health screen.

Terminal
sudo apt install -y php8.2-mysql php8.2-curl php8.2-gd php8.2-mbstring php8.2-xml php8.2-zip

Then install imagick separately. It produces higher-quality thumbnails than gd and is the WordPress-community-preferred image backend. It lives in Ubuntu’s universe component (and the equivalent on Debian); on the rare minimal image where universe is disabled, the next command fails harmlessly and WordPress falls back to gd:

Terminal
sudo apt install -y php8.2-imagick || echo "imagick not available; WordPress will use gd as fallback"

The php8.2-mysql meta-package pulls in mysqli and the modern mysqlnd driver. After installation, reload PHP-FPM so the new modules are picked up:

Terminal
sudo systemctl reload php8.2-fpm

Confirm all required extensions are loaded against the FPM SAPI (not the CLI), since that’s the one nginx will use:

Terminal
php-fpm8.2 -m | grep -E '^(mysqli|curl|gd|mbstring|xml|zip|imagick)$'
Output
curl
gd
imagick
mbstring
mysqli
xml
zip

You should see all 7 names if imagick installed, or 6 (everything except imagick) if it fell back. If any of the required ones (everything except imagick) are missing, install the matching php8.2-<name> package individually and reload PHP-FPM.

Create a Database for WordPress 

WordPress stores all site content (posts, settings, users) in a single MySQL/MariaDB database. Create a dedicated database and user with privileges scoped to just that database. Never let WordPress use the root account.

Log into the database shell as root:

Terminal
sudo mysql

Now create the database and user. Use utf8mb4 with utf8mb4_unicode_ci collation; this is the standard for modern WordPress (4-byte UTF-8 supports emoji and the full Unicode range). Substitute change-this-strong-password with a strong generated password before running:

MySQL
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'change-this-strong-password';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Verify the user can connect to the new database:

Terminal
mysql -u wpuser -p wordpress -e 'SHOW DATABASES;'

Enter the password when prompted. You should see wordpress listed alongside information_schema.

Download WordPress 

Fetch the latest stable release from wordpress.org. The latest.tar.gz URL always serves the current version, so this command keeps working without edits as new releases ship.

Terminal
cd /tmp
wget https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz

This extracts a wordpress/ directory inside /tmp. Move its contents into nginx’s web root, then remove the empty extracted folder and the downloaded archive:

Terminal
sudo rm -f /var/www/html/index.nginx-debian.html /var/www/html/index.html
sudo cp -a /tmp/wordpress/. /var/www/html/
rm -rf /tmp/wordpress /tmp/latest.tar.gz
The default nginx placeholder index.nginx-debian.html is what shows the “Welcome to nginx” page after a fresh install. Removing it (along with any index.html) ensures the server block’s index index.php directive lands on WordPress’s bootstrap script.

Set File Permissions 

WordPress runs as the PHP-FPM user (www-data on Ubuntu and Debian, which is also nginx’s user) and needs write access to upload media, install plugins, and update itself. The canonical permission scheme is 755 for directories (read+execute for everyone, write for owner), 644 for files (read for everyone, write for owner), and 640 for wp-config.php (which contains your database password, so other users on the system don’t need to read it).

Terminal
sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;

You’ll tighten wp-config.php to 640 in a later step, after it’s been created.

Configure Nginx for WordPress 

WordPress’s pretty permalinks (/2026/05/post-title/ rather than /?p=123) require a single nginx directive: try_files $uri $uri/ /index.php?$args;. This tells nginx to serve a static file if one exists at the requested path, fall back to a directory index, and otherwise pass the request to WordPress’s index.php for routing. Unlike Apache, there’s no .htaccess involved; the rule lives directly in the server block.

Create a server block for your WordPress site. Replace devanswers.test with your real domain:

Terminal
sudo nano /etc/nginx/sites-available/wordpress

Paste the following:

/etc/nginx/sites-available/wordpress
server {
    listen 80;
    listen [::]:80;
    server_name devanswers.test www.devanswers.test;
    root /var/www/html;
    index index.php index.html;

    access_log /var/log/nginx/wordpress-access.log;
    error_log /var/log/nginx/wordpress-error.log;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Allow ACME (Let's Encrypt) HTTP-01 challenges even though dotfiles are denied below
    location ^~ /.well-known/acme-challenge/ {
        allow all;
        root /var/www/html;
    }

    # Block PHP execution inside the uploads directory (defense against malicious uploads)
    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }

    # Deny access to dotfiles, including .htaccess, .git, etc.
    location ~ /\. {
        deny all;
    }

    # Deny direct PHP access to wp-config.php
    location = /wp-config.php {
        deny all;
    }

    # Block xmlrpc.php (brute-force and pingback amplification target)
    location = /xmlrpc.php {
        deny all;
    }
}

Save and exit (press CTRL + X, press Y, then press ENTER)

The try_files line is the load-bearing one for pretty permalinks. The location ~ \.php$ block sends .php requests to PHP-FPM over its Unix socket; the fastcgi-php.conf snippet (shipped with the nginx package) sets SCRIPT_FILENAME and the other FastCGI variables WordPress expects. One caveat on the xmlrpc.php deny at the bottom: it shuts down a heavily brute-forced endpoint, but Jetpack and the official WordPress mobile apps both use it, so drop that block if you rely on either.

Disable the default site (which still owns /var/www/html) and enable yours:

Terminal
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/wordpress
sudo nginx -t

nginx -t should report syntax is ok and test is successful. If you see “server_names_hash_bucket_size”, add server_names_hash_bucket_size 64; at the top of /etc/nginx/nginx.conf in the http block. Otherwise reload:

Terminal
sudo systemctl reload nginx

Set Up wp-config.php 

WordPress ships with wp-config-sample.php; you copy it to wp-config.php and fill in the database credentials plus a set of cryptographic salt keys.

Terminal
sudo -u www-data cp /var/www/html/wp-config-sample.php /var/www/html/wp-config.php

WordPress provides a free API that generates fresh salt keys on every request. Fetch them directly:

Terminal
curl -s https://api.wordpress.org/secret-key/1.1/salt/

You’ll get eight define() statements with random keys. Copy that block, then open wp-config.php:

Terminal
sudo nano /var/www/html/wp-config.php

Make three edits:

  1. Database connection: set DB_NAME to wordpress, DB_USER to wpuser, and DB_PASSWORD to the password you used when creating the database. Leave DB_HOST as localhost.
  2. Salt keys: find the block of eight define('AUTH_KEY', ...) lines near the middle of the file (they say 'put your unique phrase here') and replace the entire block with the output from the salt API.
  3. Table prefix (optional but recommended): change $table_prefix = 'wp_'; to something less guessable like $table_prefix = 'da_';. This is a small obscurity measure that only deflects low-effort bots hardcoded to the default prefix; it does not defend against real SQL injection, since an injection able to reach the database can read the prefix from information_schema. Treat it as tidy-up, not a security control.

Save and exit (Ctrl+X, then Y, then Enter). Now tighten the permissions on this file, which holds your database password:

Terminal
sudo chmod 640 /var/www/html/wp-config.php

Enable HTTPS Before You Install 

Run Certbot before the WordPress installer, not after. WordPress records the site URL you install under, so obtaining the certificate first means it records https:// and you avoid chasing mixed-content warnings later.

Terminal
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx --redirect -d devanswers.test -d www.devanswers.test

Certbot edits the server block you created above and adds the port 443 listener. The --redirect flag sends all HTTP traffic to HTTPS without waiting on a prompt. Both names must resolve to this server, or the certificate request fails.

Renewal runs from a systemd timer installed with the package. Confirm it is armed:

Terminal
systemctl is-active certbot.timer
Output
active

Our full SSL guide for Debian 12 (Bookworm) covers DNS validation, wildcard certificates, and renewal troubleshooting.

Run the WordPress Installer 

Everything is in place. Time to let WordPress finish the setup. In your browser, navigate to your domain. WordPress detects that wp-config.php exists but the database is unpopulated, and presents the famous “five-minute install” form.

  • Pick a language.
  • Set a Site Title (changeable later).
  • Choose a Username for the admin account. Avoid the default admin, which is the first thing brute-force bots try.
  • Use the strong password WordPress generates (or paste your own), and store it in a password manager immediately.
  • Enter an admin email address. WordPress sends update notifications and password-reset emails here.
  • Untick “Discourage search engines from indexing this site” when you’re ready to go live (leave it ticked while still testing; you can flip it later under Settings → Reading).

Click Install WordPress. Within a few seconds you’ll land on a “Success!” screen with a Log In button.

By default WordPress uses ugly query-string URLs (/?p=123). Switch to a human-readable structure:

  • Log into the dashboard and go to Settings → Permalinks.
  • Select Post name (recommended for blogs and most sites) or Day and name (better for news-style date relevance).
  • Click Save Changes.

Unlike Apache, nginx doesn’t need a separate rewrite file. The try_files rule in your server block already routes every non-matching URL through /index.php, where WordPress’s internal router handles it. The Permalinks settings page only writes a database setting; no .htaccess is generated.

Test by visiting any post URL with the new structure (e.g. devanswers.test/hello-world/). If you see a 404 with nginx’s white-on-grey error page, your try_files directive is wrong or your server block isn’t enabled. Check sudo nginx -t and tail the error log: sudo tail -f /var/log/nginx/wordpress-error.log.

Security Hardening 

Three low-cost constants in wp-config.php close common attack vectors without changing how WordPress feels to use.

Terminal
sudo nano /var/www/html/wp-config.php

Add these lines just above the comment that reads /* That's all, stop editing! Happy publishing. */:

/var/www/html/wp-config.php
// Disable the in-dashboard theme/plugin file editor.
// Anyone who gains admin access can otherwise drop PHP into the editor and execute it.
define( 'DISALLOW_FILE_EDIT', true );

// Force all admin and login pages over HTTPS.
// Safe to switch on here: you obtained the certificate earlier in this guide.
define( 'FORCE_SSL_ADMIN', true );

// Skip WordPress's slower FTP-credentials prompt for plugin updates.
// Works because www-data already owns the files.
define( 'FS_METHOD', 'direct' );

Save and exit (press CTRL + X, press Y, then press ENTER)

You enabled HTTPS earlier, so this is safe to set now. On any site that is not yet reachable over HTTPS, FORCE_SSL_ADMIN will lock you out of the dashboard with a redirect loop.

Next Steps 

Your WordPress site is live over HTTPS. Two things to do next:

Conclusion 

Your Debian 12 server now hosts a fresh WordPress installation served by Nginx and PHP-FPM, with a dedicated database user, hardened file permissions, working pretty permalinks via try_files, and three security constants protecting the admin area.

With HTTPS already in place, the next thing to sort is backups: at minimum a daily database dump and a weekly archive of the web root. WordPress is robust, but databases corrupt for unrelated reasons (failed disks, runaway plugins) and you will only miss a backup the one time you needed it. Beyond that, a stock install leaves most of the server’s performance unused: speeding up WordPress on a VPS walks through the measured Nginx recipe: FastCGI page caching, a Redis object cache, OPcache, and sizing your PHP-FPM workers.

Support DevAnswers
Written, tested and maintained by one developer. Your support keeps these guides accurate and ad-light.
Buy me a coffee

Comments

Leave a reply