How to Host Multiple Ghost Blogs on One Server with Docker
A daunting task for a beginner, but Ghost just made our lives much easier.
At the start of August, Ghost introduced the highly-anticipated version 6.0. It most notably features ActivityPub and native analytics. It's awesome!
But there is another change that wasn’t as marketed, but is maybe just as important. The core architecture of how Ghost is hosted on servers is changing with 7.0, and the team at Ghost are so kind to offer a detailed tutorial.
From Ghost 7.0, instances will be run with Docker and Caddy to better manage the complexity of the services now used to run Ghost. In my opinion, the installation process and hosting are straight-forward, even easier than the classic Ghost way.
Follow their installation tutorial for more, it makes everything very clear.
How do you host multiple Docker containers?
Before moving all of my Ghost instances to Docker, my first challenge was to figure out how to run them side-by-side. This tutorial from Luke Harris rescued my multiple failures.
The solution is to use IPv6 addresses and Cloudflare. Once you get the hang of it, it's very straight-forward.
Nevertheless, I found it difficult to start and found no resource that teaches exactly how to do that. So I’m doing this here to save you a couple of hours.
Use a reliable hosting provider and Cloudflare
I'm using Hetzner (CX32 cloud) as my server provider; the server has run flawlessly so far.
After adding the SSH key to log into the server securely, start by creating a new user and assigning it with sudo
privileges.
# Add a new user
adduser newuser
# Assign sudo privileges
sudo usermod -aG sudo newuser
# Set up SSH access (replace "public key" user's SSH publi key)
sudo mkdir -p /home/newuser/.ssh
echo "public-key" | sudo tee /home/newuser/.ssh/authorized_keys
# Set permissions
sudo chown -R newuser:newuser /home/newuser/.ssh
sudo chmod 700 /home/newuser/.ssh
sudo chmod 600 /home/newuser/.ssh/authorized_keys
On the other hand, handling the DNS settings through Cloudflare allows pointing the domains to IPv6 addresses instead of IPv4, using the AAAA
instead of A
records.
Assign unique IPv6 addresses
The first step is to find the IPv6 address of your server, most likely somewhere inside your provider's settings.
Once you have it, create a new IPv6 address for every Ghost instance. You can do that by accessing the netplan
folder and changing its settings and permissions.
sudo nano /etc/netplan/50-cloud-init.yaml
Find the addresses
part and add one for each Ghost instance. Include your IPv6 address and add the number at the end, like shown below.
addresses:
- aaaa:bbbb:cccc:dddd::1/64
- aaaa:bbbb:cccc:dddd::2/64
- aaaa:bbbb:cccc:dddd::3/64
- aaaa:bbbb:cccc:dddd::4/64
- aaaa:bbbb:cccc:dddd::5/64
- aaaa:bbbb:cccc:dddd::6/64
Save by pressing Ctrl+O, Enter and Ctrl+X.
etc/netplan/50-cloud-init.yaml
file. Other hosting providers might use a different file name, but it should be located in the same folder.Next, run the following commands to set permissions, apply the settings and verify the IP addresses.
# Set correct permissions
sudo chmod 600 /etc/netplan/50-cloud-init.yaml
# Generate and apply updated netplan config
sudo netplan generate && sudo netplan apply
# Verify new IP addresses
ip addr show eth0
This is almost everything you have to do outside Ghost's own tutorial to host multiple Docker instances.
Set AAAA records in Cloudflare
Next, move over to Cloudflare's DNS management area. For every domain you intend to connect to Ghost, assign it with an AAAA
record corresponding to the IPv6 address you just created. You can also set up the admin domain by assigning admin.example.com
to that same AAAA
record.
AAAA
records. Before starting, make sure your provider has this option.Install Ghost instance with Docker
This section will be the same as in Ghost Docker tutorial with one minor change.
Create a directory for each Ghost instance in the /opt
folder. The easiest way is to just run the following command and changing the /opt/ghost
folder to your desired name.
git clone https://github.com/TryGhost/ghost-docker.git /opt/ghost && cd /opt/ghost
Next, copy and paste the .env
and Caddyfile
to your new instance.
cp .env.example .env
cp caddy/Caddyfile.example caddy/Caddyfile
sudo
.Proceed to edit the .env
and Caddyfile
as you would according to Ghost Docker tutorial. Do not run any other commands just yet.
The last step is to edit the compose.yml
file, and it’s important. Edit the ports
in the Caddy settings under services
by adding the corresponding IPv6 address for a given Ghost instance.
services:
caddy:
image: caddy:2.10.0-alpine
restart: always
ports:
- "[aaaa:bbbb:cccc:dddd::2]:80:80"
- "[aaaa:bbbb:cccc:dddd::2]:443:443"
environment:
DOMAIN: ${DOMAIN:?DOMAIN environment variable is required}
ADMIN_DOMAIN: ${ADMIN_DOMAIN:-}
ACTIVITYPUB_TARGET: ${ACTIVITYPUB_TARGET:-https://ap.ghost.org}
volumes:
- ./caddy:/etc/caddy
- caddy_data:/data
- caddy_config:/config
depends_on:
- ghost
networks:
- ghost_network
Save the file by pressing Ctrl+O, Enter and Ctrl+X.
Pull Ghost image and run it
The last step you have to do is to run exactly 2 commands, and then you’re done.
# Make sure you're still inside your installation folder
docker compose pull
docker compose up -d
The instance should run without any problems. Repeat the same exact process for every new instance you want to run.
Concluding remarks
The installation is heavily based on the Ghost Docker tutorial, with the small change of assigning specific IPv6 addresses to each instance.
To set up native analytics and ActivityPub, just follow their guide. It's very straight-forward and easy to do.
Happy coding and publishing.