Migrating Several WordPress Sites onto One Trellis Server: Configure Everything First, and the Pitfalls That Bite
Trellis can host many WordPress sites on one server, and moving three sites over is not three times the work of moving one. Most of the first migration is learning; the second and third reuse it. What makes them go smoothly, or not, is mostly setup: one project, unique names, and every site configured before the first provision.
This builds on the single-site migration. Here are the parts that change when there is more than one site, and the mistakes that cost me time.
One project, one directory per site
trellis new example.com creates trellis/ and a site/ directory for the Bedrock install. Rename site/ straight away to something that says which site it is, because the next site needs its own:
example.com/
├── trellis/
├── site-example/ # Bedrock for example.com
├── site-clienttwo/ # Bedrock for clienttwo.com
└── site-clientthree/ # Bedrock for clientthree.com
Point each site at its directory in trellis/group_vars/<environment>/wordpress_sites.yml:
wordpress_sites:
example.com:
site_hosts:
- canonical: example.com
local_path: ../site-example
repo: git@github.com:yourname/trellis-multi.git
repo_subtree_path: site-example
clienttwo.com:
site_hosts:
- canonical: clienttwo.com
local_path: ../site-clienttwo
repo: git@github.com:yourname/trellis-multi.git
repo_subtree_path: site-clienttwo
All sites share one Trellis configuration and, in this layout, one Git repository. repo_subtree_path tells Trellis which folder of that repository holds each site.
To add a Bedrock install for another site, create it next to the others:
composer create-project roots/bedrock site-clienttwo
Do the same in both the development and production wordpress_sites.yml, and in both vaults.
Configure everything, then provision once
Add every site to wordpress_sites.yml before the first provision. One run then creates all the databases, Nginx hosts and certificates:
cd trellis
trellis provision production
Adding a site later is fine, but it is a separate step: the new site has no database, no host and no directory until you provision again, and trellis deploy alone fails.
# add the site to wordpress_sites.yml, then:
trellis provision production
trellis deploy production clienttwo.com
trellis deploy production with no site name deploys all of them.
Work the sites in parallel
The slow parts are transfers, and they do not depend on each other. Import the database for site one in one terminal, upload the uploads for site two in another, and prepare site three’s theme in a third.
A folder per site for the old backups keeps this tidy:
~/migrations/
├── example/ {database, files/uploads, files/themes, files/plugins}
├── clienttwo/
└── clientthree/
Two small scripts remove most of the repetition:
#!/bin/bash
# migrate-uploads.sh <site-dir> <domain>
SITE=$1; DOMAIN=$2
SRC=~/migrations/$SITE/files/uploads
DEST=/srv/www/$DOMAIN/shared/uploads
rsync -avz --progress "$SRC"/ admin@your.server.ip:"$DEST"/
ssh admin@your.server.ip "sudo chown -R web:www-data $DEST && sudo chmod -R 775 $DEST"
#!/bin/bash
# import-database.sh <site-dir> <domain>
SITE=$1; DOMAIN=$2
scp ~/migrations/$SITE/database/backup.sql admin@your.server.ip:/tmp/backup-$SITE.sql
ssh admin@your.server.ip "cd /srv/www/$DOMAIN/current && wp db import /tmp/backup-$SITE.sql && rm /tmp/backup-$SITE.sql"
If you push from the local VM instead, use the database and files push playbooks, which take the remote backup before they overwrite anything.
Pitfalls
1. The same local_path for every site. Copy a site block and forget to change the path, and deploys fail or ship the wrong site’s code. Check them all:
grep -A5 "local_path" group_vars/production/wordpress_sites.yml
2. The same repo_subtree_path. Same symptom, different cause: the deploy checks out the wrong folder of the repository. Each site needs its own.
3. Reused database passwords and salts. Copying the vault block between sites copies the password and the salts. One compromised site then exposes the others. Generate a password per site, and fetch fresh salts for each, in both vaults.
4. Not re-provisioning after adding a site. Database connection errors, a 502 from Nginx, a missing certificate, or /srv/www/newsite.com/ not existing all mean the same thing. Provision, then deploy.
5. Updating only one environment. The development and production files and vaults both need the new site, or trellis vm and production disagree.
6. A wrong branch name. The branch: in the site config must match what your repository actually uses (main or master). Check with git branch -a.
7. Skipping the path conversions on later sites. Each site needs its own /wp-content/ to /app/ search-replace and a check for leftovers:
wp db search '/wp-content/' --all-tables
It should return nothing.
8. Upload permissions. Files copied as your own user are not writable by the web user. After every uploads transfer, chown -R web:www-data and chmod -R 775 on shared/uploads.
9. DNS TTL. Lower it 24 to 48 hours ahead and keep the old server running afterwards. See the cutover post.
10. Pushing over a production database by accident. Take a manual export first, then run the push. The playbook also backs up the remote database to database_backup/ automatically, but confirm the file exists before you rely on it.
Which site is that database?
When something looks wrong on one site, find out which database it is using before you change anything:
ssh admin_user@your.server.ip
cd /srv/www/clienttwo.com/current
grep DB_NAME .env
wp --path=web/wp db tables | head
And to run WP-CLI for a specific site from anywhere on the server, give it that site’s path:
wp --path=/srv/www/clienttwo.com/current/web/wp plugin list
When not to share a server
Separate servers make sense when the sites have very different traffic, need different PHP versions, have security or regulatory isolation requirements, or when one of them is mission-critical and should not compete for memory with the others. Watch RAM and PHP-FPM as sites are added; this post covers what it looks like when you run out.
Before sites two and three
For each site, check that it has:
- A unique
local_pathandrepo_subtree_path - A unique database password and unique salts in the vault
- The right branch name
- Both development and production config updated
- Uploads permissions set after transfer
- Path conversions verified with
wp db search - A lowered DNS TTL and a tested local copy
Running many sites on one Trellis box and doing it differently? Find me on Mastodon at @jfrumau@mastodon.social.