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