Installing WordPress on a VPS by hand
A step-by-step guide to installing WordPress on a VPS using only SSH and the command line. Covers database creation, file setup, web server configuration, and browser-based installation without any control panel.
Installing WordPress on a VPS by hand takes about twenty minutes and comes down to five things: a database and a database user, the WordPress files in the right place, correct ownership and permissions, a web server that knows how to hand PHP to PHP-FPM, and a certificate so the browser installer runs over HTTPS. No control panel is involved at any point. Everything below is typed into an SSH session and then finished in a browser.
This picks up from a VPS you can already reach over SSH as a user with sudo, with a web server (Nginx or Apache), PHP-FPM and MariaDB or MySQL already installed. If you have not got that far, start with our VPS guides and come back.
Before you start
A few things decide whether this goes smoothly or turns into an afternoon.
- Versions. WordPress currently asks for PHP 8.3 or greater and MariaDB 10.11 or greater, or MySQL 8.0 or greater. It still runs on PHP 7.4 and MySQL 5.5.5, and plenty of tutorials still quote those numbers, but they are the end-of-life floor rather than a recommendation. Do not build a new site on them.
- DNS. The browser installer stores whatever URL you visit it on, so point the domain at the VPS before you run it. If your domain is at another registrar and you want Hostworld handling DNS, set the nameservers there to
ns1.serverworld.uk,ns2.serverworld.uk,ns3.serverworld.ukandns4.serverworld.uk, then create an A record for the domain pointing at your VPS IP address. - A way back in. You are about to change firewall rules. If you lock yourself out of SSH, the VNC console in Virtualizor gets you back to a local login without a support ticket. Know where it is before you need it.
- SELinux. AlmaLinux 9 enforces it, Ubuntu 24.04 does not use it. If you are on AlmaLinux, Step 8 is not optional, and skipping it produces a site that loads but cannot upload media or install plugins.
Step 1: Check what PHP and database versions you actually have
This prints the PHP version the command line is using, which is normally the same build as PHP-FPM:
php -v
On Ubuntu 24.04 the distribution's PHP is 8.3, which meets the recommendation with no third-party repository involved. The service is called php8.3-fpm.
On AlmaLinux 9, PHP is delivered as a module stream and the versions available depend on the point release you are on. Rather than trusting a pasted repository instruction from a 2024 article, list the streams your machine can see and check the database package:
sudo dnf module list php
sudo dnf info mariadb-server
If the stream you need is present, enable it and install from it. If the highest stream available is below PHP 8.3, you have a real decision to make about adding a third-party repository, and it is worth opening a support ticket before you do, rather than pulling in a repository that later fights with updates.
Step 2: Install the PHP extensions WordPress needs
A missing extension does not produce a useful error. It produces a blank or half-drawn installer page, which readers then blame on the database. Install the set up front.
Ubuntu 24.04:
sudo apt install php8.3-mysql php8.3-mbstring php8.3-xml php8.3-curl php8.3-gd php8.3-zip php8.3-bcmath php8.3-common php8.3-cli
AlmaLinux 9: package names follow the same pattern without the version prefix.
sudo dnf install php-mysqlnd php-mbstring php-xml php-curl php-gd php-zip php-bcmath php-cli
Beyond the strict minimum, curl, imagick, mbstring and openssl are the ones worth having. Without them image processing and outbound API calls degrade quietly rather than failing loudly. Restart PHP-FPM afterwards so the new modules load.
Step 3: Create the database and a dedicated database user
If you have not run the hardening script on a fresh database server, do that first. It removes anonymous accounts and the test database and asks about the root password:
sudo mysql_secure_installation
Now open a database shell. On Ubuntu the root database account authenticates through the socket, so sudo mysql works and mysql -u root -p often returns "access denied" to people who never set a password. That is not a broken install.
sudo mysql
Create the database with the utf8mb4 character set. This supports the full Unicode range including emoji, which WordPress has needed since version 4.2, and converting a database after it has content in it is unpleasant work.
CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Now create the user, and then grant it privileges, as two separate statements. This is the single most common place a command-line install breaks. Older MySQL let you write GRANT ALL ... IDENTIFIED BY 'password' and create the user implicitly. MySQL 8.0 removed that: GRANT no longer creates accounts, and the inline password syntax returns ERROR 1064. MariaDB still accepts the old form, which is why half the guides on the internet still print it. The two-statement version works on both engines, so write it this way and you never meet the error.
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Do not point WordPress at the root account. If the site is compromised, a scoped user loses one database and root loses the whole server.
Step 4: Download and unpack WordPress
Download the current release as a tarball and unpack it in /tmp. Use the latest.tar.gz URL rather than a versioned one: point releases land constantly, and a hard-coded version in your notes is out of date within weeks.
cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
The archive always unpacks into a directory called wordpress. Move that whole directory into place:
sudo mv /tmp/wordpress /var/www/wordpress
Two traps here. Moving the directory into a path that already ends in wordpress gives you /var/www/wordpress/wordpress, and the site then 404s at the document root. And if you choose instead to move the contents with mv wordpress/* /var/www/html/, the glob skips dotfiles, so anything beginning with a full stop is left behind in /tmp. Moving the directory itself avoids both.
Step 5: Set ownership and permissions that are not 777
Ownership has to match the user PHP-FPM runs as. On Ubuntu that is www-data. On AlmaLinux the RPM default is apache, and that stays true even if you are running Nginx, because the php-fpm pool is unchanged. If you are using Nginx on AlmaLinux, either edit /etc/php-fpm.d/www.conf to set user = nginx and the matching group, or leave it as apache and own the files as apache. Check rather than copy a value from a tutorial:
grep -E '^(user|group)' /etc/php-fpm.d/www.conf
Ubuntu 24.04:
sudo chown -R www-data:www-data /var/www/wordpress
AlmaLinux 9 (substitute nginx:nginx if that is what the pool says):
sudo chown -R apache:apache /var/www/wordpress
Now the modes. The WordPress Advanced Administration Handbook is clear: directories 755 or 750, files 644 or 640, and no directory should ever be 777, including uploads. The reason 755 is enough is that PHP runs as the owner of the files, so it gets the owner's permissions and can write anywhere it owns. With that in place WordPress detects it can create files directly and stops asking for FTP credentials during updates.
Never run a blanket recursive chmod across the tree. A directory with mode 644 cannot be entered, so chmod -R 644 breaks the site. Set directories and files separately, on both operating systems:
sudo find /var/www/wordpress -type d -exec chmod 755 {} \;
sudo find /var/www/wordpress -type f -exec chmod 644 {} \;
wp-config.php does not exist yet. Come back to it at Step 10.
Step 6: Configure the web server
Whichever server you use, PHP has to be handed to PHP-FPM over a socket, and the socket filename differs by distribution and PHP version. Find yours rather than trusting a path from a tutorial:
sudo find /run -name '*.sock'
Nginx
Nginx needs an explicit PHP handler and a try_files rule, otherwise permalinks fail. Put this in /etc/nginx/conf.d/wordpress.conf on AlmaLinux 9, or /etc/nginx/sites-available/wordpress.conf on Ubuntu 24.04 with a symlink into sites-enabled:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/wordpress;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm/www.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Test the configuration before reloading. A reload on a broken config takes the site down, and nginx -t catches it first. You want "syntax is ok" and "test is successful":
sudo nginx -t
sudo systemctl reload nginx
Remove any default placeholder file left in the web root, or it shadows the site.
Apache
On Ubuntu 24.04, enable the modules that connect Apache to PHP-FPM and the rewrite module, then the FPM configuration:
sudo a2enmod proxy_fcgi setenvif rewrite
sudo a2enconf php8.3-fpm
sudo systemctl reload apache2
Pretty permalinks need two things done together. Enabling rewrite is one. The other is changing AllowOverride None to AllowOverride All in the <Directory /var/www/> block, so .htaccess is read at all. Enable the module and leave AllowOverride alone and you get a working homepage with 404s on every other page, which almost everyone blames on WordPress.
On AlmaLinux 9 the equivalent lives in a vhost under /etc/httpd/conf.d/, and the service is httpd. On both, check that index.php comes before index.html in the DirectoryIndex list, or a leftover index.html hides the site.
Step 7: Open the firewall
These commands allow HTTP and HTTPS through and make the change persist across reboots.
AlmaLinux 9 uses firewalld:
sudo firewall-cmd --add-port=80/tcp --permanent
sudo firewall-cmd --add-port=443/tcp --permanent
sudo firewall-cmd --reload
Ubuntu 24.04 uses ufw, if you have it enabled:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Step 8: Deal with SELinux (AlmaLinux 9 only)
SELinux blocks the web server from writing to files it has not been told about, which is why an AlmaLinux install can serve pages perfectly and still refuse every upload. Add a policy rule for the WordPress directory and then relabel it:
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/wordpress(/.*)?"
sudo restorecon -Rv /var/www/wordpress
Some guides use chcon instead. Avoid it here: chcon changes the label but does not record it in policy, so the next relabel undoes your work. Also avoid httpd_sys_content_t, which is read-only and blocks uploads and plugin installs.
Separately from file labels, SELinux blocks outbound network connections from the web server, which breaks update checks and plugin installation. List the relevant booleans and find the one covering outbound httpd connections, then turn it on permanently:
getsebool -a | grep httpd
sudo setsebool -P <boolean-name> on
Step 9: Get HTTPS working before you touch the browser
Do this now rather than after. The installer form you are about to fill in carries your admin username and password, and WordPress stores the site URL using whatever scheme you visited it on. Installing over plain HTTP means either sending credentials in the clear or rewriting the site URL afterwards.
Certbot is the usual route on both AlmaLinux 9 and Ubuntu 24.04, using the plugin that matches your web server so it can edit the vhost and set up renewal. Confirm the domain resolves to your VPS first, because the certificate authority checks that.
Step 10: Finish the install in the browser
Visit https://yourdomain.com. WordPress asks for the database name, the database user, the password you set in Step 3, and the database host, which is localhost here. It then writes wp-config.php. If the web server user cannot write to the directory, it shows you the file contents instead so you can save them yourself.
Next it asks for the site title and your admin account. Pick an admin username that is not "admin".
Once wp-config.php exists, tighten it. The handbook value is 440 or 400, so that no other user on the server can read your database credentials:
sudo chmod 440 /var/www/wordpress/wp-config.php
Sources disagree on this one. Some recommend 600, or 640 where the web server runs as a different user in the same group. The rule behind all three is the same: the file should be readable by the user PHP runs as and by nobody else. Check what your setup actually needs rather than pasting a number.
Step 11: Two pieces of hardening worth doing straight away
Disable the built-in theme and plugin file editor by adding this line to wp-config.php. It turns one stolen admin password into something much less useful, because the attacker can no longer paste PHP into a theme file from the dashboard:
define( 'DISALLOW_FILE_EDIT', true );
Second, stop PHP executing inside the uploads directory. That is a web server rule rather than a WordPress setting, and the correct form differs between Apache and Nginx, so write it in the same place you wrote your vhost in Step 6.
What next
You now have a working WordPress install on your own server with no panel in the way. The logical next step is backups, because nothing in the process above protects you from your own rm -rf, followed by keeping PHP and the database server patched.
If something in the middle of this went wrong and you cannot work out which layer is at fault, open a support ticket and tell us the distribution, the web server and the exact error. Tickets are logged against your account, so whoever picks it up can see the machine. For the wider set of server tasks around this one, including firewalls and SSH, see our VPS guides.
Common questions
Should I be doing this by hand at all?
Sometimes the honest answer is no. Installing by hand makes sense when you want control over the stack, when you are running several sites with unusual requirements, or when you are learning. If what you actually want is a WordPress site without maintaining a server, our shared and WordPress hosting run on cPanel, and the installation is handled for you. Nothing in this guide is a rite of passage.
Why does the installer page come up blank?
Almost always a missing PHP extension, which produces a blank or partial page rather than an error. Go back to Step 2, install the full list, restart PHP-FPM and reload the web server. If the page is still blank, check the PHP-FPM and web server error logs, which will name the missing piece.
My homepage works but every other page is a 404.
That is the permalinks problem from Step 6. On Apache, you need the rewrite module enabled and AllowOverride All on the directory. On Nginx, you need the try_files $uri $uri/ /index.php?$args; line inside location /. Reload the web server after either change.
Can I set the uploads directory to 777 to fix upload errors?
No. 777 lets any user on the server write executable code into a directory the web server will happily serve. If uploads fail, the cause is ownership (Step 5) or, on AlmaLinux, the SELinux file context (Step 8). Fix the cause. Directories stay at 755 or 750.
Do I need to pin a specific WordPress version in the download URL?
No, and it is better not to. Use https://wordpress.org/latest.tar.gz. Security and maintenance releases go out frequently, and a pinned version in your build notes means you install something out of date and then have to update it in the dashboard anyway.