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/