After building my Ghost Docker container I wanted to make sure that everything is served encrypted over the internet at the insistence & coercion of my peers.

Setting up SSL using Caddy, Docker and Let's Encrypt is simple. Read on for steps on how to accomplish this configuration.

Introducing Let's Encrypt

Let's Encrypt is an organization dedicated to providing free, secure and trusted SSL certificates to anyone who can prove they control a web server. I haven't spent a lot of time obtaining certificates through the "old" method. I know that legacy certificates cost money and can be purchased for relatively long durations (> 1 year).

Let's Encrypt certificates are revoked after 30 days, at which point you need to request a new one. Since it can be automated this whole process has been mostly abstracted from me. I suspect that sooner or later I will run into an issue where I will need to get more familiar, at which point I can write another post!

A word on Nginx

My first attempt at using a Let's Encrypt enabled web server was through Nginx. Nginx is a mature web server with plenty of Docker support so I was certain I could get it to work with Let's Encrypt. After Googling around and making several attempts with different container templates I was stumped and ready to try alternatives.

Introducing Caddy

Caddy makes it very simple to set up an encrypted site. Caddy advertises "encryption by default" and to my pleasant surprise this set up was very simple.

Golf is great fun and I encourage you to playCaddy is also a term used in golf | Golf-Clubs by TeacherPouch LLC / CC BY-SA-NC 3.0


If you want to obtain SSL certificates through Let's Encrypt you will need to have your web server open to the internet. You might be able to get a certificate issued for an IP address, but I didn't try this. Safest bet would be to go to your registrar of choice and buy/configure a domain name before proceeding. I recommend Make sure to check the box for whois guard.


Caddy does not (yet as of writing) have a Docker official repository. I used abiosoft/caddy which I highly recommend.

docker run -d -p 80:80 -p 443:443 --name caddy -v /path/to/caddyfile:/etc/Caddyfile -v /path/to/srv:/srv -v /path/to/certs:/root/.caddy abiosoft/caddy

This command will spin up the default caddy web server and open ports 80 and 443 to the host. Caddy uses 3 volumes that you'll need to bind locally. These volumes can be bound anywhere on the local file system, but within the container they are located by default at

  1. /etc/Caddyfile - Caddy's config file
  2. /srv - put helloworld.html here
  3. /root/.caddy - certificates directory

Gotcha - #1 above points directly at the config file CaddyFile rather than a directory. Make sure you're pointing at a file (empty is OK)

If you followed my previous post, you will need to stop/remove your existing Ghost container if it is also serving on port 80. You can't have two containers contesting the same port. The ghost container should be spun up like so

docker run -d -v /some/local/directory:/var/lib/ghost --name myGhost ghost

Note here you don't need to specify a port. The Ghost template will use port 2368 by default.

Docker's virtual network

Docker contains its own VLAN. Each container is assigned an IP address within Docker's VLAN. So far I've been using the -p flag regularly to open ports to the host system. The -p flag is setting Docker's VLAN configuration. I'm not exactly sure how this works under the hood, but I think of it like opening ports in a firewall and/or setting up a routing table.

Before proceeding with the Caddy configuration you will need the IP address of the Ghost container. This is easy!

docker inspect myGhost

Gotcha - you can docker inspect both a container template and a container instance. Make sure you're targeting your container instance directly by name

This command will spit out the JSON configuration of the specified container. Inside this configuration you'll see something like

            "Networks": {
            "bridge": {
                "IPAMConfig": null,
                "Links": null,
                "Aliases": null,
                "NetworkID": "someSha",
                "EndpointID": "someSha",
                "Gateway": "",
                "IPAddress": "",
                "IPPrefixLen": 16,
                "IPv6Gateway": "",
                "GlobalIPv6Address": "",
                "GlobalIPv6PrefixLen": 0,
                "MacAddress": "someMac"

IP address is what you're looking for. Save this for later.

Configure Caddy

Now that you have both Ghost and Caddy containers running, its time to get Caddy serving Ghost over SSL. Since you've no doubt followed my prerequisite, you also have a domain name handy, already resolving to the IP address of your web server.

SSL is probably more secure than this doorLook for the padlock on your browser | Lock-2 by TeacherPouch LLC / CC BY-SA-NC 3.0

You may want to take a few minutes getting familiar with the CaddyFile syntax. In this example, our objective is to configure Caddy as a reverse proxy. For this you'll use a directive called proxy. In the end, the config looks like {
    proxy /
} {
    proxy /

After saving the CaddyConfig, restart your Caddy container

docker restart caddy

Gotcha - Docker IP addresses are not static by default! Keep this in mind when rebooting containers or the Docker engine. You may need to revisit this config.

After a few seconds, Caddy should negotiate a certificate with Let's Encrypt and you should be serving your site over SSL.

If you're still running into issues, make sure that your web server can be accessed over the internet on port 80 and 443. If traffic is being blocked by a firewall or for some other reason, Let's Encrypt will not be able to negotiate a certificate.

If you're still running into issues, check out my post on common troubleshooting steps.

Good luck!

Title Image | Cargo Container in Gatun Locks by skjoiner / CC BY-NC-ND 2.0