Side story: WordPress on shared hosting and on a VPS
Install WordPress
This page is optional. The course does not need
WordPress, and every lesson works with the
small hand-written PHP site instead. Read on only if you already run
WordPress, or have decided to, and want the steps that a WordPress site
adds to the ones the lessons already cover.
The database is the same one the lessons use. Create
example_db and example_user
in the panel on shared hosting, or with
MariaDB
on a VPS, exactly as lesson 4 and lesson 5 show. WordPress then needs
four values: database name, database user, password and host, which is
localhost when the database runs on the same
machine. The installer stores them in
wp-config.php as
DB_NAME, DB_USER,
DB_PASSWORD and DB_HOST.
There are two ways to install the files. On shared hosting the panel usually has WordPress in its applications section: choose the domain, the folder and the database, and it downloads and configures everything. Manually, download the archive from wordpress.org, unpack it, and upload the contents of the wordpress folder into the site folder, so that index.php and wp-admin/ sit directly in the web root. Open https://example.com/wp-admin/install.php and create the administrator account.
wp-config.php above the web root
The installer writes wp-config.php into the web root. That file holds the database password, so it belongs one level above the web root, next to html/, where the web server cannot serve it. WordPress looks for it there as well as in the installation folder, which is why the move works without any other change. On the VPS:
sudo mv /var/www/example.com/html/wp-config.php /var/www/example.com/wp-config.php
sudo chown example:example /var/www/example.com/wp-config.php
sudo chmod 600 /var/www/example.com/wp-config.php
The owner is changed as well, because the moved file keeps the owner the
installer gave it, often www-data, and once
lesson 8 runs PHP as example the site could no
longer read it.
On shared hosting, move the file up one folder over
SFTP: from
www/example.com/ to www/.
The site keeps working, and a PHP error can no longer print the file or
hand it to a visitor.
HTTPS and the site address
WordPress stores its own address in the database, so switching the site to HTTPS is two settings, not one. After the certificate works, open Settings, General and set both the WordPress address and the site address to https://example.com. The same values can be fixed in wp-config.php, which wins over the database and is easier to correct when the admin pages stop loading:
sudo vi /var/www/example.com/wp-config.php
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');
Then open the site and look for mixed content: an image or a stylesheet still loaded over http:// is blocked by the browser on an HTTPS page, and the theme looks broken. Update the address in the content, or use a relative path.
Permalinks and try_files
WordPress sends every request that is not a real file to index.php. On Nginx the server block from lesson 5 uses try_files $uri $uri/ =404;, which is right for the small site and returns 404 for a pretty permalink. Change that line for WordPress:
sudo vi /etc/nginx/sites-available/example.com
location / { try_files $uri $uri/ /index.php?$args; }
sudo nginx -t
sudo systemctl reload nginx
Then choose Post name under Settings, Permalinks. On shared hosting with Apache the same job is done by the .htaccess file that WordPress writes itself.
Uploads and PHP in uploads
WordPress writes every uploaded file into wp-content/uploads, so PHP must be able to write there. Until lesson 8 gives the site its own pool, PHP runs as www-data; give that group write access:
sudo -u example mkdir -p /var/www/example.com/html/wp-content/uploads
sudo chgrp -R www-data /var/www/example.com/html/wp-content/uploads
sudo chmod -R g+w /var/www/example.com/html/wp-content/uploads
The Nginx snippets below assume the per-site PHP-FPM pool from lesson 8. Before that pool exists, the socket is unix:/run/php/php8.3-fpm.sock, the default one from lesson 5.
An uploads folder is also the place where an attacker who found an upload hole tries to leave a PHP file. Refuse to execute PHP there, and put the rule before the PHP location of the site:
sudo vi /etc/nginx/sites-available/example.com
location ~* ^/wp-content/uploads/.*\.php$ { deny all; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.sock; }
The login page
/wp-login.php is the page every bot knows. A rate limit from lesson 8 makes guessing slow without blocking a person who mistypes a password once:
sudo vi /etc/nginx/sites-available/example.com
location = /wp-login.php { limit_req zone=perip burst=5 nodelay; limit_req_status 429; include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.sock; }
Two-factor authentication on the administrator account is the stronger measure; the limit only removes the noise.
Plugins, themes and updates
unattended-upgrades patches Ubuntu, not the code inside the site folder. In the admin, open Plugins and Themes and switch on automatic updates for every one you keep; WordPress then installs new versions without you. Delete the plugins and themes you do not use, because an inactive plugin is still code on the disk and still a way in.
On a server with WP-CLI, the same job is two commands. Run them as the site user once a week, from a cron line or by hand:
sudo -u example -H wp plugin update --all --path=/var/www/example.com/html
sudo -u example -H wp theme update --all --path=/var/www/example.com/html
Read the output: a plugin that fails to update is a plugin to remove or to replace.
xmlrpc.php and the database check
xmlrpc.php is an old interface used by some desktop apps and by Jetpack. If nothing on your site uses it, block it in the site configuration:
sudo vi /etc/nginx/sites-available/example.com
location = /xmlrpc.php { deny all; }
After moving the site and importing the database, check that the posts arrived. The default table prefix is wp_; if the site used another one, use that name:
sudo mariadb -e "SELECT COUNT(*) FROM example_db.wp_posts;"
+----------+ | COUNT(*) | +----------+ | 24 | +----------+
Quiz
1 Where should wp-config.php live?
Answer
b) The file holds the database password, so it must not be servable. WordPress looks in its installation folder and one directory above it.
2 Which try_files line makes WordPress permalinks work?
Answer
b) A pretty permalink is not a file on disk, so the request goes to the front controller. The plain =404 version is correct for the small hand-written site.
3 Why refuse to execute PHP in wp-content/uploads?
Answer
b) The uploads folder must hold images and documents, never code. The rule goes before the PHP location, because nginx uses the first matching regex location.
4 After the certificate works, the admin keeps redirecting to http://. What is still wrong?
Answer
b) WordPress stores its base address in the database. Set both addresses to https://example.com, in the admin or in wp-config.php.
5 What should you do with xmlrpc.php if no app uses it?
Answer
b) It is an old interface with a long history of abuse. If nothing needs it, a deny all rule removes the risk.
Article author: Arthur Isaev