By the end of this guide you'll have a working WordPress site running on your own Linux VPS, served by Nginx with PHP-FPM and a real MariaDB database behind it. No one-click installer, no magic. This is for anyone who has SSH access to a fresh Ubuntu or Debian server and wants to understand every moving part instead of clicking a button and hoping.
Doing it by hand sounds harder than it is, and the payoff is real. When something breaks later, you'll actually know where to look. We've set up this exact stack on Bytte.cloud VPS boxes plenty of times, and the steps below are the ones we keep coming back to.
What you're building (the LEMP stack)
WordPress needs three things to run: a web server, a database, and PHP. The combination here is usually called LEMP, which stands for Linux, Nginx (the E is for how you say it, "engine-x"), MariaDB, and PHP. Here's the job each one does.
- Nginx takes requests from browsers and either serves static files directly or hands PHP requests off to PHP-FPM.
- MariaDB is the database. It stores your posts, users, settings, and everything else. MySQL works the same way if you prefer it, and the commands are nearly identical.
- PHP-FPM is the process that actually runs WordPress's PHP code and returns HTML for Nginx to send back.
Before you start, make sure you can log in over SSH and that you can run commands with sudo. A domain pointed at the server is nice to have, but you can do the whole install with just the IP address and add the domain later.
Step 1: Update the server and install the packages
Always start with a fresh package list so you're not installing something stale. Then pull in Nginx, MariaDB, and the PHP pieces WordPress depends on.
sudo apt update
sudo apt upgrade -y
sudo apt install -y nginx mariadb-server php-fpm php-mysql \
php-curl php-gd php-xml php-mbstring php-zip php-imagick
Those extra PHP modules matter. WordPress and most plugins expect gd and imagick for image handling, xml and mbstring for content, and curl for talking to outside services. Skip them and you'll hit odd errors during setup.
Once that finishes, check that the services came up:
sudo systemctl status nginx
sudo systemctl status mariadb
Both should say active (running) in green. If Nginx is running, open the server's IP in a browser and you'll see the default Nginx welcome page. That confirms the web server is reachable.
Take note of your PHP version too, because you'll need it for the socket path later:
php -v
On a current Ubuntu release that's typically something like PHP 8.3. Remember that number.
Step 2: Secure MariaDB and create the database
MariaDB ships with a few loose defaults. Run the built in hardening script to tidy them up.
sudo mysql_secure_installation
It'll ask a short list of questions. On a fresh install you can press Enter for the current root password (there isn't one yet). When it asks about switching to unix_socket authentication or setting a root password, pick whatever fits your habits. For everything else, say yes: remove anonymous users, disallow remote root login, drop the test database, and reload privileges. Saying yes to all of those is the safe choice.
Now log in to the database as root and create a database plus a dedicated user for WordPress. Never let WordPress connect as root. Give it its own user with access to just one database.
sudo mariadb
At the MariaDB [(none)]> prompt, run these statements. Change the database name, user, and password to your own values, and use a long random password.
CREATE DATABASE wordpress_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'ChangeThisToAStrongPassword';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
A couple of things worth understanding here. The utf8mb4 character set lets WordPress store emoji and the full range of characters without garbling them. The 'wp_user'@'localhost' part means this user can only connect from the server itself, which is exactly what you want since WordPress runs on the same box. And GRANT... ON wordpress_db.* limits the user to that one database, so a compromise can't touch anything else.
Step 3: Download WordPress into the web root
Grab the latest WordPress, unpack it, and move it into place. We'll use /var/www/wordpress as the web root.
cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
That gives you a wordpress folder inside /tmp. Move its contents into the web root:
sudo mkdir -p /var/www/wordpress
sudo cp -a /tmp/wordpress/. /var/www/wordpress/
The -a flag copies everything including hidden files and keeps timestamps. The trailing dot on /tmp/wordpress/. copies the contents of the folder rather than the folder itself, which keeps your paths clean.
While you're here, copy the sample config to the real config filename. You'll edit it next.
sudo cp /var/www/wordpress/wp-config-sample.php /var/www/wordpress/wp-config.php
Step 4: Edit wp-config.php
This file tells WordPress how to reach the database and sets a batch of secret keys. Open it with a text editor.
sudo nano /var/www/wordpress/wp-config.php
Find the database section near the top and fill in the three values to match what you created in Step 2.
define( 'DB_NAME', 'wordpress_db' );
define( 'DB_USER', 'wp_user' );
define( 'DB_PASSWORD', 'ChangeThisToAStrongPassword' );
define( 'DB_HOST', 'localhost' );
Next come the authentication keys and salts. These are random strings WordPress uses to make login cookies harder to forge. The sample file ships with placeholder text that you must replace. The easy way is to pull a fresh set from the official generator:
curl -s https://api.wordpress.org/secret-key/1.1/salt/
That prints eight define() lines. Copy the whole block, then in your editor delete the matching placeholder lines (the ones for AUTH_KEY, SECURE_AUTH_KEY, and so on) and paste the new block in their place. They should look like this, with real random values instead of put your unique phrase here:
define( 'AUTH_KEY', 'long random string here' );
define( 'SECURE_AUTH_KEY', 'long random string here' );
define( 'LOGGED_IN_KEY', 'long random string here' );
define( 'NONCE_KEY', 'long random string here' );
define( 'AUTH_SALT', 'long random string here' );
define( 'SECURE_AUTH_SALT', 'long random string here' );
define( 'LOGGED_IN_SALT', 'long random string here' );
define( 'NONCE_SALT', 'long random string here' );
Save and exit (in nano that's Ctrl+O, Enter, then Ctrl+X). Getting the salts right is a small step that quietly improves your site's security, so don't leave the placeholders in.
Step 5: Set ownership and permissions
Nginx and PHP-FPM run as the www-data user on Debian and Ubuntu. WordPress needs to read every file and write to a few of them (uploads, plugins, updates), so the web server user has to own the files.
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 {} \;
Here's what those numbers mean in plain terms. Directories get 755 so the owner can read, write, and enter them while others can read and enter. Files get 644 so the owner can read and write while others can only read. That's the standard, sane setup. Resist the urge to chmod 777 anything to fix a problem later, because that opens files to the whole world and is almost never the real fix.
Step 6: Write the Nginx server block
Now tell Nginx about the site. Create a config file in sites-available.
sudo nano /etc/nginx/sites-available/wordpress
Paste in the following. Replace example.com with your domain, or just use the server's IP if you don't have one yet. Also match the fastcgi_pass socket to the PHP version you noted in Step 1.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/wordpress;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; }
}
Two lines are doing the heavy lifting. The try_files $uri $uri/ /index.php?$args; line is what makes pretty permalinks work. It tells Nginx to look for a real file or folder first, and if neither exists, hand the request to index.php so WordPress can route it. The fastcgi_pass line sends PHP requests to the PHP-FPM socket. If your php -v showed 8.2, change the path to php8.2-fpm.sock. You can confirm the exact socket name with:
ls /run/php/
Now enable the site by linking it into sites-enabled, remove the default site so it doesn't shadow yours, test the config, and reload.
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
The nginx -t step is your safety net. It checks the syntax and tells you the file and line number of any mistake before you reload. Always run it. If it prints syntax is ok and test is successful, you're good.
Step 7: Finish the install in your browser
Open http://example.com (or http://your-server-ip) in a browser. WordPress should greet you with a language picker, then the famous five minute setup screen. It asks for:
- A site title
- An admin username (don't use "admin", pick something less obvious)
- A strong password
- Your email address for recovery and notifications
Fill those in and click Install WordPress. After a few seconds you'll get a success message and a login link. Log in at http://example.com/wp-admin and you're looking at a live dashboard. The hard part is done.
One thing worth doing right away: go to Settings then Permalinks and choose "Post name". It gives you clean URLs like /my-first-post instead of /?p=123, and because of the try_files rule you wrote earlier, it'll just work.
A note on HTTPS
Right now your site runs on plain HTTP. For anything public you'll want a free SSL certificate from Let's Encrypt, which takes a couple of minutes with Certbot:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot edits your Nginx config to add the certificate and can set up an automatic HTTP to HTTPS redirect. After that, update the WordPress Address and Site Address under Settings then General to use https://. We'll keep that brief here since it deserves its own walkthrough.
Troubleshooting
502 Bad Gateway
This almost always means Nginx can't reach PHP-FPM. The usual cause is a wrong socket path in the fastcgi_pass line. Run ls /run/php/ and make sure the filename in your config matches exactly. Then confirm PHP-FPM is running:
sudo systemctl status php8.3-fpm
sudo systemctl restart php8.3-fpm
If it still fails, check the Nginx error log, which names the exact problem:
sudo tail -n 30 /var/log/nginx/error.log
Error establishing a database connection
This points at wp-config.php or the database user. Double check that DB_NAME, DB_USER, and DB_PASSWORD exactly match what you set in Step 2, with no stray spaces. Test the credentials directly:
mysql -u wp_user -p wordpress_db
If that command logs you in, your credentials are fine and the issue is a typo in the config file. If it's rejected, recreate the user or reset its password. Also make sure DB_HOST is localhost and MariaDB is running.
Permalinks return 404 (only the homepage works)
If single posts give a "Not Found" but the homepage loads, the try_files rule is missing or wrong in your server block. Confirm this line is present inside the location / block:
try_files $uri $uri/ /index.php?$args;
Run sudo nginx -t and sudo systemctl reload nginx after any change.
Cannot upload images or install plugins
This is a permissions problem. Make sure the files are owned by www-data:
sudo chown -R www-data:www-data /var/www/wordpress
If uploads of larger images fail, raise PHP's limits. Edit your PHP-FPM config (the path matches your version, for example /etc/php/8.3/fpm/php.ini) and bump these values, then restart PHP-FPM:
upload_max_filesize = 32M
post_max_size = 32M
White screen with no error
A blank page usually means a PHP error that's being hidden. Temporarily turn on debugging by editing wp-config.php and setting:
define( 'WP_DEBUG', true );
Reload the page, read the error, fix it, then set it back to false so you don't leak details to visitors. A missing PHP module is a common culprit here, which is why installing them all in Step 1 saves headaches.
Where to go from here
You now have a self-managed WordPress install that you understand end to end. The sensible next moves are adding HTTPS if you skipped it, setting up regular database backups with mysqldump, and keeping the server patched with security updates. From here you can pick a theme, install plugins, and start publishing, knowing exactly what's running underneath. And if a VPS with NVMe storage and DDoS protection is what you're after to host it on, that's the kind of box this setup is built for.



