Managing your own WordPress hosting gives you complete control over performance, security, and cost. For membership sites running Paid Memberships Pro, a well-tuned server can handle thousands of concurrent members, process payments reliably, and keep your content protected without breaking a sweat.

This is pretty complicated stuff, but it is getting more manageable over time. A lot of the value added by traditional “managed WordPress hosting” can now be accomplished with some combination of Cloudflare (security and caching at the edge), Digital Ocean (server maintenance and backup), and Large Language Models (support).

Throughout this guide, we will walk you through setting up a production-ready LAMP stack on a Digital Ocean Droplet running Ubuntu 24.04 LTS. You will configure Apache with MPM Event, MySQL with performance tuning, PHP 8.3 with FPM for optimal resource management, and Redis for object caching. This is the same stack we use for our own hosting infrastructure.

This guide is for technical users who are comfortable with the command line and want full control over their hosting environment. If you have never SSH’d into a server before, you may want to start with a managed hosting solution first.

Featured Image for How to Set Up High-Performance Hosting for WordPress and Paid Memberships Pro with Digital Ocean blog post

We will be publishing a series of posts here that share almost every detail of how we set up our hosting platform for ourselves and our customers. Future posts will cover how we configure WordPress itself and then also how we configure Cloudflare. We will also be releasing templates, blueprints, scripts, and tools that will help to automate aspects of this, but we thought it would be good to have one place where every step is laid out in detail.

Callout: Managing all of this yourself takes time and expertise. If you would rather focus on growing your membership business while we handle the infrastructure, learn about our managed hosting plans built specifically for Paid Memberships Pro.

Note: While this guide is tailored for Paid Memberships Pro sites, the same configuration works exceptionally well for WooCommerce stores or any high-transaction WordPress site. The principles of database tuning, PHP process management, and caching apply universally to WordPress sites that handle significant traffic or process transactions.

Prerequisites

Before you begin, make sure you have:

  • A Digital Ocean account (sign up at digitalocean.com if you do not have one)
  • A domain name you control
  • Access to your domain’s DNS settings (we recommend Cloudflare, but any DNS provider works)

Step 1: Create Your Digital Ocean Droplet

A Droplet is Digital Ocean’s term for a virtual private server. You will create one running Ubuntu 24.04 LTS.

Note: There are droplet “images” that come preinstalled with Apache or Nginx, MySQL, and even WordPress. For this guide, we are starting from a more general purpose Ubuntu image so we can install and configure things how we like. When we publish our own image that follows these guidelines, we will post a link to it here which will make it possible to match our set up with fewer steps.

  1. Log into your Digital Ocean account
  2. Click the green Create button in the top right and select Droplets
  3. Choose Region: Select the region closest to your members. If most of your audience is in the US, New York or San Francisco are good choices.
  4. Choose an image: Under the OS tab, select Ubuntu and choose 24.04 (LTS) x64
  5. Choose Size:
    • Click Shared CPU: Basic
    • Under CPU options, select Premium AMD for better performance
    • Choose your Droplet size:
      • 4 GB RAM / 2 CPUs ($48/month) – Good for most membership sites
      • 8 GB RAM / 4 CPUs ($96/month) – Better for high-traffic sites or sites with many concurrent members
  6. Choose Authentication Method: Select SSH Key and add your public SSH key. If you do not have one, Digital Ocean has a guide on how to create SSH keys.
  7. Enable Backups: Check the box to enable automated backups. This adds a small cost but is essential for a membership site where you cannot afford to lose data.
  8. Hostname: Enter your domain name (for example, yourdomain.com). This helps you identify the Droplet later.
  9. Click Create Droplet

Once the Droplet is created, note the IP address displayed. You will need this for the next step.

Step 2: Point Your Domain to Your Droplet

Before you can access your site, you need to create a DNS record that points your domain to your Droplet’s IP address.

Note: If you are setting up a staging site before going live, use a subdomain like staging.yourdomain.com instead of your primary domain. During our hosting launch, if you host with us, we will migrate your existing site for you.

We recommend using Cloudflare for DNS. Beyond basic DNS, Cloudflare provides a web application firewall, DDoS protection, and edge caching that can significantly improve your site’s security and performance. The free tier includes everything you need to get started.

Set Up Your DNS Record

  1. Log into your DNS provider (Cloudflare, your domain registrar, or wherever you manage DNS)
  2. Navigate to the DNS settings for your domain
  3. Create a new A record:
    • Name: @ (or your subdomain, like staging)
    • Value/Points to: Your Droplet’s IP address
    • TTL: Auto (or 300 seconds)
  4. If you want www.yourdomain.com to work as well, create another A record:
    • Name: www
    • Value/Points to: Your Droplet’s IP address

Verify DNS Propagation

DNS changes can take anywhere from a few minutes to 48 hours to propagate, though it is usually fast.

  1. Visit dnschecker.org
  2. Enter your domain name
  3. Select A from the record type dropdown
  4. Click Search

When you see your Droplet’s IP address with green checkmarks across most locations, your DNS is ready.

Step 3: Connect via SSH and Create a Sudo User

Now you will connect to your Droplet and create a non-root user for day-to-day administration. Running as root is a security risk, so you should create a regular user with sudo privileges.

Connect to Your Droplet

ssh root@your_droplet_ip

Replace your_droplet_ip with the IP address from Step 1.

Create a New User

Create a new user (replace yourusername with your preferred username):

adduser yourusername

You will be prompted to set a password and enter some optional information.

Grant the user sudo privileges:

usermod -aG sudo yourusername

Set Up SSH Key Authentication for the New User

Copy the SSH key from root to your new user:

rsync --archive --chown=yourusername:yourusername ~/.ssh /home/yourusername

Test the New User

Open a new terminal window and connect as your new user:

ssh yourusername@your_droplet_ip

Verify sudo works:

sudo whoami

This should output root. If it works, you can close your root SSH session and continue as your new user.

Step 4: Update System Packages

Start by updating your system packages to ensure you have the latest security patches and software versions.

sudo apt update

This refreshes the package cache so your system knows about the latest available packages.

Step 5: Configure the UFW Firewall

Security should be your first priority. Ubuntu’s Uncomplicated Firewall (UFW) helps you control which services are accessible from the internet.

First, check the current firewall status:

sudo ufw status

By default, UFW is inactive on a fresh Ubuntu installation.

Allow SSH connections so you do not lock yourself out:

sudo ufw allow OpenSSH

Enable the firewall:

sudo ufw enable

Verify the firewall is running:

sudo ufw status

You should see that OpenSSH is allowed and the firewall is active.

Step 6: Install and Configure Apache

Apache is a battle-tested web server that handles WordPress exceptionally well. While Nginx has its advantages, Apache’s .htaccess support makes it easier to work with WordPress plugins that need to modify server configuration. It is very important to us that it be as easy as possible to configure your server for your own needs, and to be able to take your configuration with you if you choose to host your site elsewhere. For those reasons, we chose Apache for our web server layer.

Note: Nginx has become standard in the managed WordPress hosting space due to its better performance out of the box, but Apache has improved since Nginx’s release. If you configure Apache using In our preferred MPM setup, it is nearly as fast as Nginx. Nginx shows modest performance gains, mostly in serving static assets that are often cached at the edge (Cloudflare) anyway.

Install Apache

sudo apt install -y apache2

Verify the installation:

sudo systemctl status apache2

Allow web traffic through the firewall:

sudo ufw allow "Apache Full"

Visit your server’s IP address in a browser to confirm Apache is running. You should see the default Apache welcome page.

Enable MPM Event Module

By default, Apache may use the prefork module, which creates a new process for each connection. The MPM Event module is more efficient for handling concurrent connections because it uses a hybrid multi-process, multi-threaded approach.

Disable the prefork and worker modules, then enable the event module:

sudo a2dismod mpm_prefork mpm_worker
sudo a2enmod mpm_event

Configure AllowOverride for WordPress

WordPress uses .htaccess files for permalink rules and other configurations. You need to tell Apache to respect these files. You could get more granular about what override’s are allowed, but we are assuming the admin of the server is the only administrator of the site as well. We mostly want to disable the default indexes option and allow rewrites and other configuration typically done via htaccess for WordPress sites.

Edit the Apache configuration:

sudo nano /etc/apache2/apache2.conf

Find the <Directory /var/www/> section and update it to look like this:

<Directory /var/www/>
        Options FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>

The AllowOverride All directive tells Apache to process .htaccess files in this directory tree.

Enable the Rewrite Module

WordPress permalinks require the rewrite module:

sudo a2enmod rewrite

Test your configuration and restart Apache:

sudo apachectl -t
sudo systemctl restart apache2

The apachectl -t command checks for syntax errors before restarting. Always run this before restarting Apache in production.

Step 7: Install and Configure MySQL

MySQL is the database engine that stores all your WordPress content, membership data, and payment transactions. Proper tuning is critical for performance, especially when processing member signups and recurring payments.

Install MySQL

sudo apt install -y mysql-server

Verify the installation:

sudo systemctl status mysql

Configure MySQL for Performance

Edit the MySQL configuration file:

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

Add these settings at the end of the file:

max_binlog_size = 50M
binlog_expire_logs_seconds = 86400
max_connections = 200
innodb_buffer_pool_size = 5G

Here is what each setting does:

  • max_binlog_size and binlog_expire_logs_seconds: Binary logs are used for replication and point-in-time recovery. These settings limit individual log files to 50MB and automatically delete logs older than 24 hours, preventing disk space from filling up.
  • max_connections: Sets the maximum number of simultaneous database connections. Membership sites with many concurrent members logging in, checking out, or accessing protected content need this headroom. The default of 151 can be too low for busy sites.
  • innodb_buffer_pool_size: This is the most important setting for performance. InnoDB uses this memory pool to cache table data and indexes. Set this to 50-70% of your available RAM. For an 8GB server, 5GB is a good starting point. For a 4GB server, use 2GB.

These values are better defaults for the size droplet we recommend and the typical pressures put on a typical server hosting a decent sized Paid Memberships Pro site. If you are experiencing memory issues or performance issues on the site, you may want to study up on these values and revisit the settings for your specific site.

Restart MySQL to apply the changes:

sudo systemctl restart mysql

Secure the Installation

Run the secure installation script to remove test databases and set a root password:

sudo mysql_secure_installation

Follow the prompts to:

  • Set a root password
  • Remove anonymous users
  • Disallow root login remotely
  • Remove the test database
  • Reload privilege tables

Create the WordPress Database

Connect to MySQL:

sudo mysql

Create a database and user for WordPress. Replace your_very_strong_password_here with a secure password:

CREATE DATABASE mypmprosite DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'mypmprosite'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_very_strong_password_here';
GRANT ALL ON mypmprosite.* TO 'mypmprosite'@'localhost';
FLUSH PRIVILEGES;
EXIT;

The utf8mb4 character set supports the full range of Unicode characters, including emojis, which users may include in comments or product descriptions.

Step 8: Install and Configure PHP 8.3 With FPM

PHP-FPM (FastCGI Process Manager) is a more efficient way to run PHP than the traditional mod_php approach. It manages a pool of PHP worker processes and handles requests through FastCGI, which gives you better resource management and the ability to run multiple PHP versions.

Install PHP and Extensions

Install PHP and the extensions required by WordPress and Paid Memberships Pro:

sudo apt install php php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-xmlrpc php-soap php-intl php-zip imagemagick php-imagick -y

Here is what each extension provides:

  • php-mysql: Database connectivity
  • php-curl: HTTP requests for APIs and updates
  • php-gd: Image manipulation for thumbnails
  • php-mbstring: Multibyte string support for international characters
  • php-xml: XML parsing for feeds and sitemaps
  • php-xmlrpc: XML-RPC API support
  • php-soap: SOAP API support for some payment gateways and integrations
  • php-intl: Internationalization for multi-language and currency formatting
  • php-zip: Archive handling for plugin and theme installations
  • php-imagick: Advanced image processing

Verify the installation:

php -v

Check the installed modules:

php -m

Verify PHP-FPM is running:

sudo systemctl status php8.3-fpm

Configure Apache to Use PHP-FPM

Disable the mod_php module and enable the proxy modules:

sudo a2dismod php8.3
sudo a2enmod proxy proxy_fcgi setenvif

Enable the PHP-FPM configuration:

sudo a2enconf php8.3-fpm
sudo apachectl -t
sudo systemctl restart apache2

Configure PHP Settings

Edit the PHP configuration file:

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

Find and update these settings:

upload_max_filesize = 256M
post_max_size = 256M
max_execution_time = 30
memory_limit = 512M
error_log = /var/log/php/errors.log

Here is why these values matter:

  • upload_max_filesize and post_max_size: Membership sites often need to upload member import files, downloadable content for members, or large media files. 256MB handles most use cases.
  • max_execution_time: Limits how long a script can run. 30 seconds is usually sufficient, but you may need to increase this for large imports.
  • memory_limit: Paid Memberships Pro with many members and Add Ons can use significant memory, especially during checkout or when generating reports. 512MB provides headroom for complex operations.
  • error_log: Centralizes PHP errors in one location for easier debugging.

Note: We also recommend increasing these values on the fly for admin users in WordPress. Here is a gist to do that. We will cover these kinds of optimizations more in our post on setting up WordPress.

Create the log directory:

sudo mkdir -p /var/log/php
sudo chown www-data:www-data /var/log/php

Configure PHP-FPM Pool Settings

The pool configuration controls how PHP-FPM manages worker processes. Edit the pool configuration:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Find and update these settings. This example is tuned for a 4vCPU/8GB server:

pm = dynamic
pm.max_children = 4
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500

Understanding the process manager settings:

  • pm = dynamic: Allows the number of worker processes to scale based on demand
  • pm.max_children: The maximum number of PHP worker processes. Each process uses memory, so this limits total PHP memory usage. For an 8GB server, 4-8 workers is reasonable.
  • pm.start_servers: How many workers to create when PHP-FPM starts
  • pm.min_spare_servers: Minimum idle workers to keep available for new requests
  • pm.max_spare_servers: Maximum idle workers before extras are terminated
  • pm.max_requests: How many requests a worker handles before being recycled. This prevents memory leaks from causing issues over time.

Restart PHP-FPM:

sudo systemctl restart php8.3-fpm

Step 9: Configure the Apache Virtual Host

A virtual host configuration tells Apache how to serve your membership site.

Create a new configuration file:

sudo nano /etc/apache2/sites-available/yourdomain.com.conf

Add this configuration, replacing yourdomain.com with your actual domain:

<VirtualHost *:80>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com
    ServerAdmin webmaster@yourdomain.com
    DocumentRoot /var/www/vhosts/yourdomain.com/public

    <Directory /var/www/vhosts/yourdomain.com/public>
        Options FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/yourdomain_error.log
    CustomLog ${APACHE_LOG_DIR}/yourdomain_access.log combined
</VirtualHost>

The <FilesMatch> block tells Apache to pass PHP files to PHP-FPM through the Unix socket, which is more efficient than TCP.

Create the document root directory:

sudo mkdir -p /var/www/vhosts/yourdomain.com/public

Disable the default site and enable your new configuration:

sudo a2dissite 000-default.conf
sudo a2ensite yourdomain.com.conf

Test and restart Apache:

sudo apachectl -t
sudo systemctl restart apache2

Step 10: Install WordPress

Now you can install WordPress itself.

Download WordPress to a temporary directory:

cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz

Prepare the WordPress files:

touch /tmp/wordpress/.htaccess
cp /tmp/wordpress/wp-config-sample.php /tmp/wordpress/wp-config.php
mkdir /tmp/wordpress/wp-content/upgrade

Copy the files to your document root:

sudo cp -a /tmp/wordpress/. /var/www/vhosts/yourdomain.com/public

Set the correct ownership and permissions:

sudo chown -R www-data:www-data /var/www/vhosts/yourdomain.com/public sudo find /var/www/vhosts/yourdomain.com/public -type d -exec chmod 755 {} \; sudo find /var/www/vhosts/yourdomain.com/public -type f -exec chmod 644 {} \;

Understanding WordPress file permissions:

  • 755 for directories: Owner can read, write, and execute (enter). Group and others can read and execute. This allows Apache to serve files while preventing unauthorized modifications.
  • 644 for files: Owner can read and write. Group and others can only read. This protects files from being modified by other users on the server.
  • www-data ownership: This is the user Apache runs as. WordPress needs to own its files to perform updates and install plugins.

Configure WordPress

Generate security keys:

curl -s https://api.wordpress.org/secret-key/1.1/salt/

Copy the output. You will paste it into the configuration file.

Edit the WordPress configuration:

sudo nano /var/www/vhosts/yourdomain.com/public/wp-config.php

Replace the placeholder salt values with the ones you just generated.

Update the database settings:

define('DB_NAME', 'mypmprosite');
define('DB_USER', 'mypmprosite');
define('DB_PASSWORD', 'your_very_strong_password_here');
define('DB_HOST', 'localhost');

Add this line to enable direct filesystem access for updates:

define('FS_METHOD', 'direct');

Save the file and visit your domain in a web browser to complete the WordPress installation wizard.

Step 11: Configure SSL With Certbot

Let’s Encrypt provides free SSL certificates, and Certbot automates the process of obtaining and renewing them.

Note: Even if you are using Cloudflare (which provides SSL at the edge), you should still install a certificate on your server. This ensures traffic between Cloudflare and your origin server is encrypted. In Cloudflare, set your SSL/TLS encryption mode to “Full (strict)” for the best security.

Install Certbot and the Apache plugin:

sudo apt install certbot python3-certbot-apache -y

Obtain a certificate for your domain:

sudo certbot --apache -n --redirect --agree-tos -d yourdomain.com -d www.yourdomain.com -m admin@yourdomain.com

The flags do the following:

  • --apache: Use the Apache plugin to automatically configure your virtual host
  • -n: Non-interactive mode
  • --redirect: Automatically redirect HTTP to HTTPS
  • --agree-tos: Agree to the Let’s Encrypt terms of service
  • -d: Domain names to include in the certificate
  • -m: Email address for renewal notifications

Test that automatic renewal is working:

sudo certbot renew --dry-run

Verify the Certbot timer is enabled:

sudo systemctl status certbot.timer

Certbot automatically runs twice daily to check for expiring certificates and renew them.

Step 12: Set Up Fail2Ban Security

Fail2Ban monitors log files and automatically bans IP addresses that show malicious behavior, such as repeated failed login attempts. This is just one step in addressing the security of your web server. The setup below addresses the most straight forward brute force attacks on the server. On our sites, we regularly update the server software, firewall rules, and Cloudflare rules to address the never ending stream of security probes and attacks affecting our servers.

Install Fail2Ban:

sudo apt install fail2ban -y

Enable and start the service:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Copy the default configuration to a local file that will not be overwritten by updates:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edit the local configuration:

sudo nano /etc/fail2ban/jail.local

Find the [DEFAULT] section and update these settings:

[DEFAULT]
bantime = 3600
findtime = 1800
maxretry = 15
backend = systemd
banaction = iptables-multiport
ignoreip = 127.0.0.1/8 ::1

Understanding the default settings:

  • bantime: How long an IP is banned (3600 seconds = 1 hour)
  • findtime: The time window to count failures (1800 seconds = 30 minutes)
  • maxretry: Number of failures before banning
  • backend: Use systemd for log monitoring
  • ignoreip: Never ban these addresses (localhost)

Configure individual jails for specific services:

[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 5

[apache-auth]
enabled = true
port = http,https
logpath = /var/log/apache2/*error.log
maxretry = 5

[apache-badbots]
enabled = true
port = http,https
logpath = /var/log/apache2/access.log

[apache-noscript]
enabled = true
port = http,https
logpath = /var/log/apache2/error.log

The SSH jail uses a stricter maxretry of 5 since SSH brute force attacks are common and SSH access is critical to protect.

Restart Fail2Ban:

sudo systemctl restart fail2ban

Check the status:

sudo fail2ban-client status

You should see a list of active jails.

Step 13: Install Redis Object Caching

Redis is an in-memory data store that can dramatically speed up WordPress by caching database queries, session data, and transients. This is especially valuable for membership sites, where member data, access levels, and checkout sessions are frequently accessed.

Install Redis:

sudo apt install redis-server -y

Configure Redis to only accept local connections:

sudo nano /etc/redis/redis.conf

Find and ensure this line is set:

bind 127.0.0.1 ::1

This prevents Redis from being accessible from the internet.

Enable and start Redis:

sudo systemctl enable redis-server
sudo systemctl start redis-server

Test the connection:

redis-cli ping

You should see PONG as the response.

Install the PHP Redis extension:

sudo apt install php-redis -y

Restart PHP-FPM and Apache:

sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2

Verify the extension is loaded:

php -m | grep redis

You should see redis in the output.

To activate Redis caching in WordPress, you will need to install a Redis object cache plugin such as Redis Object Cache or Object Cache Pro.

Conclusion

You now have a production-ready LAMP stack optimized for WordPress and Paid Memberships Pro running on Digital Ocean. Your server includes:

  • A properly sized Digital Ocean Droplet with automated backups
  • DNS configured and pointing to your server
  • Apache with MPM Event for efficient connection handling
  • MySQL tuned for membership transaction loads
  • PHP 8.3 with FPM for optimal resource management
  • SSL certificates with automatic renewal
  • Fail2Ban protecting against brute force attacks
  • Redis ready for object caching

Next Steps

  1. Configure WordPress with best practice settings and plugins, including the Redis object cache plugin to take advantage of Redis on this server. (Guide coming soon.)
  2. Configure Cloudflare for caching and security. (Guide coming soon.)
  3. Set up Paid Memberships Pro. (See our install documentation here.)

Want Some Help With This?

Our hosting packages are reasonable and cover everything from the server, to WordPress and Paid Memberships Pro, to Cloudflare setup and configuration. All plans come with automated updates for PMPro and all of our Add Ons, our premium support system staffed by our team of experts, and our 100 day money back guarantee — no questions asked.

Cover image from ebook 29 Nuggets of Wisdom Volume 1 - Sample Collection

Download the book: Get 29 insights and ‘aha moments’ for new or veteran membership site business owners. Use these nuggets of wisdom to inspire or challenge you.



Was this article helpful?
YesNo
Tagged: . Posted in . Bookmark the . Last updated: .