Building an IPv6-only Caddy Docker stack for easy HTTPS with Ghost V5
AWS have announced they will begin charging for public IPv4 addresses from 1st May 2024. Lightsail 1GB images will go from $5 to $7 if they have a public IPv4 address. I am thus motivated to migrate my ageing Ubuntu 18.04 AWS Lightsail VPS' (including this very site) onto IPv6-only equivalents.
I'm rebooting my article on building a Caddy / Docker Ghost V4 docker stack. This time, I'll do it on an IPv6-only VPS, with Ghost V5.
IPv6-only Docker Architecture

Create a VPS
Once again I will be using an AWS Lightsail VPS, but the fundamentals apply to almost any VPS where you can get a Linux prompt.
Create an AWS Lightsail or equivalent $5 VPS, with at least 1GB memory
Select an Ubuntu 22.04 image with IPv6-only networking profile - it's among the Lightsail build options.
Add HTTPS to the IPv6 firewall in the network page.
Configure a domain for the VPS
As soon as the VPS starts spinning up the IPv6 address will be revealed.
Configure a domain name or subdomain name you can point to the VPS public IP address, using an "AAAA" IPv6 record (as opposed to an "A" IPv4 record).
At this time the built in AWS web console does not work with IPv6-only, you will need an external SSH tool to connect.
Install Docker and Compose
Install Docker by first following this Digital Ocean tutorial, (Step 1 & 2). If you log in with a SSH Keyfile, in Step 2 where it says su - ${USER} it will probably be easiest to just disconnect and reconnect.
The Docker Compose setup below is based on this great DigitalOcean article (Step 1), which is just 4 commands. The second command, curl, they use in the article will not work with IPv6 so I have tweaked below.
Compose grab / without curl
Follow the link to the Releases pages and look through the assets until you find the most recent `docker-compose-linux-x86_64` entry. You might need to select the drop arrow next to Assets to see the available files. At the time of writing the latest version (used here) is 2.24.5.
Instead of the curl step, we must switch to manual. Download the docker-compose-linux-x86_64 file to a staging area (your local workstation?), make the directory in step 1, and manually copy the file from your staging area into place:
mkdir -p ~/.docker/cli-plugins/
cp docker-compose-linux-x86_64 ~/.docker/cli-plugins/docker-compose
sudo chmod +x ~/.docker/cli-plugins/docker-composeTest it's working with docker compose version
Enable ipv6 in Docker
Hold on to your butt I have news - IPv6 is not enabled in Docker by default. It's "experimental". Lol. What is the world coming to.
Create (or edit if existing) /etc/docker/daemon.json with your favourite text editor - use sudo - and put in the following:
{
"experimental": true,
"ip6tables": true
}Restart docker with sudo systemctl restart docker
Getting a base Caddy set up
Once you are ready and SSH'd into your server, we can start creating some directories for Caddy. These will be:
/data/caddy: the Caddy and Compose root/data/caddy/data: the house for certificates/data/caddy/config: JSON config files
It's good to create the data and config directories separately to allow for better support and for mounting as volatile data volumes.
Create Caddy directories
$ sudo mkdir /data
$ sudo chown ubuntu:ubuntu /data
$ mkdir -p /data/caddy/data
$ mkdir -p /data/caddy/config
$ cd /data/caddyCreate a Docker Compose file
I keep my Docker Compose primary config file in the same folder as Caddy - /data/caddy - so I can always find it, and it fits in with my backup strategy. You can keep it anywhere.
With your favourite text editor, create & open docker-compose.yml and seed with this.
networks:
ip6external:
enable_ipv6: true
ipam:
config:
- subnet: 2001:0DB8::/112 # Change these two hextets
internal:
external: false
driver: bridge
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /data/caddy/Caddyfile:/etc/caddy/Caddyfile
- /data/caddy/data:/data # Optional
- /data/caddy/config:/config # Optional
networks:
- ip6external
- internal2001:0DB8:: but that's only a documentation example. Use the first two hextets of your actual IPv6 address.Create a Caddyfile
A Caddyfile is structured to have blocks for global settings, snippets, and server. The overview is on the Caddy site here. The main things to update are your domains and email. Create with your favourite editor /data/caddy/Caddyfile and seed with this.
Of course replace myghost.mydomain.com entries with your front end site URI and [email protected] with the email you would like to attach your TLS certificates to. This will also get notifications if the regular certificate renewals fail for some reason.
{
# Global options block. Entirely optional, https is on by default
# Optional email key for lets encrypt
email [email protected]
# Optional staging lets encrypt for testing. Comment out for production.
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
myghost.mydomain.com {
reverse_proxy ghost:2368
}
www.myghost.mydomain.com {
redir https://myghost.mydomain.com{uri}
}You can see the main reverse proxy function carried out by Caddy, taking inbound traffic, and relaying it to the ghost container at the back end on private port 2368.
Point your domain
Assign your domain to the IP address of your VPS, if you haven't already. For IPv6 only, it's an AAAA record. In this case it could be myghost.mydomain.com and beyond testing if you'd like to allow a wildcard including www.myghost.mydomain.com, an entry for *.myghost.mydomain.com.
Doing a lookup for myghost.mydomain.com should resolve to the VPS IPv6 address.
Start the Caddy container
At this stage we have no application at the back end to send the traffic to, but the Caddy entry will enable a listener so it's a good time to test the basic plumbing is in place, and that you can get a certificate.
Run docker compose up -d to pull the Caddy image and start. If successful you should see the Caddy container running something like:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bbfad4339928 caddy:2-alpine "caddy run --config β¦" 23 seconds ago Up 21 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 443/udp, 2019/tcp caddy-caddy-1A curl -v to your URL with https:// prefix should show IPv6 TLS certificates in place.
Add Ghost to the docker compose file
Edit the docker-compose.yml file with your favourite editor and add this. With the development flag, Ghost V5 will run with an embedded SQLite database, similar to Ghost V4.
ghost:
image: ghost:5-alpine
restart: unless-stopped
environment:
url: https://myghost.mydomain.com # Change to your domain
NODE_ENV: development
database__connection__filename: /var/lib/ghost/content/data/ghost.db
security__staffDeviceVerification: false # If no email
volumes:
- /data/myghostapp:/var/lib/ghost/content
networks:
- internal- Create the Ghost application volume on the server with
mkdir /data/myghostapp - Change the domain in the URL parameter to be the domain or subdomain you are pointing to this server. This should match the front end domain in the Caddyfile.
- Note I have chosen to allow latest Ghost sub-versions within V5 with
5-alpineimage choice, you can opt to lock it down to specific sub-releases. If so, check Docker hub for up to date Ghost image version numbers.
Add
security__staffDeviceVerification: false to the environment section if you would like to bypass the email check.Run Ghost V5
Go nuts with docker compose up -d once more.
All going well you will have two containers running. caddy, and ghost, and after a few minutes be presented with the default Ghost homepage on your URL.

IPv6 Speed bump summary
At the time of writing, these are potential issues with IPv6 specifically.
- Github does not support IPv6 for pulling files
- Docker needs IPv6 specifically enabled (and suitable Compose entry)
- AWS does not allow Lightsail web console access .. wonderful since they are introducing a charge for public IPv4. You'll need third party SSH client.
- BlueSky cannot read a link-card from an IPv6 server (TBC)
- Bing/MS crawler does not seem to be able to pull a sitemap .. if anyone cares about Bing.
Conclusion
Only time will tell what other nasties are lurking in an IPv6-only world. But one less nasty thing will be not paying an IPv4 tax from May '24. It may only be a matter of time before other cloud providers follow suit.
While moves like this from AWS are totally a cash grab, one silver lining is it will push the adoption of IPv6.
Production Readiness
This has been a nice P.O.C. but there are many reasons you should not immediately go into production use with this as-is.
Ghost V5 in production is not recommended (by them) in "development" mode. Production mode requires an additional MariaDB/MySQL database. Although I cannot see much difference for a light usage/traffic blog.
Email configuration
Essential for user registrations, email for notifications can be enabled with six more lines added to the compose environment section for Ghost, this example from an IPv4 server, I have not tested from IPv6:
environment:
- url=https://myghost.mydomain.com # Change to your domain
- mail__transport=SMTP
- mail__options__host=smtp.yourprovider.com # Find out from provider
- mail__options__port=587 # Find out from provider
- [email protected]
- mail__options__auth__pass=Your_secure_pass
- mail__from=Cool Admin <[email protected]>It can be tested by sending invitations.
Logging, backups, cache
I have some other articles on logging, and backups. If you expect heavy traffic, consider a cache container.
Caddy security headers
This is an example I have used before, but check the Caddy docs on security headers. I've also added compression by default with encode.
{
# Global options block. Entirely optional, https is on by default
# Optional email key for lets encrypt
email [email protected]
# Optional staging lets encrypt for testing. Comment out for production.
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
myghost.mydomain.com {
encode zstd gzip
reverse_proxy ghost:2368
header {
# enable HSTS
Strict-Transport-Security max-age=31536000;
# disable clients from sniffing the media type
X-Content-Type-Options nosniff
# clickjacking protection
X-Frame-Options DENY
# keep referrer data off of HTTP connections
Referrer-Policy no-referrer-when-downgrade
# Content-Security-Policy: default-src 'self'
}
}
www.myghost.mydomain.com {
redir https://myghost.mydomain.com{uri}
}Main golf caddy photo before being enhanced courtesy of Kenan Kitchen on Unsplash.
New blog post π: Building an IPv6-only Caddy Docker stack for easy HTTPS with Ghost V5 techroads.org/building-ipv... #IPv6 #Docker #Caddy #GhostBlog
— TechRoads blog (@techroads.org) Feb 22, 2024 at 3:47 pm
[image or embed]