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

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

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

  1. You have a server/VM/node.
  2. For your server, all networking ports should be open at the firewall/security group level.
  3. You have Docker and docker compose installed. Available in the previous article.
  4. 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.

Shivam Anand