Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Wordpress Ubuntu
GUIDEInstall Guides

How to Install WordPress on Ubuntu 24.04 — LEMP Stack, WP-CLI, Redis, FastCGI Cache

29 min read

How to Install WordPress on Ubuntu 24.04 — LEMP Stack, WP-CLI, Redis, FastCGI Cache

WordPress powers more than 43% of every website on the internet, and the overwhelming majority of those sites run on some flavour of a Linux web stack. When you outgrow a shared-hosting plan — because a plugin is banned, a resource cap throttles a traffic spike, a support reply takes three days, or the yearly renewal silently tripled — moving to your own VPS is the permanent fix. You get the full platform, you own the database, and the price stops climbing. This tutorial walks you through a production-hardened WordPress install on a fresh Ubuntu 24.04 VPS using the LEMP stack (Nginx, MariaDB, PHP 8.3-FPM), WP-CLI, Redis object caching, Nginx FastCGI page caching, a free Let's Encrypt TLS certificate, and a UFW firewall baseline.

Just want a VPS? Deploy an Ubuntu 24.04 server in under 60 seconds. Launch a Starter VPS now and follow along — the whole install takes about 45 minutes start to finish.

Table of Contents

  • What is WordPress?
  • Why Self-Host WordPress vs. WP Engine, Kinsta or Bluehost?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Install the LEMP Stack
  • Step 3: Secure MariaDB and Create the Database
  • Step 4: Install WP-CLI
  • Step 5: Download WordPress and Generate wp-config.php
  • Step 6: Configure Nginx with FastCGI Cache
  • Step 7: Install Redis and Enable the Object Cache Plugin
  • Step 8: Issue a Let's Encrypt TLS Certificate
  • Step 9: Harden the Server with UFW
  • Verify the Installation
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is WordPress?

    WordPress is an open-source content management system released in 2003 and now maintained by the WordPress Foundation and thousands of contributors. It began life as blogging software but has matured into a general-purpose CMS that runs everything from single-author blogs to Fortune 500 marketing sites, news publishers, online stores (via WooCommerce), membership sites, learning platforms, and headless back-ends feeding React and Next.js front-ends over the REST API.

    The codebase is PHP with a MySQL-compatible database, a templating layer called "themes", and a plugin architecture that lets you extend or replace nearly any behaviour without touching core. The official plugin directory at wordpress.org/plugins lists more than 60,000 free plugins; the theme directory lists another 12,000. Commercial plugins from vendors such as WooCommerce, Gravity Forms, Yoast SEO, and The Events Calendar add another layer on top. For developers, developer.wordpress.org is the canonical reference for the plugin API, REST API, block editor (Gutenberg), theme handbook, coding standards, and WP-CLI.

    The ecosystem around self-hosted WordPress is the largest of any CMS by a wide margin. Integrations exist for every analytics tool, email provider, payment gateway, CDN, search engine, and backup destination you can name. When a new technology lands — headless architectures, AI content assistants, Core Web Vitals, full-site editing — the plugin ecosystem catches up within weeks. That breadth is impossible on a closed platform, and it is the reason 43% of the public web runs on WordPress.

    Why Self-Host WordPress vs. WP Engine, Kinsta or Bluehost?

    Managed WordPress hosts remove some of the sysadmin work but charge heavily for the privilege, ban plugins they dislike, cap your traffic, and leave you nowhere to go when a problem falls outside their playbook. A self-hosted install on a VPS is the same open-source software with none of those restrictions.

    Cost comparison at equivalent traffic. The quoted prices below are the visible first-year rates; most managed hosts double the price on renewal.

    HostEntry planMonthly visit capSitesStoragePrice (renewal)
    CloudCore Starter (self-host)2 vCPU / 4 GB / 60 GB NVMeUnmeteredUnlimited60 GBEUR 7.99 / EUR 7.99
    WP Engine StartupManaged WordPress25,000 visits1 site10 GBUSD 25 / USD 30
    Kinsta StarterManaged WordPress35,000 visits1 site10 GBUSD 35 / USD 35
    Bluehost Online StoreShared Apache"Unmetered"*1 site40 GBUSD 9.95 / USD 24.95
    SiteGround GrowBigShared Nginx~100,000 visitsUnlimited20 GBUSD 4.99 / USD 24.99
    *Bluehost's "unmetered" shared plans enforce CPU and I/O throttling once sustained usage crosses an undocumented threshold. Every managed host above will charge overage fees or force you to upgrade the instant you cross the visit cap.

    Beyond price, self-hosting on your own VPS gives you:

    • Unlimited plugins and themes. WP Engine and Kinsta maintain plugin deny-lists that ban popular caching, backup, and security plugins on the grounds that they conflict with the host's proprietary stack. On your own VPS there is no deny-list — install any plugin from any source.
    • Full data ownership. Every post, every upload, every user record, every WooCommerce order sits in a MariaDB database and an uploads folder on your server. Dump it, mirror it, move it, delete it on your own schedule.
    • No traffic caps and no overage fees. A flash of viral traffic on a managed host triggers either throttling or a surprise invoice. On a VPS, the fixed monthly price stays fixed.
    • SSH, WP-CLI, cron and root. Managed hosts sandbox or remove these. Self-hosting gives you full access to debug, automate, and script anything.
    • Run other services alongside WordPress. Drop Nginx reverse-proxy routes for a staging site, add a Ghost newsletter, a Redis cache, or a Caddy edge server on the same machine. Managed WordPress hosts allow only WordPress.
    • Customizable PHP, MariaDB and Nginx tuning. Raise max_execution_time, tune innodb_buffer_pool_size, enable http2 and Brotli — impossible on shared hosting.
    For any site pushing more than a thousand visits a day or running WooCommerce, a self-hosted VPS is both cheaper and faster than the managed alternatives.

    Prerequisites

    Before you start, you will need:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access.
    • A domain name (for example example.com) with an A record — and ideally an AAAA record — pointing to your VPS IP. Let's Encrypt requires a real DNS name.
    • SSH access to the server.
    • At least 2 GB of RAM for a single low-traffic site; 4 GB recommended for any site running WooCommerce or a heavy page builder.
    • At least 20 GB of free disk space for the OS, MariaDB, WordPress core, and a modest uploads directory; 60 GB+ if you plan to host media-heavy content.
    Recommended Plan: CloudCore Starter
    >
    For a single production WordPress site with Redis, FastCGI cache and daily backups, the CloudCore Starter VPS is the right fit:
    >
    - 2 vCPU cores
    - 4 GB RAM
    - 60 GB NVMe SSD
    - Unmetered bandwidth
    - EUR 7.99/month
    >
    Step up to the Professional plan (4 vCPU / 8 GB) once you add WooCommerce, membership plugins, or more than one site.

    Connect to the server:

    bash
    ssh root@your-server-ip

    Throughout this guide replace example.com with your actual domain and your-server-ip with your server's public address.

    Step 1: Update System Packages

    Always start a fresh server by applying every pending security patch and rebooting if the kernel was upgraded.

    bash
    apt update && apt upgrade -y

    Expected output (abbreviated):

    text
    Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
    Get:2 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
    Reading package lists... Done
    Building dependency tree... Done
    Calculating upgrade... Done
    42 packages will be upgraded.

    If the kernel, systemd, or glibc was updated, reboot now:

    bash
    reboot

    Wait one minute and reconnect over SSH.

    Set the hostname and timezone:

    bash
    hostnamectl set-hostname example.com
    timedatectl set-timezone UTC

    Step 2: Install the LEMP Stack

    The LEMP acronym stands for Linux + Engine-X (Nginx) + MariaDB (or MySQL) + PHP. Ubuntu 24.04 ships with packages recent enough for a modern WordPress install: Nginx 1.24, MariaDB 10.11, and PHP 8.3. If you want a deeper dive on the web server itself see the dedicated Nginx install guide; for the database layer see the MariaDB install guide.

    Install all three components and the PHP extensions WordPress requires in a single apt command:

    bash
    apt install -y \
      nginx \
      mariadb-server \
      php8.3-fpm \
      php8.3-mysql \
      php8.3-curl \
      php8.3-gd \
      php8.3-mbstring \
      php8.3-xml \
      php8.3-zip \
      php8.3-intl \
      php8.3-imagick \
      php8.3-bcmath \
      php8.3-soap \
      php8.3-redis \
      php8.3-opcache \
      unzip curl wget

    What each PHP extension does:

    • php8.3-mysql — communicates with MariaDB.
    • php8.3-curl — powers WordPress HTTP API, plugin update checks and REST calls.
    • php8.3-gd / php8.3-imagick — resize and process uploaded images (Imagick is faster and supports more formats).
    • php8.3-mbstring — multi-byte string handling for non-ASCII content.
    • php8.3-xml — required for oEmbed, RSS, SOAP and XML-RPC.
    • php8.3-zip — needed for plugin and theme installs from the admin dashboard.
    • php8.3-intl — locale-aware sorting and number formatting.
    • php8.3-bcmath — arbitrary-precision math used by WooCommerce and tax plugins.
    • php8.3-redis — PHP client for the Redis object cache.
    • php8.3-opcache — bytecode cache, essential for PHP performance.
    Enable and start the services:

    bash
    systemctl enable --now nginx mariadb php8.3-fpm

    Confirm each is running:

    bash
    systemctl is-active nginx mariadb php8.3-fpm

    Expected output:

    text
    active
    active
    active

    Tune OPcache and PHP-FPM

    Open the PHP-FPM php.ini and raise memory and OPcache limits:

    bash
    nano /etc/php/8.3/fpm/php.ini

    Set the following values (search for each key and update):

    ini
    memory_limit = 256M
    upload_max_filesize = 64M
    post_max_size = 64M
    max_execution_time = 120
    max_input_vars = 3000

    [opcache] opcache.enable = 1 opcache.memory_consumption = 256 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 opcache.validate_timestamps = 1 opcache.revalidate_freq = 2

    Restart PHP-FPM:

    bash
    systemctl restart php8.3-fpm

    Step 3: Secure MariaDB and Create the Database

    Ubuntu 24.04 installs MariaDB with a permissive default configuration. Run the built-in hardener:

    bash
    mysql_secure_installation

    Answer the prompts:

    • Enter current password for root: Press Enter (there is none yet).
    • Switch to unix_socket authentication: n (keep password auth so you can script later).
    • Change the root password: Y, then set a strong password.
    • Remove anonymous users: Y.
    • Disallow root login remotely: Y.
    • Remove test database: Y.
    • Reload privilege tables: Y.
    Now create the WordPress database and user. Log in as root:

    bash
    mysql -u root -p

    Run the following SQL, replacing SET-A-STRONG-PASSWORD-HERE with a long random string:

    sql
    CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'SET-A-STRONG-PASSWORD-HERE';
    GRANT ALL PRIVILEGES ON wordpress.* TO 'wp_user'@'localhost';
    FLUSH PRIVILEGES;
    EXIT;

    utf8mb4 is critical — it is the only MySQL/MariaDB charset that correctly stores four-byte characters like emoji and the full CJK range. WordPress has required it since 4.2.

    Step 4: Install WP-CLI

    WP-CLI is the official WordPress command-line tool. Anything you can do in the wp-admin dashboard you can do faster from the terminal: install and download core, create users, run database search/replace, enable plugins, manage multisite networks, and scaffold themes.

    Download the phar archive, verify it, and move it into your PATH:

    bash
    curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
    chmod +x wp-cli.phar
    mv wp-cli.phar /usr/local/bin/wp

    Verify:

    bash
    wp --info

    Expected output (abbreviated):

    text
    OS:     Linux 6.8.0-45-generic
    Shell:  /bin/bash
    PHP binary:     /usr/bin/php8.3
    PHP version:    8.3.6
    WP-CLI root dir:        phar://wp-cli.phar/vendor/wp-cli/wp-cli
    WP-CLI version: 2.11.0

    Step 5: Download WordPress and Generate wp-config.php

    Create the web root, set ownership, and switch to it:

    bash
    mkdir -p /var/www/example.com
    chown -R www-data:www-data /var/www/example.com
    cd /var/www/example.com

    Download the latest WordPress release as the www-data user so every file has the correct ownership from the start:

    bash
    sudo -u www-data wp core download --locale=en_US

    Expected output:

    text
    Downloading WordPress 6.7.1 (en_US)...
    md5 hash verified: [hash]
    Success: WordPress downloaded.

    Generate wp-config.php. WP-CLI fetches fresh cryptographic salt keys from the api.wordpress.org/secret-key endpoint automatically — you never need to copy-paste them from a web form:

    bash
    sudo -u www-data wp config create \
      --dbname=wordpress \
      --dbuser=wp_user \
      --dbpass='SET-A-STRONG-PASSWORD-HERE' \
      --dbhost=localhost \
      --dbprefix=wp_

    Complete the install non-interactively:

    bash
    sudo -u www-data wp core install \
      --url=https://example.com \
      --title="My WordPress Site" \
      --admin_user=admin \
      --admin_password='SET-A-DIFFERENT-STRONG-PASSWORD' \
      [email protected]

    Expected output:

    text
    Success: WordPress installed successfully.

    Harden wp-config.php Further

    Open wp-config.php and add the following before the / That's all, stop editing! / line:

    php
    // Disable file editing from wp-admin (prevents shell execution if an admin account is compromised)
    define('DISALLOW_FILE_EDIT', true);

    // Force SSL on admin and login define('FORCE_SSL_ADMIN', true);

    // Automatic updates for minor releases only define('WP_AUTO_UPDATE_CORE', 'minor');

    // Limit post revisions define('WP_POST_REVISIONS', 5);

    // Empty trash after 7 days define('EMPTY_TRASH_DAYS', 7);

    // Move wp-content uploads path check define('WP_MEMORY_LIMIT', '256M'); define('WP_MAX_MEMORY_LIMIT', '512M');

    // Redis object cache (plugin installed in Step 7) define('WP_CACHE', true); define('WP_REDIS_HOST', '127.0.0.1'); define('WP_REDIS_PORT', 6379); define('WP_REDIS_DATABASE', 0); define('WP_REDIS_PREFIX', 'example_com_');

    Lock the permissions so only the web user can read it:

    bash
    chmod 640 /var/www/example.com/wp-config.php
    chown www-data:www-data /var/www/example.com/wp-config.php

    Verify the Salt Keys Look Right

    Open wp-config.php and confirm you see eight unique blocks with random 64-character values — not the placeholder text put your unique phrase here:

    php
    define('AUTH_KEY',         'J!7{W3#Hc...redacted...x$VD');
    define('SECURE_AUTH_KEY',  'Pz9,*d-Y)...redacted...Z&eN2');
    define('LOGGED_IN_KEY',    ' ...redacted... ');
    define('NONCE_KEY',        ' ...redacted... ');
    define('AUTH_SALT',        ' ...redacted... ');
    define('SECURE_AUTH_SALT', ' ...redacted... ');
    define('LOGGED_IN_SALT',   ' ...redacted... ');
    define('NONCE_SALT',       ' ...redacted... ');</code></pre></div>

    If they ever look wrong, regenerate them with:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo -u www-data wp config shuffle-salts</code></pre></div>

    Step 6: Configure Nginx with FastCGI Cache

    FastCGI cache stores fully rendered HTML at the Nginx layer so anonymous visitors never reach PHP. Combined with Redis object cache (Step 7) you have a two-layer strategy that handles thousands of requests per second on modest hardware.

    Remove the default server block:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">rm /etc/nginx/sites-enabled/default</code></pre></div>

    Create a cache path config file:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">nano /etc/nginx/conf.d/fastcgi-cache.conf</code></pre></div>

    Contents:

    <div class="code-block" data-lang="nginx"><div class="code-block__header"><span class="code-block__lang">nginx</span></div><pre><code class="language-nginx">fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g; fastcgi_cache_key &quot;$scheme$request_method$host$request_uri&quot;; fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503; fastcgi_ignore_headers Cache-Control Expires Set-Cookie;</code></pre></div>

    Create the WordPress server block:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">nano /etc/nginx/sites-available/example.com.conf</code></pre></div>

    Contents:

    <div class="code-block" data-lang="nginx"><div class="code-block__header"><span class="code-block__lang">nginx</span></div><pre><code class="language-nginx">server { listen 80; listen [::]:80; server_name example.com www.example.com; return 301 https://example.com$request_uri; }

    server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com www.example.com;

    root /var/www/example.com; index index.php;

    # TLS cert paths set by certbot in Step 8 # ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    client_max_body_size 64m;

    # Security headers add_header X-Frame-Options &quot;SAMEORIGIN&quot; always; add_header X-Content-Type-Options &quot;nosniff&quot; always; add_header Referrer-Policy &quot;strict-origin-when-cross-origin&quot; always; add_header Permissions-Policy &quot;geolocation=(), microphone=(), camera=()&quot; always;

    # Define cache skip conditions set $skip_cache 0; if ($request_method = POST) { set $skip_cache 1; } if ($query_string != &quot;&quot;) { set $skip_cache 1; } if ($request_uri ~ &quot;/wp-admin/|/xmlrpc.php|wp-..php|/feed/|index.php|sitemap(_index)?.xml&quot;) { set $skip_cache 1; } if ($http_cookie ~* &quot;comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in&quot;) { set $skip_cache 1; }

    location / { try_files $uri $uri/ /index.php?$args; }

    location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; fastcgi_cache WORDPRESS; fastcgi_cache_valid 200 301 302 60m; add_header X-FastCGI-Cache $upstream_cache_status; }

    # Block hidden files and common PHP execution in uploads location ~ /\. { deny all; } location ~ /wp-content/uploads/.\.php$ { deny all; }

    # Static asset caching location ~* \.(jpg|jpeg|png|gif|webp|avif|ico|css|js|woff2?)$ { expires 30d; access_log off; add_header Cache-Control &quot;public, max-age=2592000, immutable&quot;; } }</code></pre></div>

    Enable the site and test the config:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/ nginx -t</code></pre></div>

    Expected output:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration test is successful</code></pre></div>

    Reload Nginx:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">systemctl reload nginx</code></pre></div>

    After Step 8 adds the TLS certificate, uncomment the two ssl_certificate lines.

    Step 7: Install Redis and Enable the Object Cache Plugin

    Redis is an in-memory key-value store that caches database query results, transients, and WordPress options. On a busy admin dashboard or WooCommerce cart (both bypass the FastCGI HTML cache) Redis reduces page generation time from hundreds of milliseconds to a handful. For a full deep dive see the Redis install guide.

    Install redis-server (the php8.3-redis extension is already installed from Step 2):

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">apt install -y redis-server</code></pre></div>

    Make Redis listen only on localhost (default in Ubuntu 24.04, but verify):

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">grep -E &quot;^bind|^protected-mode&quot; /etc/redis/redis.conf</code></pre></div>

    Expected output:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">bind 127.0.0.1 -::1 protected-mode yes</code></pre></div>

    Set the memory policy so Redis evicts least-recently-used keys when full:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sed -i 's/^# maxmemory .*/maxmemory 256mb/' /etc/redis/redis.conf sed -i 's/^# maxmemory-policy .*/maxmemory-policy allkeys-lru/' /etc/redis/redis.conf</code></pre></div>

    Restart Redis and enable it at boot:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">systemctl enable --now redis-server systemctl restart redis-server redis-cli ping</code></pre></div>

    Expected output:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">PONG</code></pre></div>

    Install and activate the Redis Object Cache plugin via WP-CLI:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">cd /var/www/example.com sudo -u www-data wp plugin install redis-cache --activate sudo -u www-data wp redis enable</code></pre></div>

    Expected output:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Success: Object cache enabled.</code></pre></div>

    Verify the cache is serving hits:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo -u www-data wp redis status</code></pre></div>

    Expected output:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Status: Connected Client: PhpRedis (v6.0.2) Host: 127.0.0.1 Port: 6379 Database: 0</code></pre></div>

    Step 8: Issue a Let's Encrypt TLS Certificate

    Install Certbot and its Nginx plugin:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">apt install -y certbot python3-certbot-nginx</code></pre></div>

    Request the certificate. Certbot will automatically detect your Nginx server block, add the ssl_certificate directives, and configure an HTTP -> HTTPS redirect:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">certbot --nginx -d example.com -d www.example.com \ --non-interactive --agree-tos --email [email protected] --redirect</code></pre></div>

    Expected output (abbreviated):

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem This certificate expires on 2026-07-15. Successfully deployed certificate for example.com to /etc/nginx/sites-enabled/example.com.conf Your existing certificate has been successfully renewed.</code></pre></div>

    Certbot installs a systemd timer that renews within 30 days of expiry. Verify:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">systemctl status certbot.timer certbot renew --dry-run</code></pre></div>

    Reload Nginx one last time:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">systemctl reload nginx</code></pre></div>

    Visit https://example.com — you should see the default WordPress theme front page served over TLS.

    Step 9: Harden the Server with UFW

    UFW (Uncomplicated Firewall) is the default Ubuntu firewall. Lock the server down so only SSH, HTTP, and HTTPS are reachable from the outside.

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">ufw default deny incoming ufw default allow outgoing ufw allow OpenSSH ufw allow &quot;Nginx Full&quot; ufw --force enable</code></pre></div>

    Check the result:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">ufw status verbose</code></pre></div>

    Expected output:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Status: active Default: deny (incoming), allow (outgoing), disabled (routed)

    To Action From -- ------ ---- OpenSSH ALLOW IN Anywhere Nginx Full ALLOW IN Anywhere OpenSSH (v6) ALLOW IN Anywhere (v6) Nginx Full (v6) ALLOW IN Anywhere (v6)</code></pre></div>

    Nginx Full opens both 80 and 443. Redis (6379) and MariaDB (3306) stay bound to localhost and are never exposed to the internet.

    Verify the Installation

    Run a final health check across every layer:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash"># Service status systemctl is-active nginx mariadb php8.3-fpm redis-server

    WordPress sanity via WP-CLI

    cd /var/www/example.com sudo -u www-data wp core version sudo -u www-data wp option get siteurl sudo -u www-data wp plugin list sudo -u www-data wp redis status

    FastCGI cache hit check

    curl -I https://example.com | grep -i x-fastcgi-cache curl -I https://example.com | grep -i x-fastcgi-cache</code></pre></div>

    On the first curl request you should see X-FastCGI-Cache: MISS, on the second X-FastCGI-Cache: HIT. That confirms the Nginx page cache is active.

    Log in to wp-admin at https://example.com/wp-login.php, install a theme, install Yoast SEO or Rank Math, and publish your first post.

    Troubleshooting

    <div class="article-table-wrap"><table><thead><tr><th>Problem</th><th>Cause</th><th>Solution</th></tr></thead><tbody><tr><td>502 Bad Gateway from Nginx</td><td>PHP-FPM socket path mismatch or service down</td><td><code>systemctl status php8.3-fpm</code>, confirm <code>fastcgi_pass unix:/run/php/php8.3-fpm.sock;</code> matches <code>/etc/php/8.3/fpm/pool.d/www.conf</code> listen directive</td></tr><tr><td><code>Error establishing a database connection</code></td><td>Wrong credentials in wp-config.php or MariaDB stopped</td><td>Check <code>systemctl status mariadb</code>, verify <code>DB_USER</code>, <code>DB_PASSWORD</code>, <code>DB_HOST</code> in wp-config.php, test with <code>mysql -u wp_user -p wordpress</code></td></tr><tr><td>White screen (WSOD) on front end</td><td>PHP fatal error, logs suppressed</td><td>Enable <code>WP_DEBUG_LOG</code> in wp-config.php, tail <code>/var/www/example.com/wp-content/debug.log</code> and <code>/var/log/nginx/error.log</code></td></tr><tr><td>Uploaded images show broken thumbnails</td><td>GD or Imagick not installed, or <code>upload_max_filesize</code> too low</td><td>Install <code>php8.3-gd php8.3-imagick</code> and raise <code>upload_max_filesize</code>/<code>post_max_size</code> in <code>/etc/php/8.3/fpm/php.ini</code></td></tr><tr><td>Redis cache never shows as enabled</td><td><code>php8.3-redis</code> extension missing or Redis refusing connection</td><td><code>apt install php8.3-redis &amp;&amp; systemctl restart php8.3-fpm</code>, then <code>redis-cli ping</code> should return PONG</td></tr><tr><td><code>X-FastCGI-Cache</code> header always MISS</td><td>Cookies, query string or admin URL hitting skip rules</td><td>Test with an anonymous incognito window against the homepage root URL</td></tr><tr><td>Certbot fails with <code>DNS problem: NXDOMAIN</code></td><td>A record not yet propagated or wrong IP</td><td>Run <code>dig +short example.com</code> — the answer must match your server IP; wait for DNS propagation (up to 24h)</td></tr><tr><td>Site is slow only in wp-admin</td><td>FastCGI cache bypasses admin URLs (correct), but object cache missing</td><td>Confirm Redis is enabled: <code>wp redis status</code> should show Connected</td></tr><tr><td>413 Request Entity Too Large on plugin upload</td><td><code>client_max_body_size</code> in Nginx too small</td><td>Raise <code>client_max_body_size 64m;</code> in the server block and reload Nginx</td></tr><tr><td>Cron events not firing</td><td>Default WP pseudo-cron depends on traffic</td><td>Disable pseudo-cron (<code>define('DISABLE_WP_CRON', true);</code>) and add a system cron: <code><em> </em> <em> </em> * cd /var/www/example.com &amp;&amp; php wp-cron.php</code></td></tr></tbody></table></div>

    Tailing Logs

    Keep these three terminals open when something goes wrong:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">tail -f /var/log/nginx/error.log tail -f /var/log/php8.3-fpm.log tail -f /var/www/example.com/wp-content/debug.log</code></pre></div>

    FAQ

    What is the minimum VPS size to run WordPress in production?

    A 2 vCPU / 4 GB RAM server such as our CloudCore Starter plan handles a single production WordPress site with Redis object cache and Nginx FastCGI page cache, serving thousands of daily visitors before you notice any load. Below 2 GB RAM the OPcache, InnoDB buffer pool, and FastCGI cache start competing for memory and latency suffers. If you plan to run WooCommerce, membership plugins, or more than one site, step up to the Professional plan (4 vCPU / 8 GB).

    Should I use MariaDB or MySQL for WordPress on Ubuntu 24.04?

    Either works, but MariaDB is the recommended default on Ubuntu 24.04. It is a drop-in replacement for MySQL, ships in the main Ubuntu repositories (MySQL requires an external repo), and is maintained by the original authors of MySQL. WordPress requires MySQL 5.7+ or MariaDB 10.4+, and the MariaDB 10.11 package in Ubuntu 24.04 exceeds that comfortably.

    Do I really need both FastCGI cache and Redis object cache?

    Yes — they solve different problems. FastCGI cache stores fully rendered HTML at the Nginx layer so anonymous visitors never touch PHP at all; a cached hit is served in single-digit milliseconds. Redis object cache stores MySQL query results and WordPress transients in memory, which dramatically speeds up the admin dashboard, logged-in user sessions, WooCommerce carts and checkout, and any request that must bypass the full-page cache. Both layers compound: FastCGI handles public pages, Redis handles everything dynamic.

    Is self-hosting WordPress cheaper than WP Engine, Kinsta or Bluehost?

    Yes, dramatically so beyond the starter tier. A self-hosted WordPress install on an EUR 7.99/month CloudCore Starter VPS replaces WP Engine's USD 25/month Startup plan (25k visits cap), Kinsta's USD 35/month Starter (35k visits cap), and Bluehost's USD 9.95/month shared hosting (renews at USD 24.95) with no visit caps, no overage fees, and no plugin deny-lists. On renewal, a typical managed WordPress host doubles its price, while a VPS monthly rate stays fixed for the lifetime of the server.

    How do I install WordPress without using the web-based five-minute install wizard?

    Use WP-CLI. The four commands wp core download, wp config create, wp db create and wp core install perform the entire installation — including the admin user — from the terminal in under 30 seconds, and can be scripted into cloud-init templates or Ansible playbooks for reproducible provisioning.

    What are WordPress salt keys and why do I need fresh ones?

    Salt keys are eight long random strings in wp-config.php (AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY and their _SALT counterparts) used to sign cookies and session tokens. Fresh values fetched automatically by wp config create from the WordPress salts API prevent anyone who has ever seen a default or leaked wp-config.php template from forging session cookies against your site. Regenerate them any time you suspect a compromise with wp config shuffle-salts.

    Can I run multiple WordPress sites on one VPS?

    Yes. Create a separate Nginx server block, separate database, separate database user, and separate web root for each site. The LEMP stack comfortably handles a dozen low-to-medium traffic WordPress sites on a single 4 GB RAM VPS. Once you add WooCommerce or cross 10 active sites, move up to an 8 GB plan so OPcache and the InnoDB buffer pool have room to keep everything hot in memory.

    How do I migrate from a shared-hosting Apache environment to this Nginx stack?

    Export the database with mysqldump, tar up the wp-content directory, install the LEMP stack on your new VPS using this guide, import the database, restore wp-content, update database credentials in wp-config.php, and translate any .htaccess rewrite rules into the Nginx try_files directive (the block in Step 6 already handles standard WordPress permalinks). If the domain changed, run wp search-replace 'https://old.example.com' 'https://new.example.com' to update URLs throughout the database. Test with the new hostname in your local /etc/hosts before switching DNS.

    Next Steps

    • Automate daily backups with restic or BorgBackup. Schedule nightly snapshots of /var/www/example.com and mysqldump wordpress to an offsite S3 bucket or a second VPS.
    • Add a CDN. Cloudflare's free plan in front of your Nginx FastCGI cache cuts global latency to single-digit milliseconds and absorbs DDoS traffic before it reaches your server.
    • Install WooCommerce for an online store. See our dedicated WooCommerce install guide for the additional PHP extensions and Nginx tuning it needs.
    • Harden logins with Fail2Ban. Add a jail for wp-login.php` to block brute-force attempts after 5 failed attempts in 10 minutes.
    • Publish a companion newsletter with Ghost. Run Ghost on a subdomain of the same VPS for member-only posts, while WordPress handles the public marketing site.
    • Switch the reverse proxy to Caddy if you prefer automatic HTTPS without Certbot and a simpler config file format.
    • Monitor with Uptime Kuma and Netdata. Get paged the moment the site goes down, and watch MariaDB, Redis, and PHP-FPM metrics in real time.
    For canonical reference documentation while building themes and plugins, bookmark the official WordPress documentation hub and the WordPress Developer Resources — the latter is the authoritative source for the plugin API, REST API, block editor, and coding standards.

    ### Skip the Manual Install — Get a Starter VPS
    >
    Our CloudCore Starter VPS gives you a clean Ubuntu 24.04 server in under 60 seconds for EUR 7.99/month:
    >
    - 2 vCPU cores / 4 GB RAM / 60 GB NVMe SSD
    - Unmetered bandwidth
    - 9 global locations
    - Full root SSH access
    >
    Deploy, follow this guide, and run your WordPress site at a fraction of managed-host pricing — with no visit caps, no plugin deny-lists, and no renewal price hikes. Launch your VPS now.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket