You know those articles that frequently appear around the internet with catchy titles like:
- AWS + Docker + Wordpress: Get rich in 5 minutes.
- GPC & Snowflake: How to build a successful blah blah blah.
I decided to make one. I’ll keep it brief though.
School of E is live
I have started School of E — Engineers from first principle — and because I mean it to be only content, but good content, the site is based on Ghost. Also, I simply had wanted to try Ghost out for a while.
I remember checking it out a while ago and thinking the cost to begin with a site managed by them was fair ($9/month).
Not so anymore. It’s gone. Now the basic tier is literally twice that ($18/month) and it doesn’t include the possibility to create paid subscriptions. I may never do that, maybe, or maybe I do. Anyway, just to keep that as a possibility, one has to enroll in the second tier ($35/month). The basic tier also misses a few other things and, for whatever reason, it only allows one newsletter. I don’t know what the technical limitation is (for them) to allow more than one.
Anyway, the school is named Engineers from first principle, so I decided to self host. I am grateful to the Ghost people for their open-source work, and the nice documentation they have made. After all, having a pricey service does make sense for a software that is actually open-source.
As always when making stuff with one own hands, there were a few tricks along the way, which I thought were worth sharing.
To be fair to Ghost’s team, I had to make changes to what they provide because I decided to use a Cloudflare tunnel in front of everything. Had I not, and simply followed the Ghost docs, it would probably have been simpler, maybe immediate. Let me tell you more.
Tricks worth remembering
- The tunnel should go within the same docker network where Ghost, Caddy and MySql run. In fact, I put it in the same compose file. I bet it’s possible to make it work even from outside the network, but it doesn’t feels like the correct thing to do. The edits to the compose file that Ghost provides are as below:
# compose.yml
services:
+ tunnel:
+ image: cloudflare/cloudflared:latest
+ restart: unless-stopped
+ command: tunnel run
+ environment:
+ - TUNNEL_TOKEN=abc***efg
+ networks:
+ - ghost_network
+
caddy:
-
The tunnel should direct HTTPS requests to Caddy. So, it must go to
http://caddy:80, orhttps://caddy:443. Notlocalhost, becausecaddyis the host name in the Docker network (unless you’ve changed that; don’t). This setting must be made from the Cloudflare Tunnel dashboard, and it goes under the name “Public hostnames”. I made a new public hostname formydomain.com, with path*and servicehttp://caddy:80. -
Now the trickiest of them all. When a browser requests
https://mydomain.com, the request will go to Cloudflare, which knows it must use the tunnel (because of the public hostname in the previous point). The tunnel sends the request to the Caddy container onhttp://caddy:80. Caddy does its stuff, then redirects to the Ghost container onghost:2368. All of this is happening within the internal docker network, and now there’s a problem. Ghost is made so that HTTP requests will be redirected to HTTPS. These would normally go to Caddy, but in this case they go to Cloudflare and the cycle begins again, and again, and…you get an infinite 301 redirect. The solution is to let Ghost know that the original request was already HTTPS, so it won’t do the redirect. To do that, I changed the Caddyfile as below:
# caddy/Caddyfile
- reverse_proxy ghost:2368
+ reverse_proxy ghost:2368 {
+ # 1. FIX PROTOCOL: Tells Ghost the connection was originally HTTPS
+ header_up X-Forwarded-Proto https
+ # 2. FIX HOST: Ensures Ghost knows it's the main domain (example.com)
+ header_up Host {host}
+ }
-
A simple one: Secure port for Mailgun must be 465. For the records, if you are just making a new account, they will probably block it. Email the support and explain what you are doing clearly. That solved it for me.
-
All throughout setting the site up, one must do
docker compose up -d --force-recreate ghost caddy, to ensure changes to the Caddyfile take effect. -
This is an optional one. If you are using a lean VM provider (Linode, Digital Ocean, etc.), it may be worth setting up a VPC from the start. It’s because I like to think about growth: if I ever need to scale up the service horizontally (that is, with a Docker swarm), then I need a private network (that is, a VPC) for the nodes to communicate. I mean it just in case, and — for the records — VPCs are free on Linode/Akamai.
-
If one wants a different subdomain for the Ghost Admin, like
admin.mydomain.com, which I did, then the set up is essentially repeated another time: one more public hostname in Cloudflare, and another block of code changes in the Caddyfile. Remember though, that the Admin page will be onamdmin.mydomain.com/ghost(it will be a 404 without the path/ghost). Don’t panic, the link to the code is below.
I promised this will be brief. The changes are in this commit. Also, in case it wasn’t obvious, before you can put anything on top of Ghost, like Cloudflare, you need to understand how Ghost works. Their documentation is good and at this page.
Plausible experiments
- Removing Caddy altogether could make sense given that the SSL termination is made at the Cloudflare nodes. I have not tested this.
- Do not expose Caddy’s ports 80 and 443 outside the internal network could also make sense given that the tunnel does the work. Not tested.
- Add a firewall to block all inbounds connections to the server. Yes, I mean all, even HTTPS. The tunnel is outbound only, so this should work and feels nice from a security standpoint. Not tested yet.