Documentation

How WP Freighter works and how to drive it from the admin, the command line, and PHP. Questions and ideas go to GitHub issues; releases are listed in the changelog, or arrive by email.

Install

WP Freighter is one plugin on the site that will become the host. Current release: 1.5.1, for WordPress 5.4+ and PHP 7.0+.

  1. Download wp-freighter.zip and upload it under Plugins → Add New, or install it from the command line:
    wp plugin install --activate \
      https://github.com/WPFreighter/wp-freighter/releases/latest/download/wp-freighter.zip
  2. Open Tools → WP Freighter. On first run the plugin writes wp-content/freighter.php (the bootstrap) and adds one require_once line to wp-config.php, right after $table_prefix.
  3. Pick a files mode and add your first tenant site. That's the whole setup.

Hosts that lock wp-config.php

If the plugin cannot write those two files it says so and shows the exact bootstrap contents and the one-line include to paste yourself, or to hand to your host. Nothing else in WordPress core is touched. Updates arrive through the plugin's own updater, which reads the release manifest from GitHub.

Files modes

Every tenant site always has its own database tables under stacked_<id>_. The files mode decides how much of wp-content is shared. Switch it any time with Tools → WP Freighter or wp freighter files set <mode>.

Shared
One wp-content/ for everyone: plugins, themes and uploads. Right for brand variants or language editions built from the same assets.
Hybrid
Plugins and themes come from wp-content/; each site's media lives in content/<id>/uploads/. Update a plugin once for every site while keeping libraries separate. The default for most people.
Dedicated
Each site gets a whole content/<id>/: its own plugins, themes and uploads. Full isolation, so a plugin update can be trialled on one site without touching the others.

Changing mode does not move files. Going from shared to hybrid, for example, gives new uploads their own folder while existing media stays where it is.

Tenant sites

Create

Add an empty site with a title, a label (or a domain when mapping is on) and an administrator. Only database tables are created, so it is ready in a few seconds. The new site gets a theme it can load: the default theme when the host has it, otherwise the main site's theme; dedicated sites download the default theme into their own folder.

Clone

Clone the main site or any tenant. The source's tables are copied to a new prefix; in hybrid and dedicated mode its content folder is copied too. Internal URLs are rewritten to the new site. This is the staging move: clone production, work in the clone, delete it when done.

Switch

Administrators move between sites from the admin bar. Switching sets a stacked_site_id cookie that the bootstrap reads on the next request; the cookie is only honoured alongside a valid login cookie, so it cannot be used to reach a site you are not signed in to. "Exit" returns you to the main site.

Magic login

Each site has a one-time login link that drops you straight into its dashboard as its administrator. Generate one from the site list, or from the command line with wp freighter login <id>.

Delete

Deleting drops the site's tables and, in hybrid and dedicated mode, removes its content/<id>/ folder. The dialog shows how much is about to go. There is no undo, so clone first if in doubt.

Self-healing

Before switching into a tenant the plugin checks it is present and active there and restores itself if someone deleted or deactivated it from inside the child site.

Domain mapping

With domain mapping off, tenants ride on the host's domain and are reached through the admin switcher or magic login. Good for sandboxes and staging.

With domain mapping on, each tenant is addressed by its own hostname: the bootstrap matches the requested host against the mapping list and loads that tenant. Point the domain's DNS at the host and add it to the server or hosting account so requests actually arrive at this WordPress install; WP Freighter routes from there. Toggle with wp freighter domain set on|off.

WP-CLI

Everything the Tools page does is also a wp freighter command.

wp freighter infoMain site URL, domain-mapping state, files mode, tenant count.
wp freighter list [--format=table|csv|ids|json|yaml]Tenant sites. --field=Domain for one column.
wp freighter add --title="…" --name="…" [--domain=…] [--username=…] [--email=…] [--password=…]Create an empty site. create is an alias.
wp freighter clone main|<id> --name="…" [--domain=…]Clone the main site or a tenant.
wp freighter login main|<id> [--url-only]Magic login URL. open $(wp freighter login 2 --url-only).
wp freighter delete <id> [--yes]Delete a tenant and its content folder.
wp freighter files get|set shared|hybrid|dedicatedFiles mode.
wp freighter domain get|set on|offDomain mapping.
wp freighter regenerateRewrite wp-content/freighter.php and the wp-config.php include.

Running any command inside a tenant

WP-CLI's --url is not how tenants are selected. Set STACKED_SITE_ID in the environment and every wp command runs against that site:

STACKED_SITE_ID=15 wp option get home
STACKED_SITE_ID=15 wp plugin update --all

Combined with list --format=ids, that is a fleet update in three lines:

for id in $(wp freighter list --format=ids); do
  STACKED_SITE_ID=$id wp plugin update --all
done

WP-Cron only runs when a site gets traffic, so for quiet tenants schedule the same loop with wp cron event run --due-now.

PHP API

The same operations are static methods on WPFreighter\Site, for provisioning from your own code.

// Create. Returns an array with 'stacked_site_id', or WP_Error.
$site = \WPFreighter\Site::create( [
    'title'    => 'Project Alpha',
    'name'     => 'alpha',               // label
    'domain'   => 'alpha.example.com',   // optional, needs domain mapping on
    'username' => 'admin',
    'email'    => 'admin@example.com',
    'password' => 'optional, generated if omitted',
] );

// Clone the main site, or a tenant by ID.
$staging = \WPFreighter\Site::clone( 'main', [ 'name' => 'Staging' ] );
$copy    = \WPFreighter\Site::clone( 5, [ 'name' => 'Copy of 5' ] );

// One-time login URL, optionally landing on a specific admin screen.
$url = \WPFreighter\Site::login( 2 );
$url = \WPFreighter\Site::login( 2, 'post-new.php' );

// Delete.
\WPFreighter\Site::delete( 4 );

The admin screen talks to the same code through REST routes under wp-freighter/v1, which require a logged-in administrator.

Questions

How is this different from WordPress Multisite?

Multisite is a network mode built into WordPress: one set of users, one super-admin, network-activated plugins, and tables that all share one prefix. WP Freighter runs ordinary, separate WordPress sites side by side. Each tenant has its own users, its own options, its own plugin list, and a plain stacked_<id>_ prefix. A tenant can be exported as a normal single site because that is what it already is.

The trade is scope: Multisite is for one organisation running a network; Freighter is for one person or team running several unrelated sites on one install. The origin story is in Experimental Stackable WordPress Mode.

Why was it built?

For maintenance work at Anchor Hosting. Checking how a site behaves with a plugin removed or updated used to mean deploying to a separate staging environment first. Cloning the live site into a tenant on the same install, testing there, and deleting the clone is faster, and it needs nothing but a WordPress administrator account. It has not replaced staging for larger changes, but it has replaced most of the small ones.

Who is it for?

Developers, maintenance providers and plugin authors who need a safe copy of a site quickly; and people with a handful of small sites who would rather run them from one install than pay for and update several.

Can it lower hosting costs?

Yes, within the limits of the plan. One install answers for every tenant, so a plan that comfortably serves the sum of their traffic can host them all for one price. Low-traffic sites stack well; a single busy site will still want its own resources. Anchor Hosting's own tool sites, including this one, share one Kinsta plan this way.

When should I not use it?

When the sites belong to different people. Everything shares one set of hosting credentials, one SFTP login and one wp-config.php, so there is no way to give one tenant's owner access to only their files. Stack sites you own or fully manage; give clients who need file-level access their own install.

Does it work with WP-CLI?

Yes. Tenants are selected with the STACKED_SITE_ID environment variable rather than --url; see WP-CLI for the commands and a fleet-update loop.

Can I move an existing site in as a tenant?

There is no importer yet, but it is the same manual job as moving a site into a Multisite network: create an empty tenant, export the old site's database, rename its table prefix to the tenant's stacked_<id>_, import, rewrite internal URLs (wp search-replace with STACKED_SITE_ID set), and copy its wp-content into content/<id>/. Practise on a clone before doing it to anything that matters.

Can I contribute?

Please. The plugin is MIT-licensed at github.com/WPFreighter/wp-freighter. Issues and pull requests both welcome.

Limitations

Tenants share one install, so they share the things an install has only one of.

  • wp-config.php has to be writable, or editable by you. The bootstrap include is the one hook into WordPress. If a host forbids both automatic and manual edits to wp-config.php, WP Freighter cannot run there.
  • Root files are shared. .htaccess, robots.txt, anything in the web root, and the WordPress core version are the same for every tenant. If a tenant needs its own root-level folder, put it under content/<id>/ and add a redirect with the Redirection plugin: for example /presentations/ to /content/1/presentations/.
  • One PHP, one server, one database. Tenants are separate sites, not separate machines. Size the host for the sum of them, and remember one backup covers them all.
  • WP-Cron depends on traffic. A quiet tenant's scheduled tasks wait for a visitor. Drive them with a system cron and STACKED_SITE_ID=<id> wp cron event run --due-now.
A Redirection plugin rule sending /presentations/ to /content/1/presentations/
A Redirection rule that gives tenant 1 its own /presentations/ folder.

Videos

Recorded for the 1.0 release in 2020. The admin screen has been redesigned since, but the ideas and the hosting walkthroughs still hold.