Running Ghost with nginx and certbot in a Docker stack

In this note you will learn to run a blog with popular CMS Ghost in Docker contaniers. As a matter of fact, I will show you how you can run multiple Ghost blogs and nginx proxy in the same stack. So you just pop a docker-compose and everything is up... well, not really. But almost ๐Ÿ™ƒ.

Prerequisites

  • a linux server (or other flawour), but let's be serious here...
  • don't forget to open required ports in the firewall (http/s)
  • a domain name
  • a little time

Install Docker

My server is CentOS, so commands are for this breed. Please visit the official Docker documentation for your breed.

dnf config-manager --add repo=https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io
sudo systemctl start docker.service
sudo systemctl enable docker.service

For convenience you can add your user to the docker group, so you don't have to type sudo before every docker command to drive you nuts and you should also test the Docker installation.

sudo usermod -aG docker YOURUSER
docker run hello-world

We want to get fancy, so let's also install Docker Compose:

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
#test
docker-compose --version

Now, if you would like to run Ghost in a container we would just execute:

docker run -d \
--name ghost \
-p 2368:2368 \
-v /path/to/your/local/ghost:/var/lib/ghost/content \
-e url=http://localhost:2368 \
ghost

You should be able to pop up a browser and visit your Ghost site at localhost:2368. You could also create an external environment file, if you have more variables (like mail settings) and call it with --env-file /srv/ghost/env.list \ - but we ain't gonna do that.

Preparing the environment

Let's create the directory structure. Main directory is ghost-blog in our home folder.

cd
mkdir -p ~/ghost-blog/ghost/content ghost-blog/letsencrypt ghost-blog/nginx

For nginx to run, we need to run a temporary nginx container, extract some files and delete the temporary container. There might be a better way to do this, if you happen to know one, drop me a line.

docker run --rm --name nginxtmp -it -v ~/ghost-blog/nginx:/tmp/nginx nginx:1.21 cp -rv /etc/nginx/conf.d/ /tmp/nginx/

Now we need to create the initial config file for nginx, which we we'll do with

cd ghost-blog
nano nginx/conf.d/ghost.conf

and add the following:

server {
        server_name your.domain.com;
        listen 80;
        
        location / {
                proxy_pass	 http://ghost-prd:2368;
                proxy_set_header    X-Real-IP $remote_addr;
                proxy_set_header    Host      $http_host;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        
        #maximum upload file size
        client_max_body_size 10M;
        
}

CTRL+O to save and CTRL+X to exit nano.

After this we are going to create the docker-compose file. This file is used to control our ghost-nginx stack.

nano docker-compose.yml

Copy and paste the below config in your docker-compose.yml

version: "3.9"
# Compose file reference: https://docs.docker.com/compose/compose-file/compose-file-v3/

services:
  ghost-prd:
    image: ghost:4.32
    container_name: ghost-prd
    hostname: ghost
    restart: unless-stopped
#    ports:
#      - 2368:2368
    #with the expose option only the stack can see the port
    expose:
      - "2368"
    environment:
      #change to site url
      url: https://your.domain.com
      #set email option or comment out
      mail__transport: SMTP
      mail__from: Sender <[email protected]>
      mail__options__service: Mailgun
      mail__options__host: smtp.mailgun.org
      mail__options__port: 465
      mail__options__auth__user: [email protected]
      mail__options__auth__pass: pass
      mail__option__secure: "true"
      #logging (info, warn, error)
      logging__level: info
      #logging__rotation__enabled: "true"
      #logging__rotation__count: 10
      #logging__rotation__period: 1d
    volumes:
      - ./ghost/content:/var/lib/ghost/content

  nginx-prd:
     image: nginx:1.21
     container_name: nginx-prd
     hostname: nginx
     restart: unless-stopped
     depends_on:
       - ghost-prd
     ports:
       - 80:80
       - 443:443
     volumes:
       - ./nginx/conf.d:/etc/nginx/conf.d
       - ./letsencrypt:/etc/letsencrypt

Just to note some things here, I use Mailgun for these sort of things, but email settings should work with gmail or any other mail service.

I usually set the Ghost logging option to warn as nginx also logs and Ghost logs can get out of hand.

Using expose setting for the port makes it visible inside the stack, but it won't map it to the host. Which in my opinion is cleaner and because KISS ๐Ÿ˜›

After the docker-compose.yml is saved we should take a deep breath and pray... then run the stack:

docker-compose up -d

It should start pulling the images and setting everything up. When it is finished, you have a working Ghost blog powered by nginx in a stack, in two separate containers. The only thing that is left is installing certbot for certificates and securing the site. Certbot will do all that it is needed, install certificates and modify your ghost.conf file. To install Certbot, we need to go into our nginx container and do some mambo.

docker exec -it nginx-prd /bin/bash

Once inside the matrix, run:

apt-get update && apt-get install certbot python3-certbot-nginx -y
certbot --nginx

Pay attention when certbot requires input from you, so that you add a valid email address and secure your correct site. Now the only thing that remains is to restart the stack.

docker-compose stop
docker-compose up -d

Logs can be checked with docker-compose logs. Your stack and also your secured site should be up at https://your.domain.com. Hooray!!! That wasn't so hard now, was it? To unleash the dormant philosopher upon the world, check out https://your.domain.com/ghost

If for any reason you want to destroy your stack and set everything on fire you can run docker-compose down and your containers will be gone. You would just need to ย  also delete the ghost-blog folder in your home and you can start from scratch.

Updating

I like to control stuff, so in my docker-compose.yml I use fixed image tags instead of latest. Both can be set to latest. There is only one caveat, to which I haven't ย figured out a solution yet. When upgrading nginx, certbot will be bye-bye, so you'll have to reinstall it. The certificates and site settings are safe, as there is a mapping for those in the letsencrypt folder.

One more thing ...


How to run multiple Ghost blogs in the same stack

If you want to add another Ghost instance in the same stack, you just have to add another service to your docker-compose.yml. Like this - note the exposed port and container_name:

ghost2-prd:
    image: ghost:4.32
    container_name: ghost2-prd
    hostname: ghost2
    restart: unless-stopped
    expose:
      - "2369"
    environment:
      #change to site url
      url: https://someother.domain.com
      #set if you want Ghost to run on non-default port
      server__port: 2369
      #set email option or comment out
      mail__transport: SMTP
      mail__from: Buster <[email protected]>
      mail__options__service: Mailgun
      mail__options__host: smtp.mailgun.org
      mail__options__port: 465
      mail__options__auth__user: [email protected]
      mail__options__auth__pass: passW0Rd
      mail__option__secure: "true"
      #logging (info, warn, error)
      logging__level: warn
      #logging__rotation__enabled: "true"
      #logging__rotation__count: 10
      #logging__rotation__period: 1d
    volumes:
      - ./ghost2/content:/var/lib/ghost/content

Create folder to hold the contents of the second blog and also make another nginx config file for it:

cd
mkdir -p ghost-blog/ghost2/content
nano ghost-blog/nginx/conf.d/ghost2.conf

ghost2.conf should look like this:

server {
        server_name someother.domain.com;
        listen 80;

        location / {
                proxy_pass	 http://ghost2-prd:2369;
                proxy_set_header    X-Real-IP $remote_addr;
                proxy_set_header    Host      $http_host;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        }
}

Then:

docker-compose up -d
docker exec -it nginx-prd /bin/bash
	#inside the nginx container set up the second site
    	certbot --nginx

Stop and start your stack and you should have both sites running.

Phew. That was a long one. I hope it all makes sense ๐Ÿ˜‚ I might put it all up on Github and then it will be easier. But that's a story for another day.

Do or do not. There is no try.