WordPress on LEMP (nginx, MariaDB, PHP)
Step-by-step installation of a WordPress site on an Ubuntu or Debian server, with HTTPS, daily backups and basic settings.
On this page
This guide shows you how to install a WordPress site on the LEMP stack: Linux, nginx (the web server), MariaDB (the database) and PHP-FPM, with a free HTTPS certificate from Let's Encrypt. Run all commands on the server as root, in an SSH session: Connecting to your server over SSH (Windows, macOS, Linux). You'll find the server's IP address in the app.blackbox.md panel, under “Infrastructure” → “My Servers” (app.blackbox.md/my-servers), in the “IP” field on the server card.
Available to: the holder of a BLACKBOX VPS server without a control panel, with root access over SSH. The steps are written for Ubuntu 24.04 LTS and Debian 12; the differences for AlmaLinux 9 and CentOS Stream 9 are in a separate section. On a “VPS hosting” plan with a control panel, you create sites from the control panel: “VPS hosting” plans with a control panel.
Which plan and system to choose
For a typical site, a Standard S or Standard M plan is a good start:
| Plan | vCPU | RAM | Disk | Price | Good for |
|---|---|---|---|---|---|
| Standard S | 1 | 2 GB | 20 GB | 443 MDL/month | a company presentation site, a blog |
| Standard M | 2 | 4 GB | 40 GB | 578 MDL/month | an online store, a site with many plugins |
All plans are listed in VPS plans: how to choose. When your site grows, move to a larger plan: Server upgrade: moving to a larger plan.
For the operating system, choose Ubuntu 24.04 LTS (selected by default when you order) or Debian 12. Ubuntu 20.04 LTS and Debian 11 come with PHP 7.4, which the PHP project no longer updates: don't use them for a new site.
Before you install, check that:
- the A records for
DOMAINandwww.DOMAINpoint to the server's IP address; otherwise, the HTTPS certificate can't be issued: How to point your domain to the server; - ports 80 and 443 are open. The BLACKBOX network filter leaves them open by default, and the system firewall (ufw or firewalld) is not running on a new server. If you have turned it on, allow ports 80 and 443 there as well: Firewall: network filtering and ufw/firewalld on the server.
In all commands, replace DOMAIN with the site's name without www, for example example.com.
Step 1. Install nginx, MariaDB and PHP
Update the package list and install the components. On Ubuntu and Debian, the services start automatically after installation.
apt update apt install -y nginx mariadb-server php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl curlFind out which PHP version is installed from the name of the PHP-FPM socket:
ls /run/php/System PHP PHP-FPM socket PHP-FPM service Ubuntu 24.04 LTS 8.3 /run/php/php8.3-fpm.sockphp8.3-fpmDebian 12 8.2 /run/php/php8.2-fpm.sockphp8.2-fpmUbuntu 22.04 LTS 8.1 /run/php/php8.1-fpm.sockphp8.1-fpmImportant The examples use PHP 8.3 (Ubuntu 24.04 LTS). On a different system, replace
8.3with your version everywhere: in the/etc/php/8.3/…paths, in thephp8.3-fpmservice name and in the socket in the nginx configuration.Raise the maximum size of files uploaded to WordPress (by default, PHP accepts only 2 MB) and restart PHP-FPM:
printf 'upload_max_filesize = 64M\npost_max_size = 64M\n' > /etc/php/8.3/fpm/conf.d/99-wordpress.ini systemctl restart php8.3-fpm
Step 2. Create the database
Run the MariaDB security script and answer as shown in the table:
mariadb-secure-installationQuestion Answer Enter current password for root (enter for none):press Enter Switch to unix_socket authentication [Y/n]nChange the root password? [Y/n]nif the script printed “You already have your root account protected, so you can safely answer 'n'.”; otherwise,Yand a strong passwordRemove anonymous users?,Disallow root login remotely?,Remove test database and access to it?,Reload privilege tables now?Yto all of themThe MariaDB
rootuser logs in through the system'srootaccount (unix_socketauthentication), with themariadbcommand and no password.Generate a strong password for the database user and write it down. Below, it appears as
DATABASE_PASSWORD:openssl rand -hex 20Open the MariaDB console:
mariadbCreate the
wordpressdatabase and thewpuseruser, with rights on this database only, then exit:CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'DATABASE_PASSWORD'; GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost'; FLUSH PRIVILEGES; EXIT;
The wpuser user can connect only from the server itself (localhost). Don't open port 3306 to the internet. More about MariaDB: Databases: MariaDB/MySQL and PostgreSQL.
Step 3. Copy the WordPress files
Download the latest version from wordpress.org and extract it to
/var/www/DOMAIN:mkdir -p /var/www/DOMAIN cd /tmp curl -O https://wordpress.org/latest.tar.gz tar -xzf latest.tar.gz -C /var/www/DOMAIN --strip-components=1 rm latest.tar.gzCreate
wp-config.phpand write the database details into it. ReplaceDATABASE_PASSWORDwith the password from step 2.cd /var/www/DOMAIN cp wp-config-sample.php wp-config.php sed -i "s/database_name_here/wordpress/; s/username_here/wpuser/; s/password_here/DATABASE_PASSWORD/" wp-config.phpReplace the 8 security keys (salts) with unique values generated by the WordPress.org service:
curl -s https://api.wordpress.org/secret-key/1.1/salt/ -o /tmp/wp-salts.txt sed -i '/put your unique phrase here/d' wp-config.php sed -i '/#@-/r /tmp/wp-salts.txt' wp-config.php rm /tmp/wp-salts.txtCheck the result. You should see 3
DB_lines with your details and 8 lines, fromAUTH_KEYtoNONCE_SALT, with long random strings.grep -E "DB_(NAME|USER|PASSWORD)|_KEY'|_SALT'" wp-config.phpGive ownership of the files to the
www-datauser, which nginx and PHP-FPM run as, and set the permissions:chown -R www-data:www-data /var/www/DOMAIN find /var/www/DOMAIN -type d -exec chmod 755 {} + find /var/www/DOMAIN -type f -exec chmod 644 {} + chmod 640 /var/www/DOMAIN/wp-config.phpThis way, WordPress can install its own updates and plugins, and
wp-config.php, which holds the database password and the keys, can't be read by the system's other users.
Step 4. Configure nginx
Create the site's file:
nano /etc/nginx/sites-available/DOMAINPaste the configuration below, replace
DOMAIN, save with Ctrl+O and Enter, then exit with Ctrl+X:server { listen 80; server_name DOMAIN www.DOMAIN; root /var/www/DOMAIN; index index.php index.html; client_max_body_size 64M; location / { try_files $uri $uri/ /index.php?$args; } location ~* /wp-content/uploads/.*\.php$ { deny all; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } location ~ /\.(?!well-known) { deny all; } }Line What it does try_files $uri $uri/ /index.php?$args;serves files that exist and sends all other requests to WordPress (required for permalinks) fastcgi_pass unix:/run/php/php8.3-fpm.sock;sends .phpfiles to PHP-FPM; the path must match the socket from step 1 exactlyclient_max_body_size 64M;the maximum size of an uploaded file, the same as in PHP location ~* /wp-content/uploads/.*\.php$stops PHP scripts from running in the uploads folder location ~ /\.(?!well-known)blocks hidden files ( .htaccess,.git), but keeps/.well-known/reachable for Let's EncryptEnable the site, check the configuration and reload nginx:
ln -s /etc/nginx/sites-available/DOMAIN /etc/nginx/sites-enabled/ nginx -t systemctl reload nginxnginx -tmust showsyntax is okandtest is successful. If it shows an error, fix the line it points to and run the test again before you reload.
Step 5. Enable HTTPS
Install certbot with its nginx plugin:
apt install -y certbot python3-certbot-nginxGet the certificate for both names:
certbot --nginx -d DOMAIN -d www.DOMAINOn the first run, certbot asks for an email address and for you to accept the Let's Encrypt terms. If it asks whether to redirect HTTP to HTTPS, choose the redirect. At the end, a message starting with “Congratulations!” appears: certbot has added the certificate to the nginx configuration and reloaded nginx.
The certificate is valid for 90 days and renews automatically. Check the renewal:
certbot renew --dry-run
Details and common errors: Web server with HTTPS: nginx or Apache and Let's Encrypt.
Step 6. Finish the installation in your browser
- Open
https://DOMAIN. The WordPress installation wizard appears. - Choose the site's language and continue.
- Fill in the site title, the administrator username, the password and the email address. Don't use
adminas the username, and keep the strong password that WordPress suggests. - Confirm the installation, then sign in to the admin dashboard at
https://DOMAIN/wp-admin.
Install WordPress after step 5: that way, the site saves its address with https:// from the start.
Security
- WordPress updates. Minor security updates install automatically. Update major versions, plugins and themes from the WordPress admin dashboard; for plugins, you can turn on automatic updates from the plugin list. Delete the plugins and themes you don't use.
- Permissions. After you upload files over SFTP as
root, run all four commands from step 3, item 5, again (on AlmaLinux, withapache:apache): Transferring files to the server (scp, rsync, SFTP). - The system. Turn on automatic security updates and fail2ban: System updates and automatic security updates, Protection against password-guessing attempts with fail2ban.
Also turn off the code editor in the WordPress dashboard, so that a compromised administrator account can't change the PHP files directly:
sed -i "/That's all, stop editing/i define( 'DISALLOW_FILE_EDIT', true );" /var/www/DOMAIN/wp-config.phpDaily backups
Back up the database and the wp-content folder, with its themes, plugins and uploaded files, every day. The script below runs daily through cron. If the /etc/cron.daily folder doesn't exist on the server, first install the cron package (Ubuntu, Debian) or cronie (AlmaLinux, CentOS Stream).
Create the folder for the backups and the script file:
mkdir -p /root/backup nano /etc/cron.daily/wordpress-backupPaste the content below, replace
DOMAINand save:#!/bin/sh D=$(date +%F) mysqldump --single-transaction wordpress | gzip > /root/backup/wordpress-db-$D.sql.gz tar -czf /root/backup/wp-content-$D.tar.gz -C /var/www/DOMAIN wp-content find /root/backup -type f -mtime +7 -deleteMake the script executable and run it once as a test:
chmod +x /etc/cron.daily/wordpress-backup /etc/cron.daily/wordpress-backup ls -lh /root/backupTwo files with today's date should appear. From now on, the script runs every day, and backups older than 7 days are deleted automatically. On a small disk (Standard S has 20 GB), keep an eye on the used space: The server's disk is full.
Warning The backups in
/root/backupare on the same server and are lost along with it. Copy them somewhere else regularly: to another server or to external storage. BLACKBOX does not make automatic backups of the data on your server: Backup strategy for your server.
Performance
OPcache. The PHP code cache is on by default. Check it: the output must contain Zend OPcache.
php-fpm8.3 -m | grep -i opcachePage cache. From the WordPress dashboard, install a single page-caching plugin, for example WP Super Cache. The plugin saves pages once they are generated and serves them again without querying the database, which is much faster.
Swap memory. On plans with 2 GB of RAM, such as Standard S, add 1–2 GB of swap so that traffic spikes don't stop MariaDB for lack of memory: Adding swap space. If the server uses swap all the time, move to a larger plan. How to check the load: Diagnosing server load (CPU, RAM, disk, network).
AlmaLinux 9 and CentOS Stream 9
The steps are the same, with the differences below. SELinux is active: don't turn it off; set the correct labels instead.
Enable PHP 8.3 (by default, the system installs PHP 8.0), install the packages and start the services, which don't start on their own on these systems. The curl extension is included in the base PHP package.
dnf module enable -y php:8.3 dnf install -y nginx mariadb-server php-fpm php-mysqlnd php-gd php-mbstring php-xml php-intl php-opcache php-pecl-zip nano tar systemctl enable --now nginx mariadb php-fpmUse the paths and names in the right-hand column:
What Ubuntu and Debian AlmaLinux and CentOS Stream PHP-FPM user and file owner www-dataapachePHP-FPM socket /run/php/php8.3-fpm.sock/run/php-fpm/www.socknginx site file /etc/nginx/sites-available/DOMAINand a link insites-enabled/etc/nginx/conf.d/DOMAIN.conf, no linkPHP upload limits /etc/php/8.3/fpm/conf.d/99-wordpress.ini/etc/php.d/99-wordpress.iniPHP-FPM service php8.3-fpmphp-fpmOn these systems, MariaDB listens on all interfaces: bind it to
127.0.0.1as shown in Databases: MariaDB/MySQL and PostgreSQL.In the nginx configuration, replace the
location ~ \.php$block with this one (thesnippets/fastcgi-php.conffile doesn't exist on these systems):location ~ \.php$ { try_files $uri =404; fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }Give ownership of the files to the
apacheuser, then allow, in SELinux, writing to the site's folder (uploads, updates) and PHP connections to the internet (downloading updates and plugins):chown -R apache:apache /var/www/DOMAIN dnf install -y policycoreutils-python-utils semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/DOMAIN(/.*)?" restorecon -Rv /var/www/DOMAIN setsebool -P httpd_can_network_connect 1If you later move (
mv) files into the site's folder from another folder, restore the SELinux labels:restorecon -Rv /var/www/DOMAINInstall certbot from the EPEL repository and start the renewal timer, which doesn't start on its own on these systems:
dnf install -y epel-release dnf install -y certbot python3-certbot-nginx systemctl enable --now certbot-renew.timerTo check OPcache, use
php-fpminstead ofphp-fpm8.3:php-fpm -m | grep -i opcache
Common problems
| Message or symptom | Cause | What to do |
|---|---|---|
502 Bad Gateway |
PHP-FPM is stopped, or fastcgi_pass points to a different socket |
check systemctl status php8.3-fpm and compare fastcgi_pass with the output of ls /run/php/ |
| The browser downloads the PHP file or shows the default nginx page | the site file isn't enabled, or the block for .php is missing |
check the link in sites-enabled, run nginx -t and reload nginx |
| “Error establishing a database connection” (or its translation) | wrong details in wp-config.php, or MariaDB is stopped |
compare the DB_ lines with step 2; check systemctl status mariadb |
413 Request Entity Too Large |
the file is larger than client_max_body_size |
raise the limit in nginx and in the 99-wordpress.ini file, then reload nginx and restart PHP-FPM |
| WordPress asks for FTP details when you install a plugin | the files don't belong to the PHP-FPM user | run the chown command from step 3 again (apache:apache on AlmaLinux) |
| Uploads or updates fail on AlmaLinux with “Permission denied” | SELinux labels | repeat the semanage, restorecon and setsebool commands above |
| certbot can't get the certificate | the domain doesn't point to the server, or port 80 is blocked | check the A records; see The site does not open |
nginx errors are logged in /var/log/nginx/error.log.
Frequently asked questions
Can I host several WordPress sites on the same server?
Yes. For each site, repeat steps 2–6 with a separate database, user, folder in /var/www/ and nginx file; you don't need to run the mariadb-secure-installation script again. Also add the new site's database and folder to the backup script. Keep an eye on memory and disk space.
WordPress doesn't send emails (password resets, contact forms). A new server usually has no email-sending service set up. Install an SMTP plugin and connect it to an account with an email service.
I already have a WordPress site with another provider. How do I move it here? Prepare the server with steps 1, 2 and 4, then follow Moving a site to a BLACKBOX server: copy the files, import the database and switch the DNS at the end.
Can BLACKBOX install WordPress for me? Yes, as a professional service on request, through support. You receive the scope of work, the price and the terms in our team's offer: Professional services on request: administration, DevOps, migration.