Skillversitas

WordPress on a MicroVM

Run the WordPress CMS in a MicroVM with Nginx, PHP-FPM, and MySQL. A complete setup for a production-ready WordPress.

Architecture

Client
  |
[MicroVM]
  |
[Nginx :80] -> [PHP-FPM :9000] -> [WordPress Files]
                     |
              [MySQL :3306]

Preparing the Image

Install All Packages

sudo mount -o loop rootfs.img /mnt/microvm

sudo chroot /mnt/microvm bash -c '
  apt update && apt install -y \
    nginx php8.2-fpm php8.2-mysql php8.2-gd php8.2-curl \
    php8.2-mbstring php8.2-xml php8.2-zip php8.2-intl \
    php8.2-imagick php8.2-redis \
    mysql-server \
    wget unzip

  # Download WordPress
  cd /var/www
  wget -q https://wordpress.org/latest.tar.gz
  tar xzf latest.tar.gz
  mv wordpress html
  chown -R www-data:www-data html
'

sudo umount /mnt/microvm

Install via Alpine

sudo chroot /mnt/microvm bash -c '
  apk add nginx php82-fpm php82-mysqlnd php82-gd php82-curl \
    php82-mbstring php82-xml php82-zip php82-intl \
    mysql-client

  mkdir -p /var/www/html
  cd /tmp
  wget https://wordpress.org/latest.tar.gz
  tar xzf latest.tar.gz -C /var/www/html --strip-components=1
  chown -R nginx:nginx /var/www/html
'

Configuring Nginx

cat > /etc/nginx/sites-available/wordpress << 'WPCONF'
server {
    listen 80;
    server_name yourdomain.com;
    root /var/www/html;
    index index.php index.html;

    client_max_body_size 64M;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

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

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300;
    }

    # Cache static files
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Block sensitive files
    location ~ /\.(ht|git|svn) {
        deny all;
    }

    location = /xmlrpc.php {
        deny all;
    }

    # WP-Cron
    location = /wp-cron.php {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
WPCONF

ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default

Configuring PHP-FPM

# Pool configuration for WordPress
cat > /etc/php/8.2/fpm/pool.d/www.conf << 'PHPPOOL'
[www]
user = www-data
group = www-data
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

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

; Memory limit
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
php_admin_value[max_execution_time] = 300
php_admin_value[max_input_time] = 300

; OPcache
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 128
php_admin_value[opcache.interned_strings_buffer] = 8
php_admin_value[opcache.max_accelerated_files] = 10000
php_admin_value[opcache.revalidate_freq] = 2
php_admin_value[opcache.save_comments] = 1
PHPPOOL

Configuring MySQL

cat > /etc/mysql/conf.d/wordpress.cnf << 'MYSQLCONF'
[mysqld]
innodb_buffer_pool_size = 128M
innodb_log_file_size = 32M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
max_connections = 50
skip-name-resolve
MYSQLCONF

Running the MicroVM

qemu-system-x86_64 \
  -M microvm -enable-kvm -cpu host -m 1g -smp 4 \
  -kernel vmlinuz \
  -append "console=ttyS0 root=/dev/vda rw" \
  -drive id=rootfs,file=wordpress-rootfs.img,format=raw,if=none,cache=none,aio=linux \
  -device virtio-blk-device,drive=rootfs \
  -netdev user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::2222-:22 \
  -device virtio-net-device,netdev=net0 \
  -nographic -serial stdio

Setting Up the Guest

# Start services
service mysql start
service php8.2-fpm start
service nginx start

# Create the WordPress database
mysql -u root << 'SQL'
CREATE DATABASE wordpress;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
SQL

# Configure wp-config.php
cd /var/www/html
cp wp-config-sample.php wp-config.php

# Generate salts
SALTS=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/)

# Edit wp-config.php
sed -i "s/database_name_here/wordpress/" wp-config.php
sed -i "s/username_here/wpuser/" wp-config.php
sed -i "s/password_here/secure_password/" wp-config.php

# Add the salts
# (remove the old placeholder salts, add the new ones)

Complete wp-config.php

<?php
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'secure_password' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );

define( 'AUTH_KEY',         'put your unique phrase here' );
define( 'SECURE_AUTH_KEY',  'put your unique phrase here' );
define( 'LOGGED_IN_KEY',    'put your unique phrase here' );
define( 'NONCE_KEY',        'put your unique phrase here' );
define( 'AUTH_SALT',        'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT',   'put your unique phrase here' );
define( 'NONCE_SALT',       'put your unique phrase here' );

$table_prefix = 'wp_';

define( 'WP_DEBUG', false );
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
define( 'WP_CRON_LOCK_TIMEOUT', 120 );

if ( ! defined( 'ABSPATH' ) ) {
    define( 'ABSPATH', __DIR__ . '/' );
}

require_once ABSPATH . 'wp-settings.php';

OPcache Optimization

; /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.save_comments=1
opcache.fast_shutdown=1
opcache.enable_cli=0

Cron Job

# Disable WP-Cron via HTTP (use the system cron)
echo "DISABLE_WP_CRON = true" >> /var/www/html/wp-config.php

# Add the system cron
echo "*/5 * * * * www-data php /var/www/html/wp-cron.php" > /etc/cron.d/wordpress

Backup

# Backup files
tar czf /data/wordpress-files-$(date +%F).tar.gz /var/www/html

# Backup database
mysqldump -u wpuser -p wordpress > /data/wordpress-db-$(date +%F).sql

# or combined
mysqldump -u wpuser -p wordpress | gzip > /data/wordpress-full-$(date +%F).sql.gz

Troubleshooting

White screen of death

# Enable debug
# in wp-config.php:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );

# Check the log
tail -f /var/www/html/wp-content/debug.log

PHP memory limit

// wp-config.php
define( 'WP_MEMORY_LIMIT', '512M' );

Permission issues

chown -R www-data:www-data /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod 400 /var/www/html/wp-config.php