Deploying a Ghost blog using Docker Containers

A guide to deploying a Ghost blog using Docker.

Deploying a Ghost blog using Docker Containers
Photo by Tandem X Visuals / Unsplash

Recently I decided to share information I've gathered using these blog posts. This blog runs on a platform called Ghost and I expose it to the public internet using a Cloudflare tunnel.

In this post I'm going over a very basic deployment that exposes ghost on the container host's IP address on port 80 using an Nginx container as a reverse proxy to the Ghost app container.

I like to use individual Nginx instances as a front-end for deploying containerized apps so I can control more granular access before exposing to the public internet though a reverse proxy that handles SSL termination or a Cloudflare tunnel. The Nginx configuration shown in this post will be super generic as to make sure you can get up and running right away.

At the end of this guide, there will a linked post going over setting up and adding a Cloudflare tunnel to expose your Ghost blog or other containers to the public internet.

To get started, create the directory Ghost will live in and then create the docker-compose.yml file:

mkdir -p /opt/ghost
cd /opt/ghost
nano ./docker-compose.yml
version: "3.3"
services:
  ghost-frontend:
    image: nginx:1.15-alpine
    container_name: ghost-frontend
    restart: unless-stopped
    volumes:
      - ./nginx/ghost.conf:/etc/nginx/conf.d/ghost.conf
    ports:
      - 80:80
    depends_on:
      - ghost-app
    networks:
      - ghost
      
  ghost-app:
    container_name: ghost-app
    image: ghost:latest
    restart: always
    depends_on:
      - ghost-mysql
    environment:
      url: http://blog.example.com
      database__client: mysql
      database__connection__host: ghost-mysql
      database__connection__user: mysql-user
      database__connection__password: mysql-password
      database__connection__database: ghost
    volumes:
      - ./content:/var/lib/ghost/content
    networks:
      - ghost
      
  ghost-mysql:
    container_name: ghost-mysql
    image: mysql:8
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: mysql-root-password
      MYSQL_USER: mysql-user
      MYSQL_PASSWORD: mysql-password
      MYSQL_DATABASE: ghost
    volumes:
      - ./mysql8:/var/lib/mysql
    networks:
      - ghost

networks:
  ghost:
    name: ghost
    driver: bridge      

docker-compose.yml

After the compose file is created, add the Nginx directory and paste in the following nginx configuration file:

mkdir -p /opt/ghost/nginx
cd /opt/ghost/nginx
nano /opt/ghost/nginx/ghost.conf

This generic Nginx reverse proxy configuration will allow you to access your Ghost environment via the container host's IP address directly to the project's Nginx container.

server {
        listen 80 default_server;

    location / {
        proxy_pass http://ghost-app:2368;
        proxy_set_header Range $http_range;
        proxy_set_header If-Range $http_if_range;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Access your newly running Ghost instance

Open a browser and navigate to http://docker-ip-address. You should be welcomed by the new Ghost environment.

To get started managing the blog, navigate to http://docker-ip-address/ghost and create the initial account. The initial account created will be the admin account for your ghost blog.


Add SSL and a domain name with a reverse proxy such as Nginx/Caddy or a Cloudflare tunnel:

Once you add another reverse proxy in front of this docker compose project's Nginx container, some extra steps are needed. The assumption with the following configuration is that on your public internet facing reverse proxy that you'll be accessing via a domain name and SSL.

You'll start by modifying your Nginx configuration at /opt/ghost/nginx/ghost.conf:

The first server block is redirecting the www subdomain to non-www to handle the server address in the docker-compose.yml file being set to largenut.com for example.

The second server block is defining the reverse proxy into the container with the only change being to comment two lines out to mitigate a redirect error.

server {
        listen 80;
        server_name www.largenut.com;
        return 301 $scheme://largenut.com$request_uri;
}
server {
        listen         80;
        server_name    largenut.com;

    location / {
        proxy_pass http://ghost-app:2368;
        proxy_set_header Range $http_range;
        proxy_set_header If-Range $http_if_range;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        
# The lines below are commented out for compatibility due to redirect errors when adding a another reverse proxy in front of this Nginx container. Be sure to leave these commented.

#       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#       proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Edit /opt/ghost/docker-compose.yml URL field:

Take note to the https in the domain. This will set the URL you will use from your reverse proxy that handles SSL termination. Also notice that the URL is the base of the domain (Ghost does not support adding multiple URLs to the configuration) - this is the reason for the redirect from www to the domain base in the Nginx configuration.

...
      url: https://largenut.com
...      

Bring your project back up with the new configuration

cd /opt/ghost
docker compose down
docker compose up -d

Use cloudflare tunnel to expose Ghost to the internet:

I go over the Cloudflare tunnel portion of the setup in this post:

https://largenut.com/cloudflare-tunnels-docker/