Host your blogging website — Ghost with Docker, Nginx Reverse Proxy and SSL
For a simple installation of Ghost without nginx and SSL, you can read my Article

For a simple installation of Ghost without nginx and SSL, you can read my Article
A brief context
We will run Nginx and certbot on Docker and get a self-signed certificate from Let’s Encrypt
This deployment has two parts —
1. Run the deployment without certbot and run certbot manually once to get the initial certificate. This is a 1 time process. Also, nginx should have a path /.well-known/acme-challenge/, which should point to /var/www/certbot . It is called the HTTP-01 challenge. Let’s Encrypt will use this to verify your domain ownership
2. Run the deployment again, this time with certbot, which will run every 6 hours and renew the certificate when it is about to expire. This will automate SSL certificate renewal.
Dependencies
- You have a server/VM/node.
- For your server, all networking ports should be open at the firewall/security group level.
- You have Docker and docker compose installed. Available in the previous article.
- Your server IP is configured with your domain registrar
In my case, I have a GCP VM whose IP has been configured in CloudFlare with an A record pointing to my domain. Without a domain, Let’s Encrypt will not issue a certificate. Domain is mandatory.
Runbook
The First Part of the Deployment
1. Create directories & Files
sudo mkdir -p /nginx/
sudo mkdir -p /certbot/
sudo mkdir -p /ghost/
sudo mkdir -p /mysql/
sudo touch ~/docker-compose.yml
sudo touch /nginx/nginx.conf
sudo chown ubuntu /nginx/
sudo chown ubuntu /nginx/nginx.conf
2. Prepare your docker-compose.yml file
version: '3.1'
services:
ghost:
image: ghost:latest
container_name: ghost
depends_on:
- db
restart: always
ports:
- 2368:2368
environment:
# see https://ghost.org/docs/config/#configuration-options
database__client: mysql
database__connection__host: db
database__connection__user: ghost
database__connection__password: iamahardpassword
database__connection__database: ghost
# this url value is just an example, and is likely wrong for your environment!
url: http://<your-domain>.com
# contrary to the default mentioned in the linked documentation, this image defaults to NODE_ENV=production (so development mode needs to be explicitly specified if desired)
# NODE_ENV: development
NODE_ENV: production
volumes:
- /ghost:/var/lib/ghost/content
db:
image: mysql:8.0
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: iamahardpassword
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: iamahardpassword
volumes:
- /mysql:/var/lib/mysql
nginx:
image: nginx:latest
container_name: nginx-proxy
depends_on:
- ghost
volumes:
- /nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /certbot/www:/var/www/certbot
- /certbot/conf:/etc/letsencrypt
ports:
- "80:80"
- "443:443"
Modify the file and replace it with your own domain.
3. Create the nginx configuration file /nginx/nginx.conf
events {}
http {
server {
listen 80;
listen [::]:80;
server_name <your-domain>.com ;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
proxy_pass http://ghost:2368;
proxy_set_header Host $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;
}
}
}
4. You might have to open a firewall based on your Linux distro. In my case, I have Ubuntu, and I opened ports 22,80, and 443 using ufw. I will leave this to the reader to figure out the firewall, as it is different in each case scenario.
5. Docker Deploy and get the initial certificate
Bring up the temporary deployment and complete the HTTP-01 challenge
docker compose up
docker run --rm -v "/certbot/www:/var/www/certbot" -v "/certbot/conf:/etc/letsencrypt" certbot/certbot certonly --webroot --webroot-path=/var/www/certbot --agree-tos --no-eff-email --email your-email@gmail.com -d your-domain.com
This will save the certificate in /certbot/ folder. This certificate will later be picked up by the certbot Docker container.
At this point, you can also check/debug if your port 443 is open or not. In the browser, go to <your-server-ip>:443. If you see anything, your ports are open.
6. Bring down the deployment
docker compose down
The Second Part of the Deployment
7. Modify your docker-compose.yml file to include certbot
version: '3.1'
services:
ghost:
image: ghost:latest
container_name: ghost
depends_on:
- db
restart: always
ports:
- 2368:2368
environment:
# see https://ghost.org/docs/config/#configuration-options
database__client: mysql
database__connection__host: db
database__connection__user: ghost
database__connection__password: iamahardpassword
database__connection__database: ghost
# this url value is just an example, and is likely wrong for your environment!
url: http://your-domain.com
# contrary to the default mentioned in the linked documentation, this image defaults to NODE_ENV=production (so development mode needs to be explicitly specified if desired)
# NODE_ENV: development
NODE_ENV: production
volumes:
- /ghost:/var/lib/ghost/content
db:
image: mysql:8.0
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: iamahardpassword
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: iamahardpassword
volumes:
- /mysql:/var/lib/mysql
nginx:
image: nginx:latest
container_name: nginx-proxy
depends_on:
- ghost
volumes:
- /nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /certbot/www:/var/www/certbot
- /certbot/conf:/etc/letsencrypt
ports:
- "80:80"
- "443:443"
certbot:
image: certbot/certbot
container_name: certbot
restart: always
volumes:
- /certbot/www:/var/www/certbot
- /certbot/conf:/etc/letsencrypt
entrypoint: "/bin/sh -c"
command: >
"trap exit TERM; while :; do sleep 6h & wait $${!}; certbot renew; done"
Make sure to replace your domain
8. Modify /nginx/nginx.conf file
events {}
http {
client_max_body_size 5m;
server {
listen 80;
listen [::]:80;
server_name your-domain.com ;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://ghost:2368;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
The certificate files should be available at /certbot/. You can dig to find your files.
9. Run docker compose
docker compose up
10. Viola. Your site should be ready at your-domain.com
curl localhost:2368
curl <your-server-ip>:2368
curl <your-domain>:2368
curl https://<your-domain>
should give website HTML as output. Provided port 2368 and 443 are open.
Access the ghost admin panel at https://<your-domain>.com/ghost/

Backup & Restore
As long as you can back up these folders
/mysql
/ghost
/nginx
/certbot
You can restore your website anywhere.