UPDATE: I’ve decided to make this a more quick & easy to reproduce setup, so am introducing the following constraints:

  • Vultr’s $5/mth VPS (1GB RAM, 25GB HDD)
  • OpenBSD’s -current version ISO installer for amd64
  • Hostname set to subdomain you control
  • Single WordPress site setup
  • httpd(8) webserver
  • PHP 7.3
  • mariadb-server on same VPS
  • IPv4 only
  • http basic auth in front of WordPress
  • simple pf(4) blocking all & permitting ICMP, SSH, HTTP(S)

Future enhancements / might TODO list:

  • pd-badhost
  • Redis (if significant performance boost)
  • unbound
  • sFTP

NOTE: Below guide contents are in process of being rewritten to above constraints.

Minimal Requirements:

  • account at Vultr (probably need a billing method to be able to spin up first VPS?)
  • computer/phone/device that you can use to SSH into the VPS
  • public/private keypair for connecting via SSH

Initial preparation in Vultr web UI:

  • add ISO of OpenBSD -current
  • add a firewall group
  • launch new VPS in your preferred region, using the OpenBSD snapshot ISO you just added

OS installation:

  • once your VPS has started up, access the console Vultr provides
  • TODO: set fulldisk encryption instructions
  • go through installation process, using mostly the default options besides these:
    • set hostname to the subdomain you’re planning to use, ie dev.example.com
    • choose not to configure IPv6
    • choose not to install x server
    • choose yes to start sshd on system startup
    • set a root password
    • setup a user account (puffy)
    • choose yes to allow root SSH access (we’ll disable that as soon as we’ve connected from our device)
    • reboot at the end
  • at next boot> prompt, choose to boot from the VPS’ disk, not the ISO: machine boot hd0a

DNS configuration:

  • point an A record in your DNS zone to the IP of your VPS, using your subdomain

OS Configuration:


  • SSH into your VPS from your local device: ssh root@dev.example.com
  • add your device’s public key to your user’s authorized_keys file:

    ed /home/puffy/.ssh/authorized_keys
    {paste your public key}
  • add your user to doas.conf echo 'permit persist puffy' > /etc/doas.conf

  • test connecting via public-key ssh puffy@dev.example.com whoami # puffy

  • check you can elevate privileges doas whoami # root

  • remove password and root based SSH access (you can still root in from Vultr console in emergency)

    doas sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
    doas sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
    • restart SSH daemon doas rcctl restart sshd
    • disconnect and confirm you can still connect via puffy with public key, but not via root
  • close the Vultr web console window

OK, from this point on, we should be able to follow the previous guide and add to it

Want to setup a WP2Static-friendly, minimal webserver vs using an off the shelf image? We detail the steps for setting up an optimized environment in OpenBSD, our preferred, secure by default, lightweight and easy to maintain operating system. You can also use this as a guide for configuring webservers on other platforms.

This guide is written for and maintained for the latest -current version of OpenBSD. If you’re using a stable release, some changes may be required.

We offer support for users on nginx, Apache, IIS and other webservers, but our primary development and testing machines are OpenBSD, so we can best support you when using it.

Before asking for support, invest some time in searching the comprehensive man pages, ie man httpd or man httpd.conf.

Security note: This guide is for setting a non-public server for your WordPress development site, not for production hosting your exported static site. WP2Static’s benefits allow for a less-secure WordPress installation. We’re not in the business of hosting production sites. See Deployment options for many production hosting options for your exported static site.

Don’t let bad internet interrupt you

  • connect again ssh puffy@example.com
  • start tmux(1) a tmux session will persist on the remote machine even if you get disconnected, so long-running tasks don’t otherwise die with your connection


  • update your packages to latest snapshot (in case it’s been a while since you added your -current ISO to Vultr and running this guide doas pkg_add -Dsnap -u follow any relevant instructions shown at end of update

We have most of what we need in base: the httpd(8) webserver, acme-client(1) (for optional TLS certificates).

From the ports tree, we can install the packages below. Format here is for copying into a list file and using like doas pkg_add -Dsnap -l list, else install just those you want, by removing the trailing --, ie doas pkg_add -Dsnap php, which may ask you to choose the target version.

For WordPress


For WP2Static


Optional utilities that may help in development/troubleshooting

vim--no_x11 (`vi(1)` and `ed(1)` editors are already installed in base)
node-- (optional, for WP2Static development, compiling frontend from source code)
py3-pip-- (for installing AWS CLI client)

Webroot permissions

In this guide, we’re setting up just one WordPress instance in the webserver document root (/var/www/htdocs).

We’ll want to be able to easily edit files and run WP-CLI commands from our puffy account without creating files inaccessible from our webservice. We’ll handle this in a few ways:

  • To allow the www user to write to this directory, we’re setting the user and group access to the dir:

doas chown -R www:www /var/www/htdocs - add our puffy user to the www group - make webroot group writable - add doas.conf(5) line to allowpuffyto execute commands as if it were thewwwwuser (without requiring a password) - alias our WP-CLI commandwptodoas -u www wp - re-applying recursivewwwownership to the/var/www/htdocs/ dir when we make mistakes

Note, because this is a non-public, development server, we’re not hardening the WordPress installation/webserver as one should for production use. We may expand the guide to set harder permissions for specific WP files.

  • add puffy to www group: doas usermod -G www puffy
  • make webroot group writable doas chmod -R 775 /var/www/htdocs

Testing the httpd(8) webserver

On a fresh install of OpenBSD, there is no web service running.

Let’s put a quick test configuration in place and sanity-check that we can serve a webpage:

# /etc/httpd.conf
# a minimal config to serve static content from /var/www/htdocs/

types { include "/usr/share/misc/mime.types" }

server "dev.example.com" {
	listen on * port 80

	root "/htdocs"

	location "/*" {
		directory auto index
  • add a test homepage

    ed /var/www/htdocs/index.html

Paste the following:

Welcome to vultrtest1.minutewp.com!

If you're seeing this without needing authentication, I need to set things up more securely!
# set the webserver to run on system start/allow manually starting
doas rcctl enable httpd

# (re)start the webserver
doas rcctl restart httpd

Test the webserver is serving our file by accessing http://dev.example.com from a browser, or on the server itself, run:

ftp -o - http://dev.example.com and you should see:

Trying YOUR_IP...
Requesting http://dev.example.com
  0% |                                                                                                                                                                                                     |     0       --:-- ETAWelcome to dev.example.com!

If you're seeing this without needing authentication, I need to set things up more securely!
100% |*****************************************************************************************************************************************************************************************************|   130       00:00
130 bytes received in 0.00 seconds (217.32 KB/s)

It’s alive!

Great! Now, let’s add support for PHP and MySQL, so we can install WordPress:

# /etc/httpd.conf
# a minimal config to serve static content from /var/www/htdocs/

types { include "/usr/share/misc/mime.types" }

server "default" {
	listen on * port 80

  directory index "index.php"

	# 10M max client can send to server
	connection max request body 10485760

	root "/htdocs"

	location "/*.css*" {
		request no rewrite

	location "/*.js*" {
		request no rewrite

	location "/*.png*" {
		request no rewrite

	location "/wp-admin/*.php" {
		fastcgi socket "/run/php-fpm.sock"

	location "/wp-admin" {
		block return 301 "/wp-admin/"

	location "/wp-admin*" {
		fastcgi socket "/run/php-fpm.sock"
		request rewrite "/wp-admin/index.php"

	location "*.php" {
		fastcgi socket "/run/php-fpm.sock"

	location "/*" {
		fastcgi socket "/run/php-fpm.sock"
		request rewrite "/index.php"

<?php // /var/www/htdocs/index.php
// file owner should be www: chown www:www /var/www/htdocs/index.php
echo 'PHP is now running on the server!';
# restart the webserver to apply new configuration
doas rcctl restart httpd

# Enable and (re)start the PHP service
doas rcctl enable php73_fpm
doas rcctl restart php73_fpm

Test the webserver is now serving PHP by accessing http://servername from a browser, or on the server itself, run:

ftp -o - http://localhost and you should see:

Requesting http://localhost
PHP is now running on the server!33 bytes received in 0.00 seconds (151.31 KB/s)

Installing WordPress via WP-CLI

As per our guide on WP-CLI usage for WP2Static, we install WP-CLI as follows:

# download WP-CLI phar to temp directory
cd /tmp
ftp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar

# test execution
php wp-cli.phar --info

# should result in:
OS:     OpenBSD 6.6 GENERIC.MP#584 amd64
Shell:  /bin/ksh
PHP binary:     /usr/local/bin/php-7.3
PHP version:    7.3.13
php.ini used:   /etc/php-7.3.ini
WP-CLI root dir:        phar://wp-cli.phar/vendor/wp-cli/wp-cli
WP-CLI vendor dir:      phar://wp-cli.phar/vendor
WP_CLI phar path:       /tmp
WP-CLI packages dir:
WP-CLI global config:
WP-CLI project config:
WP-CLI version: 2.4.0

# move to path with name `wp` and execution permissions
doas mv wp-cli.phar /usr/local/bin/wp
doas chmod +x /usr/local/bin/wp

# test execution again (expect same results)
wp --info

Add local user to www group, allowing to create/edit files within webroot.

doas usermod -G www MYUSERNAME
# log out and in to use new permissions

Download WordPress files

cd /var/www/htdocs
wp core download

Should show output like:

Downloading WordPress 5.3.2 (en_US)...
Warning: Failed to create directory '/var/www/.wp-cli/cache/': mkdir(): Permission denied.
md5 hash verified: ec7fcc299de72d4914a688ea34c96f37
Success: WordPress downloaded.

You should also be able to list the WordPress core files in /var/www/htdocs/ now and accessing http://localhost should contain a message like:

There doesn’t seem to be a wp-config.php file. I need this before we can get started.

MySQL database setup

This can be done earlier in the process, but for the sake of this guide, we wanted to ensure all the filesystem stuff is working first.

For the first draft of this guide and again, as we’re setting up a non-public devserver, we’ll use the quickest, but less secure method of getting a DB prepared for WordPress.

# prepare mysql
doas mysql_install_db
# follow prompts, using root user and for example, password 'banana'

# enable and start service
doas rcctl enable mysqld
doas rcctl start mysqld

# config DB
doas mysql_secure_installation
# follow prompts, using root user and for example, password 'banana'

# create new DB for WordPress
mysql -u root -p

> create database wordpress;

Generate a wp-config.php file

cd /var/www/htdocs/
wp config create --dbhost= --dbname=wordpress --dbuser=root --dbpass=banana

Install WordPress

cd /var/www/htdocs/
wp core install --url=localhost --title='WP Dev Server' --admin_user=admin --admin_password=banana --admin_email=user@example.com

Resulting in:

Success: WordPress installed successfully.

Visiting the site now, you should see a regular WordPress site with default dummy content. You can login to administer via /wp-admin and the credentials you specified earlier.

Installing WP2Static

At this point, you have a few options for installing WP2Static and its add-ons:

  • clone git repository (core and/or add-ons) and compile from latest source code
  • download plugin core and any add-ons you’ve got a license for via local or remote zip file
  • download latest official version from wordpress.org via WP-CLI
  • install latest version via the WordPress admin UI

Installing free version from wp.org via WP-CLI

cd /var/www/htdocs/
# official plugin slug is not "wp2static" due to legacy naming
# we'd love to change this, but wp plugin team don't budge on renaming :(
wp plugin install --activate static-html-output-plugin

Installing from cloned sources for development

# remove plugin version installed via WP admin UI
wp plugin deactivate static-html-output-plugin
wp plugin delete static-html-output-plugin

# clone core repo to projects dir
mkdir -p ~/gitprojects && cd $_
# use legacy plugin name for project
git clone git@github.com:WP2Static/wp2static.git static-html-output-plugin
cd ~/gitprojects/wp2static
# install dependencies
composer install
npm i
# compile frontend
composer buildjs

Use this script to do an initial sync and continue to sync while watching project dir:


# usage: run `sh this_script.sh` in its own shell

# set your project and WP plugin dirs


# initial sync of files
openrsync --rsync-path /usr/bin/openrsync --del -var "$WP2STATIC_CORE_DIR" "$WP_PLUGIN_DIR"/

while true; do
  # doesn't wach ignore files, but will sync whole dir
  ag -al --path-to-ignore "$WP2STATIC_CORE_DIR"/.gitignore | entr -d openrsync --rsync-path /usr/bin/openrsync --del -var "$WP2STATIC_CORE_DIR" "$WP_PLUGIN_DIR"/

Exporting your site with WP2Static

Following this guide up to here, we’ve not needed to login to the WordPress admin or use a browser. We can continue to work on the command line to export our site using WP2Static.

Here, we’ll do the minimal steps required to export the WordPress site to a directory, replacing all Site URLs to our target production URL.

cd /var/www/htdocs/
# set the exported site's URL to what you'll use in production
wp wp2static options set baseUrl https://example.com
# generate the static site
wp wp2static generate
# get generated directory path
"$(cat wp-content/uploads/WP2STATIC-CURRENT-ARCHIVE.txt)"

Deploy your generated static site

Use one of the many deployment options to publish your site (ideally to a staging server to check first!)

We recommend checking the exported site with Netlify in our Doing a test export guide, at least when first getting familiar with WP2Static and static site deployments. For this case, you can either run Netlify’s CLI tool, pointing it to the exported site directory, or use the ZIP deployment method and manually upload that using their web interface.

TODO: notes on copying /etc/hosts to /var/www/etc, resolv.conf, etc for WP to install plugins from repo