4 min read

Ghost blog with Nginx, Docker, Let's Encrypt and Cloudflare

Ghost blog with Nginx, Docker, Let's Encrypt and Cloudflare

At the end of this documentation you will be able to deploy a ghost site on any server, with 3 containers (nginx, percona and ghost). You will have a fully automated environment, secured with Docker and with SSL Let's Encrypt certificate, Nginx web server and mySQL Percona database management system.

Requirements

  • a server (dedicated or vps) with root shell access
  • a domain name managed by cloudflare
  • know how docker and docker-compose work

Note

This documentation is intended for CentOS but you can easily adapt it for other systems.

Part I. Configure your system

Docker

Uninstall old version :

$ sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

Install Docker :

$ sudo yum install -y yum-utils \
  device-mapper-persistent-data \
  lvm2
$ sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo
$ sudo yum install docker-ce docker-ce-cli containerd.io
$ sudo systemctl start docker
$ sudo systemctl enable docker

Install Docker Compose :

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

Installation guide is also available for Debian, Fedora, Ubuntu or with the binaries.

Cloudflare

You have to add a DNS record of type A. If you want to use a subdomain, change the name of the record (example : blog.abayard.com). If you want to use the Cloudflare CDN check that the icon on the right is orange.

Let's Encrypt

I use docker to generate ssl certificates, so the system stays clean.
At first create a cloudflare.ini file :

$ nano ~/cloudflare.ini
# Cloudflare API credentials used by Certbot
dns_cloudflare_email = [email protected]
dns_cloudflare_api_key = $YOUR_AUTH_KEY

You can put this in a script :

$ vi ~/certbot_script.sh
#!/bin/bash

docker run -ti --rm \
    -v "/etc/letsencrypt:/etc/letsencrypt" \
    -v "/root/cloudflare.ini:/cloudflare.ini" \
    certbot/dns-cloudflare:latest \
    certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /cloudflare.ini \
    -d "example.com" \
    --email [email protected] \
    --agree-tos \
    --server https://acme-v02.api.letsencrypt.org/directory

Execute it :

$ chmod +x certbot_script.sh
$ ./certbot_script.sh

You can add it to cron (example, every first day of each month at midnight : 01 0 1 * * $command).

Update your system

Don't forget to update your system (yum update, apt update && apt upgrade and so on.

Part II. Install Ghost

Prepare your environment

We will use docker-compose to quickly spawn the stack. With this method you will be able to fully deploy your site in a few minutes on any server with docker installed.

Docker Compose

Nginx

The Nginx configuration file ghost.conf :

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
    server_tokens off;
}
server {
    listen       443 ssl;
    server_name yourdomain.com;
    server_tokens off;
    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    error_log  /var/log/nginx/ghost-error.log;
    access_log /var/log/nginx/ghost-access.log;

    client_max_body_size 8M;
    client_body_buffer_size 8M;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Xss-Protection "1";

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://ghost:2368;
    }
    # I put this for restrict access to admin page, this is optionnal
    location /ghost {
        allow your_ip;
        deny all;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_pass http://ghost:2368;
}

What's in the docker compose ?

To be clean we will create 2 networks :

  • an external network for applications that need to be accessed from the outside (ghost and nginx)
  • an internal network for the backend (database container).

We will create 2 volumes that will contain the site data and the database.

We will also inject the Nginx configuration ghost.conf and mount the folder containing the SSL certificates /etc/letsencrypt/live/yourdomain.com/.

The Nginx container will listen on 80 and 443 ports. It's the only container that will listen in outside world.

version: '3.1'

services:

  nginx:
    container_name: nginx01
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./ghost.conf:/etc/nginx/conf.d/ghost.conf
      - /etc/letsencrypt:/etc/letsencrypt
    networks:
      - frontend
      - backend
    restart: unless-stopped

  ghost:
    container_name: ghost01
    image: ghost:latest
    restart: unless-stopped
    environment:
      database__client: mysql
      database__connection__host: db
      database__connection__user: ghost-user
      database__connection__password: $USER_PASSWORD
      database__connection__database: ghost
      NODE_ENV: production
    volumes:
      - data:/var/lib/ghost/content/
      - ./config.production.json:/var/lib/ghost/config.production.json
    networks:
      - frontend
      - backend

  db:
    container_name: percona01
    image: percona:latest
    volumes:
      - percona:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: $ROOT_PASSWORD
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost-user
      MYSQL_PASSWORD: $USER_PASSWORD
    networks:
      - backend
    restart: unless-stopped

networks:
  frontend:
    driver: bridge
    ipam:
     config:
       - subnet: 172.16.198.0/24
  backend:
    driver: bridge
    internal: true
    ipam:
     config:
       - subnet: 172.16.199.0/24

volumes:
  percona:
  data:

Ghost Configuration

$ cat config.production.json
{
  "url": "https://example.com",
  "server": {
    "port": 2368,
    "host": "0.0.0.0"
  },
  "database": {
    "client": "mysql",
    "connection": {
        "host": "db",
        "user": "ghost-user",
        "password": "$USER_PASSWORD",
        "database": "ghost",
        "charset": "utf8"
    }
  },
  "mail": {
    "transport": "Direct"
  },
  "logging": {
    "transports": [
      "file",
      "stdout"
    ]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  }
}

Part III. Keep up to date

For update you have just to pull the latest image :

$ docker-compose pull

Wich will answer :

docker-compose pull
Pulling nginx ... done
Pulling ghost   ... done
Pulling percona    ... done

If there is an update just execute a docker-compose up -d and your stack will restart with the latest images availables.

And that's it !

Resources

https://docs.docker.com/compose/install/
https://docs.docker.com/install/linux/docker-ce/centos/
https://ghost.org
http://nginx.org/en/docs/

Feel free to correct me if you see any typo or if something seems wrong to you.
You can send me an email or comment below.

Picture : Samuel Ferrara