How to Install WordPress on a VPS: Ubuntu 24.04 + Caddy Guide (2026)

Install WordPress on Ubuntu 24.04 with Caddy, PHP 8.3-FPM and MariaDB from official docs, then get permissions, cron, caching, backups and sizing right.

Title card reading “WordPress on Ubuntu 24.04” for the HourlyVPS guide to installing WordPress on a VPS with Caddy, PHP-FPM and MariaDB

To install WordPress on a VPS running Ubuntu 24.04, install PHP 8.3-FPM, the PHP extensions WordPress recommends and MariaDB from Ubuntu’s archive, download WordPress and verify its SHA-1 checksum, and create wp-config.php with fresh salts from api.wordpress.org. Then run the site in its own PHP-FPM pool under a dedicated user, install it with WP-CLI, and point Caddy at the pool with php_fastcgi; Caddy gets and renews the HTTPS certificate on its own.

The commands follow the official documentation as checked on October 3, 2026, for WordPress 7.1.2 (released September 22, 2026), Ubuntu 24.04’s PHP 8.3 and MariaDB 10.11 packages, and Caddy 2.11. After the seven install steps come the parts most guides skip: file ownership that lets WordPress update itself safely, real cron, caching, backups, sizing by traffic and a troubleshooting matrix.

Key takeaways

  • On Ubuntu 24.04, Ubuntu's own php8.3-fpm and mariadb-server packages meet WordPress's recommended PHP 8.3+ and MariaDB 10.11+; WordPress 7.1 is fully compatible with PHP 8.3, and Ubuntu backports security fixes into it.
  • Check the download against wordpress.org's published SHA-1 file, stream fresh keys and salts from api.wordpress.org into wp-config.php, and run wp core verify-checksums after the install.
  • Give each site its own PHP-FPM pool and system user that owns the files: WordPress then updates itself with direct writes, and listen.owner = caddy avoids the 502 caused by Ubuntu's www-data-owned default socket.
  • Ubuntu's MariaDB listens only on localhost and lets root in by Unix socket, so mysql_secure_installation is unnecessary; Caddy handles HTTPS without certbot, and php_fastcgi makes permalinks work without .htaccess.
  • Size by memory first: PHP workers × their measured size plus MariaDB's 128 MB buffer pool and the OS; one site starts on 2 GB, WooCommerce or several sites on 4 GB.

Which stack should you use: Caddy and PHP-FPM, or Docker?

WordPress needs PHP, a MySQL-compatible database and a web server. Its requirements page recommends PHP 8.3 or greater, MariaDB 10.11 or greater (or MySQL 8.0 or greater) and HTTPS support. It calls Apache and Nginx the most robust choices but adds that “any server that supports PHP and MySQL will do.” Ubuntu 24.04 ships PHP 8.3 and MariaDB 10.11, so its own packages meet the recommendation without third-party repositories.

This guide pairs them with Caddy. WordPress core recognizes Caddy by name (the $is_caddy check in wp-includes/vars.php) and treats it like Nginx, so pretty permalinks work without an .htaccess file. Caddy also obtains and renews certificates itself, so there is no certbot step. Docker is the other route:

FactorNative: Caddy + PHP-FPM + MariaDBDocker: official wordpress + mariadb images
PHP version8.3.6 from Ubuntu, with security fixes backported into itYour pick of image tag, php8.2 to php8.5; latest is PHP 8.3 with Apache
Extra PHP extensionsapt install one packageNot included; Docker’s docs tell you to build your own image FROM theirs
UpdatesUbuntu security updates through apt; WordPress updates itselfWordPress updates itself inside its volume; pull new images for PHP and the database
OverheadNo container runtimeDocker Engine, plus Apache inside the WordPress container
Good fit forOne to a few sites, small plans, admins who want every file in plain sightServers that already run Compose stacks, or sites that want PHP 8.4 or 8.5 today
Stack comparison from WordPress’s requirements page, the Docker Official Image docs for wordpress and Ubuntu 24.04’s package archive (checked October 3, 2026).

The Docker route in one Compose file

If Docker is already on the server (see how to install Docker on a VPS), this Compose file runs the official images. It differs from the example on Docker Hub in two places. It uses MariaDB 11.8 LTS instead of mysql:8.0, which the WordPress hosting handbook lists as reaching end of life in April 2026. And it publishes WordPress on 127.0.0.1 only, because ports Docker publishes on all addresses bypass UFW.

services:
  db:
    image: mariadb:11.8
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: change-me
      MARIADB_RANDOM_ROOT_PASSWORD: "1"
    volumes:
      - db:/var/lib/mysql

  wordpress:
    image: wordpress:7.1-php8.4-apache
    restart: unless-stopped
    depends_on:
      - db
    ports:
      - "127.0.0.1:8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: change-me
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wordpress:/var/www/html

volumes:
  db:
  wordpress:

Replace both change-me values with the same long random string (openssl rand -hex 24 prints one), start it with docker compose up -d, and proxy to it from Caddy on the host with a site block holding a single line, reverse_proxy localhost:8080. Configured through these environment variables, the image writes unique keys and salts and adds the X-Forwarded-Proto handling WordPress needs behind a TLS proxy. The rest of this guide follows the native route.

Before you start: DNS, SSH, the firewall and Caddy

You need a domain, an Ubuntu 24.04 VPS and a non-root user with sudo. Four jobs come first; sibling guides cover each one:

  1. Log in over SSH (connecting to a VPS with SSH) and work through the VPS security checklist: updates, a sudo user, SSH keys and no root login.
  2. Point the domain’s A record, and its AAAA record if you use IPv6, at the server, as in setting the A and AAAA records.
  3. Allow 80/tcp, 443/tcp and 443/udp in UFW, as in opening ports 80 and 443.
  4. Install Caddy from its official apt repository. The package creates the caddy system user that Step 5 hands the PHP socket to, so it has to exist first.

Deploy the server near most of your visitors. HourlyVPS servers run in Istanbul today, with New York coming soon; what server location does for SEO explains why speed matters more than the IP address. Moving an existing site rather than starting fresh? Use the migration steps from shared hosting to copy its files and database in place of the fresh download in Step 3 and the install in Step 6.

How to install WordPress on a VPS running Ubuntu 24.04

Run the seven steps in order as your sudo user. Replace example.com with your domain everywhere.

Step 1: Install PHP 8.3, its extensions and MariaDB

The WordPress hosting team’s Server Environment handbook page sorts PHP extensions into required, highly recommended, cache and fallback groups. This command installs PHP-FPM, the PHP command line (for WP-CLI later), every required and highly recommended extension that Ubuntu packages, two fallbacks, OPcache and MariaDB:

sudo apt update
sudo apt install php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl php8.3-xml php8.3-mbstring php8.3-zip php8.3-intl php8.3-gd php8.3-bcmath php8.3-opcache php-imagick php-igbinary mariadb-server

Note: php8.3-fpm, php8.3-intl, php8.3-zip, php-imagick, php-igbinary and mariadb-server come from Ubuntu’s universe component. Most Ubuntu server images enable it; if apt reports “Unable to locate package”, run sudo add-apt-repository universe and repeat the install.

Extension (handbook group)Ubuntu 24.04 packageNotes
json, hash (required), pcre, openssl, sodium, zlib, filterNone: built into Ubuntu’s PHPNo action needed
mysqli (required)php8.3-mysqlAlso brings mysqlnd and pdo_mysql
curl (highly recommended)php8.3-curlRemote requests, such as update checks
dom, xml, simplexml, xmlreaderphp8.3-xmlOne package for all XML extensions
exif, fileinfo, iconv, socketsphp8.3-commonInstalled automatically as a dependency
mbstring, intl, zip (highly recommended)php8.3-mbstring, php8.3-intl, php8.3-zipzip unpacks plugin, theme and core updates
imagick (highly recommended)php-imagickBuilt against ImageMagick 6.9.12 on 24.04; the handbook recommends ImageMagick 7.1 or later
igbinary (highly recommended)php-igbinaryA faster serializer, used when a cache or setting asks for it
gd and bcmath (fallback)php8.3-gd, php8.3-bcmathGD takes over image work if Imagick is missing
opcache (cache)php8.3-opcacheEnabled by default
apcu, memcached or redis (cache; one is enough)php-apcu or php-redisOnly with a persistent object cache; see caching
Extension groups from the WordPress Hosting Handbook’s Server Environment page; package names and build options from Ubuntu 24.04’s archive (checked October 3, 2026).

Ubuntu enables each extension for both the command line and PHP-FPM as its package installs, so php -m shows the same list FPM will load:

php -m | grep -E -i 'curl|dom|exif|fileinfo|igbinary|imagick|intl|mbstring|mysqli|openssl|zip|opcache'

A word on the version. The handbook recommends PHP 8.4 or later for production and notes that PHP 8.3 moved to security-only support on December 31, 2025, with end of life on December 31, 2027. It also lists WordPress 7.1 as fully compatible with PHP 8.3. Ubuntu keeps 24.04 on PHP 8.3.6 and backports security fixes into it: the current security update, 8.3.6-0ubuntu0.24.04.11, is dated September 2, 2026 in its changelog and patches CVEs published in 2026. If you want PHP 8.4 now without a third-party repository, the Docker route is the simpler way.

Step 2: Create the database and its user

Ubuntu’s MariaDB arrives locked down. The package’s README states that it listens only on localhost, ships no test database or test accounts, and lets the system root user in through Unix-socket authentication. That makes mysql_secure_installation unnecessary; the README warns it “might fail.” Generate a password and create the database with the statements from WordPress’s guide to creating a database:

DB_PASS=$(openssl rand -hex 24)
sudo mariadb -e "CREATE DATABASE wordpress; CREATE USER 'wordpress'@'localhost' IDENTIFIED BY '$DB_PASS'; GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost'; FLUSH PRIVILEGES;"

Keep this shell open: Step 4 writes $DB_PASS into wp-config.php, the only place it needs to live. The database, its user and the system user are all called wordpress here; for a second site, repeat Steps 2 to 7 with another name.

Step 3: Download WordPress and verify the checksum

wordpress.org publishes an MD5 and a SHA-1 checksum next to every download on its releases page. Fetch the archive and its .sha1 file and let sha1sum compare them:

cd /tmp
curl -fLO https://wordpress.org/latest.tar.gz
curl -fLO https://wordpress.org/latest.tar.gz.sha1
echo "$(cat latest.tar.gz.sha1)  latest.tar.gz" | sha1sum -c -

The last line must print latest.tar.gz: OK. On October 3, 2026 the archive was WordPress 7.1.2, about 35 MB. The check proves the file arrived intact and matches what wordpress.org publishes. Both files come from the same server, so it cannot catch a compromised origin; Step 6 adds a second check of every installed file.

Now create the system user that will own the site and run its PHP, unpack WordPress and apply the 755/644 scheme from WordPress’s hardening guide:

sudo useradd --system --user-group --create-home --home-dir /home/wordpress --shell /usr/sbin/nologin wordpress
sudo mkdir -p /var/www
sudo tar -xzf /tmp/latest.tar.gz -C /var/www
sudo chown -R wordpress:wordpress /var/www/wordpress
sudo find /var/www/wordpress -type d -exec chmod 755 {} \;
sudo find /var/www/wordpress -type f -exec chmod 644 {} \;

The wordpress user cannot log in. Its home directory sits outside the web root, so the files WP-CLI caches there can never be downloaded through the website.

Step 4: Create wp-config.php with fresh keys and salts

Copy the sample, fill in the database settings, and swap the eight placeholder keys and salts for a fresh set from the WordPress.org secret-key service, the generator wp-config-sample.php itself points to. The salts stream from the API straight into the file, so no copy is left in /tmp:

cd /var/www/wordpress
sudo -u wordpress cp wp-config-sample.php wp-config.php
sudo -u wordpress sed -i "s/database_name_here/wordpress/; s/username_here/wordpress/; s/password_here/$DB_PASS/" wp-config.php
curl -fsS https://api.wordpress.org/secret-key/1.1/salt/ | sudo sed -i -e "/define( 'AUTH_KEY'/r /dev/stdin" -e "/put your unique phrase here/d" wp-config.php
sudo -u wordpress sed -i "/That's all, stop editing/i define( 'DISALLOW_FILE_EDIT', true );\ndefine( 'DISABLE_WP_CRON', true );" wp-config.php
sudo chmod 400 wp-config.php
sudo grep -c -E "^define\( ?'(AUTH|SECURE_AUTH|LOGGED_IN|NONCE)_(KEY|SALT)'" wp-config.php
sudo -u wordpress php -l wp-config.php

The grep must print 8, one line per key and salt; 0 means nothing was written, so remove the file with sudo rm wp-config.php and run the block again. The salts line runs sed as root because sed running as wordpress cannot open a pipe that belongs to your user, and the GNU sed manual treats an unreadable file as empty, with no error; sed -i keeps the file’s owner. The last command should print No syntax errors detected in wp-config.php.

The two added constants come from the wp-config.php documentation:

  • DISALLOW_FILE_EDIT removes the dashboard’s theme and plugin code editor, which the hardening guide calls “often the first tool an attacker will use if able to login.”
  • DISABLE_WP_CRON stops WordPress from running scheduled tasks during page loads. Step 6 hands them to the system’s cron instead, the pairing WordPress’s WP-Cron docs recommend.

The hardening guide asks for mode 400 or 440 on wp-config.php. Mode 400 is enough here, because PHP runs as the file’s owner.

Step 5: Give the site its own PHP-FPM pool

Ubuntu’s default pool, www, runs PHP as www-data and creates /run/php/php8.3-fpm.sock owned by www-data with mode 0660. Caddy runs as the caddy user, so it cannot open that socket: the classic 502 error on a fresh Caddy and PHP-FPM server. PHP’s FPM configuration docs explain why: on Linux, the socket’s read and write permissions must let the web server connect. Rather than loosening the shared socket, give the site a pool that runs as wordpress and hands its socket to Caddy:

sudo nano /etc/php/8.3/fpm/pool.d/wordpress.conf
[wordpress]
user = wordpress
group = wordpress
listen = /run/php/php8.3-fpm-wordpress.sock
listen.owner = caddy
listen.group = caddy
listen.mode = 0660

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500

php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
  • The process-manager values copy Ubuntu’s www pool, so the site starts with at most 5 PHP workers. Sizing by traffic shows how to change that from your own measurements.
  • pm.max_requests = 500 recycles each worker after 500 requests, which PHP’s docs suggest as a way to work around memory leaks in third-party libraries.
  • The php_admin_value lines raise Ubuntu’s defaults (128M of memory, 2M uploads, 8M request bodies) to the 256M memory limit the handbook recommends and a 64 MB upload limit. Values set this way cannot be overridden with ini_set().

Switch off the default pool so its idle workers stop holding memory, test the configuration and restart PHP-FPM:

sudo mv /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/www.conf.disabled
sudo php-fpm8.3 -t
sudo systemctl restart php8.3-fpm
ls -l /run/php/php8.3-fpm-wordpress.sock

The test should report that the configuration file “test is successful”, and the socket should be listed as srw-rw---- owned by caddy caddy. PHP-FPM loads only files ending in .conf from pool.d, so renaming www.conf is enough to disable it.

Step 6: Install WordPress with WP-CLI before the site goes live

Until WordPress is installed, anyone who reaches /wp-admin/install.php can create its first administrator. Running the installer from the shell with WP-CLI means that page never faces the internet, and you will want WP-CLI anyway for updates, cron and backups. Install it as the WP-CLI handbook recommends, including its optional GPG check:

cd /tmp
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.asc
curl -L https://raw.githubusercontent.com/wp-cli/builds/gh-pages/wp-cli.pgp | gpg --import
gpg --verify wp-cli.phar.asc wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp

Then install WordPress as the site user. Without --admin_password, WP-CLI generates a password and prints it once as Admin password:; --skip-email skips a welcome mail the server cannot send yet. Pick a username other than admin: the hardening guide notes that easily guessed names “are typically subject to attacks first.”

cd /var/www/wordpress
sudo -u wordpress wp core install --url=https://example.com --title="Example Site" --admin_user=yourname [email protected] --skip-email
sudo -u wordpress wp core verify-checksums

wp core verify-checksums compares every core file with the checksums WordPress.org publishes for your version and should end with Success: WordPress installation verifies against checksums. Finally, give WP-Cron to the system scheduler, running due events every 5 minutes as the wordpress user with no HTTP request involved:

echo '*/5 * * * * wordpress /usr/local/bin/wp --path=/var/www/wordpress cron event run --due-now --quiet' | sudo tee /etc/cron.d/wordpress

Step 7: Point Caddy at PHP-FPM with php_fastcgi

Open the Caddyfile and replace the default site with the block below. It is the WordPress example from Caddy’s common patterns page with Ubuntu 24.04’s socket path, plus a browser-cache rule for static files, a redirect from www and the ACME email address from the Caddy guide:

sudo nano /etc/caddy/Caddyfile
{
	email [email protected]
}

example.com {
	root /var/www/wordpress
	encode
	php_fastcgi unix//run/php/php8.3-fpm-wordpress.sock
	file_server

	@blocked path /xmlrpc.php *.sql /wp-content/uploads/*.php
	rewrite @blocked /index.php

	@static path *.css *.js *.png *.jpg *.jpeg *.gif *.webp *.avif *.svg *.woff2 *.ico
	header @static Cache-Control "public, max-age=2592000"
}

www.example.com {
	redir https://example.com{uri} permanent
}

What each part does, per Caddy’s php_fastcgi docs and the patterns page:

  • root sets the site directory. The docs say php_fastcgi should almost always be paired with root and file_server.
  • php_fastcgi sends .php requests to the pool’s socket and rewrites every path that is not a file on disk to index.php, which is exactly how WordPress permalinks work. Because WordPress detects Caddy, Settings → Permalinks saves pretty URLs with no .htaccess.
  • file_server serves CSS, JavaScript, images and uploads directly, without waking PHP.
  • @blocked refuses the XML-RPC endpoint (“often targeted by brute-force attacks”), database dumps and PHP files inside uploads; WordPress answers them with its normal 404 page. Remove /xmlrpc.php from the list if an app or plugin you use needs XML-RPC.
  • @static lets browsers keep static files for 30 days (2,592,000 seconds), using the Cache-Control pattern from Caddy’s header directive docs. WordPress adds a ?ver= query string to the CSS and JavaScript it loads, so updated files still reach visitors.

Format, validate and reload as in reloading Caddy safely, then confirm the site answers over HTTPS:

sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo -u caddy caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://example.com/

Expect HTTP/2 200 once the certificate is in place, which can take a few seconds after the reload. A 502 almost always means Caddy cannot open the socket; see the troubleshooting matrix. Then log in at https://example.com/wp-admin/ with the password from Step 6 and open Tools → Site Health: it should report no missing PHP modules.

File ownership: why one user owns the files and runs PHP

WordPress decides how to write files by testing. Its get_filesystem_method() function (in wp-admin/includes/file.php) creates a temporary file and compares that file’s owner with the owner of WordPress’s own files. Only when they match does it write directly. Otherwise it falls back to FTP or SSH, and the dashboard asks for “Connection Information” every time you update a plugin. That leaves three workable models:

ModelWho owns the filesDashboard updatesTrade-off
Dedicated pool user (this guide)wordpress, which also runs this site’s PHPWork, written directlyPHP can change its own site’s code, but no other site’s; wp-config.php is unreadable to other users
www-data owns everything (most tutorials)www-data, shared by every PHP site on the serverWorkA compromised plugin on one site can write into every other site
Read-only codeYour admin user; only wp-content/uploads writable by PHPBlocked; set DISALLOW_FILE_MODS and update with WP-CLI as the ownerStrongest isolation, but every core, plugin and theme update becomes a manual step
Ownership models for WordPress on a VPS. Permission advice from WordPress’s hardening and file-permissions guides; the direct-write test from WordPress 7.1.2’s source.

Whichever model you pick, never fix a permission error with 777. WordPress’s file-permissions guide explains that world-writable files hand full access to every account on the server, including the web server’s. If the dashboard asks for FTP details after you copied files in by hand, fix ownership instead: sudo chown -R wordpress:wordpress /var/www/wordpress.

Is HTTPS automatic, and what should you cache?

HTTPS is already done. Caddy requested a certificate as soon as the site block loaded and renews it in the background; the Caddy reverse proxy guide covers how that works and what breaks it. WordPress needs nothing more: Caddy’s FastCGI transport sets HTTPS=on for TLS requests, which is what WordPress’s is_ssl() reads. The FORCE_SSL_ADMIN and X-Forwarded-Proto snippets in WordPress’s HTTPS guide are for setups where a proxy talks plain HTTP to WordPress, such as the Docker route. Behind Cloudflare, use the Full (strict) SSL mode; Flexible causes a redirect loop.

Caching in four layers

WordPress’s caching guide and the hosting handbook describe these layers. Add them in this order and stop when the site is fast enough:

  1. OPcache keeps compiled PHP in memory and is on by default with php8.3-opcache. The caching guide says an opcode cache improves PHP’s performance “by many times,” and since WordPress 7.0 Site Health checks for one.
  2. A page cache plugin, such as WP Super Cache, Cache Enabler or W3 Total Cache (the three the guide names), stores rendered pages as static files. The guide says this “can improve performance several hundred times over for fairly static pages.” Logged-in users, carts and checkouts bypass it.
  3. Browser caching is the @static rule from Step 7: repeat visitors reuse CSS, JavaScript and images.
  4. A persistent object cache keeps database query results in Redis, APCu or Memcached between requests. The handbook says only one of the three is needed, and it needs a matching drop-in plugin. Our reasoning: it pays off on WooCommerce and membership sites with many logged-in requests, much less on a small blog that already has a page cache.

How do you keep WordPress updated and backed up?

Updates: four layers, four schedules

LayerUpdated byWhat to do
WordPress coreWordPress itself. Per the upgrade docs, installs created on WordPress 5.6 or later get minor and major automatic updates by defaultNothing; they succeed because PHP owns the files. Define WP_AUTO_UPDATE_CORE as 'minor' if you want to test major releases first
Plugins and themesAutomatic only in special cases the WordPress.org security team chooses, unless you enable auto-updatesTurn on auto-updates per plugin, or run the WP-CLI pass below every week
PHP, MariaDB and Ubuntuunattended-upgrades, which installs security updates from Ubuntu’s own archiveConfirm it is on (checklist step 6)
CaddyIts own apt repository, which unattended-upgrades skips by defaultRun sudo apt update && sudo apt upgrade now and then
Update channels for this stack (checked October 3, 2026).
cd /var/www/wordpress
sudo -u wordpress wp core update
sudo -u wordpress wp core update-db
sudo -u wordpress wp plugin update --all
sudo -u wordpress wp theme update --all
sudo -u wordpress wp core verify-checksums
sudo -u wordpress wp plugin verify-checksums --all

The two verify-checksums commands catch modified files, a common sign of a compromised site; the plugin check covers plugins hosted on WordPress.org. One support detail: mariadb-server and php8.3-fpm sit in Ubuntu’s universe component. Both had current security updates on October 3, 2026, but Canonical’s standard 5-year security maintenance is promised for main; Ubuntu Pro extends that commitment to universe.

Backups: the database plus four folders

WordPress’s backup guide splits a backup into the database (posts, comments, settings) and the files; you need both to restore. Core files can be downloaded again, so the files that matter are wp-content and wp-config.php. Add the Caddyfile and the pool file and a rebuild is quick. This line dumps the database every night at 03:30 server time; --single-transaction is passed through to the mysqldump tool for a consistent dump without locking InnoDB tables:

sudo -u wordpress mkdir -p /home/wordpress/backups
echo '30 3 * * * wordpress /usr/local/bin/wp --path=/var/www/wordpress db export /home/wordpress/backups/wordpress.sql --single-transaction --quiet' | sudo tee -a /etc/cron.d/wordpress

Then get it off the server: copy /var/www/wordpress, /home/wordpress/backups, /etc/caddy and /etc/php/8.3/fpm/pool.d to storage outside this VPS, such as an S3-compatible bucket, with an encrypting backup tool like restic; our VPS backup guide with restic walks through the setup and a restore drill. WordPress’s guide suggests keeping at least 3 to 5 recent backups in different locations.

Tip: Test a restore before you need one. Deploy an hourly VPS, restore the latest backup with wp db import, confirm it with wp db check and wp post list --format=count, then delete the server. Two hours on Quartz Q2 cost $0.04. Take a provider snapshot before major updates too: a snapshot is a quick undo, not a backup.

WordPress security on a VPS: what this setup already covers

The server baseline (SSH keys, no root login, UFW, automatic security updates) lives in the VPS security checklist. On top of it, the install steps above already apply most of WordPress’s hardening guide:

RiskCovered byWhere
PHP code edited through a stolen admin loginDISALLOW_FILE_EDITStep 4
wp-config.php read by other accountsMode 400, owned by the pool userStep 4
XML-RPC brute force, PHP uploaded into uploads, exposed .sql dumpsThe @blocked rule in CaddyStep 7
Database reachable from the internetMariaDB listens on localhost only (Ubuntu default)Step 2
One compromised site spreading to othersOne pool and one user per siteStep 5
Guessable admin username--admin_user other than adminStep 6
Tampered core or plugin fileswp core verify-checksums, wp plugin verify-checksumsUpdates
Hardening measures mapped to WordPress’s hardening guide (last updated January 7, 2026).

Three things stay with you. Keep plugins few and current: “if you are not using a specific plugin, delete it from the system,” says the hardening guide. Use a strong, unique admin password and turn on two-factor login. And never open port 3306 or put phpMyAdmin on a public hostname; reach the database through an SSH tunnel.

Email: why password resets never arrive

A fresh server has no mail transport, so WordPress’s password-reset and notification emails go nowhere. New HourlyVPS servers also start with outbound port 25 closed. Send mail through a transactional email provider with an SMTP or API plugin instead, and publish the SPF and DKIM records the provider gives you, which receiving servers check.

How much VPS does WordPress need? Sizing by traffic

WordPress publishes no RAM minimum. The documented figures you can build on are Ubuntu 24.04’s 1 GB minimum for cloud images (3 GB suggested), MariaDB’s default InnoDB buffer pool of 128 MiB, and the handbook’s PHP memory_limit tiers: 128M minimum, 256M recommended, and 512M or more for WooCommerce, page builders and other memory-heavy sites. The limit is a ceiling per PHP worker, so the worst case for PHP is pm.max_children × memory_limit: 5 × 256 MB = 1.25 GB with this guide’s pool.

Real workers usually stay below their ceiling, so measure once the site has its theme, plugins and some content. After a few minutes of browsing the site and the dashboard, average the PHP-FPM processes’ resident memory:

ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$NF; n++} END {printf "%d processes, %.0f MiB average\n", n, s/n/1024}'

Then size the pool from the memory you can spare. Our rule of thumb: (RAM − about 1 GB for Ubuntu, MariaDB and Caddy, matching Ubuntu’s cloud-image minimum) ÷ average worker size. A worked example with an assumed 80 MiB per worker on a 4 GB server: 3,072 ÷ 80 ≈ 38 workers fit in memory, so CPU, not RAM, becomes the limit. Raise pm.max_children when evidence says so: PHP-FPM writes “server reached pm.max_children setting” to /var/log/php8.3-fpm.log whenever requests had to wait for a free worker.

Traffic is smaller than it sounds. 100,000 page views a month average 0.04 views per second (100,000 ÷ 2,592,000 seconds in 30 days); 1,000,000 a month average 0.39 per second. Peaks are what count, and only uncached views reach PHP. A rough ceiling for uncached pages is vCPUs ÷ CPU seconds per page. If a page needs 0.2 seconds of CPU, 1 vCPU serves about 5 uncached pages a second, or 18,000 an hour. To estimate your own figure, run this on the server a few times against a page that skips the page cache and take the lowest result:

curl -o /dev/null -s -w '%{time_starttransfer}\n' https://example.com/
Your siteStarting planMonthly capReasoning
Staging copy, test installs, a personal blog with a page cacheQuartz Q1: 1 vCPU, 1 GB, 25 GB NVMe$5.00/month capMeets Ubuntu’s 1 GB cloud-image minimum and no more; set pm.max_children to 2 or 3 and add a swap file
One blog or business siteQuartz Q2: 1 vCPU, 2 GB, 50 GB$10.00/month capAbout 1 GB left for PHP above Ubuntu’s minimum; check the default 5 workers with the ps command
WooCommerce, a membership site, or 3 to 5 small sitesQuartz Q4: 2 vCPU, 4 GB, 80 GB$15.00/month capCarts, checkouts and logged-in pages skip the page cache; meets Ubuntu’s 3 GB suggestion. For the handbook’s 512M WooCommerce tier, raise memory_limit in the pool file: php_admin_value stops WordPress from raising it
A busy store or many logged-in users at onceChrono C8: 2 dedicated vCPU, 8 GB, 100 GB$35.00/month capUncached traffic is CPU-bound; dedicated vCPUs keep response times steady, and the RAM fits a larger buffer pool
Starting points from documented minimums and the arithmetic above, not from benchmarks. The monthly cap is the most a server on that plan costs in one billing period. Measure, then resize.

Before launch, load-test the site with k6 from a second, short-lived server to replace the estimate with a measured number. Media-heavy sites should also watch traffic: Istanbul plans include the monthly traffic allowance listed for each plan, prorated for a server that exists for part of a billing period.

WordPress on Caddy not working? Troubleshooting matrix

SymptomLikely causeCheck and fix
502 Bad GatewayCaddy cannot open the PHP socket: wrong path, PHP-FPM stopped, or the socket belongs to www-datals -l /run/php/ and systemctl status php8.3-fpm; journalctl -u caddy shows “permission denied” or “no such file”. Set listen.owner = caddy in the pool
“Error establishing a database connection”Wrong DB_NAME, DB_USER or DB_PASSWORD, or MariaDB stoppedsudo -u wordpress wp db check and systemctl status mariadb
Plugin updates ask for FTP “Connection Information”Files not owned by the pool user, so WordPress cannot write directlysudo chown -R wordpress:wordpress /var/www/wordpress
“The uploaded file exceeds the upload_max_filesize directive”PHP’s 2M default still appliesRaise upload_max_filesize and post_max_size in the pool, then sudo systemctl restart php8.3-fpm
Too many redirectsCloudflare set to Flexible, or the site URL saved as http://Use Full (strict); check sudo -u wordpress wp option get home
Scheduled posts publish late or neverDISABLE_WP_CRON is set but the cron file is missingcat /etc/cron.d/wordpress and sudo -u wordpress wp cron event list
Site Health lists a missing modulePackage not installed, or PHP-FPM not restarted after installing itInstall the php8.3- package, then sudo systemctl restart php8.3-fpm
Password-reset emails never arriveNo mail transport; port 25 closedAn SMTP or API plugin with a mail provider (details)
No certificate, or a TLS errorDNS not pointing here, or ports 80/443 closedCaddy’s troubleshooting matrix
Common failures on a Caddy, PHP-FPM and MariaDB WordPress server, with the command that confirms each.

What does WordPress on a VPS cost per month?

WordPress itself is free to download; the server is the bill. Every HourlyVPS server is billed by the hour with a monthly cap, so an always-on site never costs more than its plan’s monthly price in a billing period (one month from your order date): $10.00 on Quartz Q2 and $15.00 on Quartz Q4. That cap is what a monthly VPS means here: no contract and no prepaid term. Every server comes with one dedicated IPv4 address and IPv6, included in the plan price.

Quartz Q2 (1 vCPU, 2 GB RAM): a two-hour install rehearsal, a staging day, a staging week or a live month, billed by the hour up to the monthly cap
DurationHours on the meterCost $0.02/hour · cap $10.00/monthNote
2 hours2$0.04
1 day24$0.48
7 days168$3.36
30 days720$10.00Capped at the monthly price
Charges stop at $10.00 after 500 hours (about 20.8 days) in a billing period; the rest of that period is free.

Hourly billing earns its keep around the live site. Rehearse this guide before you touch production, test a major WordPress or PHP update on a restored copy, or run a load test, then delete the server. A three-hour update rehearsal on Q2 costs $0.06. Before deleting a test server, run the checklist before you delete a VPS so no data you need goes with it.

Billing: A new server starts with an initial credit, paid at checkout, that funds its own prepaid balance; its usage is deducted from that balance every hour. A stopped server is still billed, because its vCPU, memory, disk and IP addresses stay reserved for you; only deleting the server stops billing. How hourly billing works walks through the balance, top-ups and the cap, the hourly vs monthly guide shows why a capped hourly server never costs more than a monthly one, and the pricing page lists every plan.

Deploy this setup

Host a WordPress site on Caddy with automatic HTTPS

Quartz Q2 · 1 shared vCPU · 2 GB RAM · 50 GB NVMe · Istanbul

  • Per hour$0.02/hour
  • Per day (24 h)$0.48/day
  • Monthly cap$10.00/monthFor this job
Deploy Quartz Q2

Starts with a $5 initial credit, which goes into the server’s balance and pays for its hours.

Billed by the hour, never more than $10.00 per billing period. Delete the server and billing stops.

FAQ

Can I install WordPress on any VPS?

Yes, on any Linux VPS with root access: WordPress needs PHP, a MySQL-compatible database and a web server, and recommends PHP 8.3 or newer and MariaDB 10.11 or newer. Ubuntu 24.04's own packages meet both, so no third-party repository is needed.

How much RAM does WordPress need on a VPS?

WordPress sets no minimum. Ubuntu 24.04 cloud images need 1 GB, and the hosting handbook recommends a 256M PHP memory limit per worker, so 2 GB is a sensible start for one site and 4 GB for WooCommerce or several sites; measure your PHP workers and resize from there.

Do I need cPanel or another control panel to run WordPress on a VPS?

No. A control panel is optional; this setup needs only SSH, apt and WP-CLI, and leaves all of the server's RAM to WordPress, MariaDB and Caddy.

How many WordPress sites can one VPS host?

As many as its RAM and CPU allow. Give each site its own database, PHP-FPM pool, system user and Caddy site block, and budget each pool's workers times their measured memory.

Should I use Nginx or Apache instead of Caddy for WordPress?

WordPress.org calls Apache and Nginx the most robust choices but says any server that runs PHP and MySQL works. WordPress core detects Caddy and enables pretty permalinks for it, and Caddy manages HTTPS certificates itself, so it needs less configuration for a single server.

Is PHP 8.3 still OK for WordPress in 2026?

Yes. WordPress 7.1 is fully compatible with PHP 8.3, which upstream supports with security fixes until December 31, 2027, and Ubuntu 24.04 backports security patches into its PHP 8.3 package. The hosting handbook recommends PHP 8.4 or later for new production setups.

Can I move an existing WordPress site to a VPS instead of starting fresh?

Yes. Copy the old site's files and export its database, then use them in place of the fresh download and the WP-CLI install: import the dump with wp db import, keep the same domain, and switch DNS to the VPS once the copy works.

Sources

  1. WordPress requirementsWordPress.org · wordpress.org · checked
  2. Server Environment (PHP extensions, memory limits, database versions)WordPress Hosting Team Handbook · make.wordpress.org · checked
  3. How to install WordPressWordPress Developer Resources · developer.wordpress.org · checked
  4. Hardening WordPressWordPress Developer Resources · developer.wordpress.org · checked
  5. Upgrading WordPress: configuring automatic background updatesWordPress Developer Resources · developer.wordpress.org · checked
  6. Installing WP-CLIWP-CLI Handbook · make.wordpress.org · checked
  7. php_fastcgi directiveCaddy Documentation · caddyserver.com · checked
  8. Common Caddyfile patterns (PHP and WordPress)Caddy Documentation · caddyserver.com · checked
  9. FPM configurationPHP Manual · php.net · checked
  10. mariadb-server README.Debian (Ubuntu 24.04 package)Ubuntu (Launchpad) · git.launchpad.net · checked
All posts