Get your free server today! View Plans →
Home Plans Blog About Contact Panel Join Discord
Hosting

How to install WordPress manually on a VPS

Skip the one-click installer and set up WordPress yourself on a Linux VPS. This guide walks through Nginx, MariaDB, PHP-FPM, the wp-config file, permissions and a working Nginx server block.

How to install WordPress manually on a VPS

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.

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:

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.

Common questions

Do I need a domain name to install WordPress on a VPS?

No. You can complete the whole install using just the server's public IP address and visit it in a browser. You can point a domain at the server and update the Nginx server_name and your WordPress site address later.

Should I use MariaDB or MySQL for WordPress?

Either works fine. MariaDB is a drop-in replacement for MySQL and the commands in this guide are identical for both. MariaDB is the default on most current Debian and Ubuntu systems, which is why we use it here.

Why am I getting a 502 Bad Gateway error?

A 502 almost always means Nginx cannot reach PHP-FPM. The usual cause is a wrong socket path in the fastcgi_pass line. Run ls /run/php/ to find the correct socket name, match it in your config, then confirm PHP-FPM is running and reload Nginx.

Why do only my homepage and not my posts load after I change permalinks?

That is a missing try_files rule in your Nginx server block. Make sure the location / block contains try_files $uri $uri/ /index.php?$args; then run nginx -t and reload Nginx so single posts route through index.php.

What file permissions should WordPress files have?

Set ownership to the www-data user, directories to 755 and files to 644. Avoid 777, which exposes files to everyone and is rarely the real fix for an upload or plugin problem.

DO
Daniel Okafor
Web Developer at Bytte.cloud

Part of the Bytte.cloud team. We run game servers, bots and websites for a living, and we write these guides from what we see day to day in support and on our own servers.

Want to try this on real hardware?

Bytte.cloud has free plans for game servers, bots and websites. No credit card, set up in seconds.

Start for free See the plans