How to Host Multiple Ghost Blogs on a Single Server with Docker and Nginx

Learn how to efficiently host multiple Ghost blogs on a single server using Docker, Nginx, and MySQL. This guide covers setting up a scalable, secure environment with HTTPS via Let’s Encrypt.

How to Host Multiple Ghost Blogs on a Single Server with Docker and Nginx

This guide demonstrates how to host multiple Ghost blogs on a single Ubuntu server using Docker, Nginx as a reverse proxy, and MySQL as the database backend. We’ll also secure the setup with Let’s Encrypt for HTTPS.

In this example, we’ll host two Ghost blogs on the following subdomains:

  • Blog 1: blog.mydomain1.com
  • Blog 2: blog.mydomain2.com

By following this setup, you can easily scale and add more blogs in the future.

Prerequisites:

  • A basic understanding of Linux and Docker.
  • An Ubuntu server. (We used a $12/month DigitalOcean droplet with 2 GB RAM and 50 GB SSD.)
  • Access to DNS settings to configure subdomains.

Step 1: Prepare Your Server

  1. Update Your Server. Ensure your server is updated and ready:
sudo apt update && sudo apt upgrade -y
  1. Configure DNS. Add the following DNS A records to your domain provider:
  • blog.mydomain1.com → [Your Server IP]
  • blog.mydomain2.com → [Your Server IP]

If you’re using Cloudflare: Go to the SSL/TLS section and set the mode to Full or Full (Strict). Disable "Automatic HTTPS Rewrites" and "Always Use HTTPS" if they are enabled.

  1. Set Up HTTPS with Let’s Encrypt. Install Certbot and generate SSL certificates:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot certonly --standalone -d blog.mydomain1.com -d blog.mydomain2.com

Using --standalone mode, Certbot starts a temporary web server to complete the validation. Ensure that ports 80 and 443 are not in use (e.g., temporarily stop Nginx or Apache if they are running).

You can then verify that the certificates were created by listing the contents of the directory:

ls /etc/letsencrypt/live/

Step 2: Configure Docker Compose

Install Docker and Docker Compose to run Docker containers:

sudo apt install docker.io -y
sudo apt install docker-compose -y

Create a working directory to hold your configurations:

mkdir docker-dir
cd docker-dir

Create a docker-compose.yml file to define your services. Open the file for editing:nano docker-compose.yml

Insert the following configuration:

version: "3.1"

services:
  reverse-proxy:
    image: nginx
    restart: always
    container_name: reverse-proxy
    volumes:
      - /root/docker-dir/nginx.conf:/etc/nginx/nginx.conf
      - /etc/letsencrypt:/etc/letsencrypt
      - /etc/ssl:/etc/ssl
    ports:
      - 80:80
      - 443:443

  ghost-blog1:
    image: ghost:latest
    restart: always
    container_name: ghost-blog1
    ports:
      - 2368:2368
    volumes:
      - ./ghost-blog1/content:/var/lib/ghost/content
    environment:
      NODE_ENV: production
      database__client: mysql
      database__connection__host: mysql-ghost-db
      database__connection__user: root
      database__connection__password: <password>
      database__connection__database: ghost_blog1
      url: https://blog.mydomain1.com

  ghost-blog2:
    image: ghost:latest
    restart: always
    container_name: ghost-blog2
    ports:
      - 2369:2368
    volumes:
      - ./ghost-blog2/content:/var/lib/ghost/content
    environment:
      NODE_ENV: production
      database__client: mysql
      database__connection__host: mysql-ghost-db
      database__connection__user: root
      database__connection__password: <password>
      database__connection__database: ghost_blog2
      url: https://blog.mydomain2.com

  mysql-ghost-db:
    image: mysql:8.0
    restart: always
    container_name: mysql-ghost-db
    environment:
      MYSQL_ROOT_PASSWORD: <password>
    volumes:
      - ghost-database:/var/lib/mysql

volumes:
  ghost-database:

Save and exit the file.

Step 3: Configure Nginx as a Reverse Proxy

Create an Nginx configuration file to route traffic to the correct Ghost container. Open the file:nano nginx.conf

Insert the following configuration:

events {
    worker_connections 1024;
}

http {
    # Redirect HTTP to HTTPS for blog.mydomain1.com
    server {
        listen 80;
        server_name blog.mydomain1.com;
        location / {
            return 301 https://$host$request_uri;
        }
    }

    # HTTPS server block for blog.mydomain1.com
    server {
        listen 443 ssl;
        server_name blog.mydomain1.com;

        ssl_certificate /etc/letsencrypt/live/blog.mydomain1.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/blog.mydomain1.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;

        location / {
            proxy_pass http://ghost-blog1: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 https;
        }
    }

    # Redirect HTTP to HTTPS for blog.mydomain2.com
    server {
        listen 80;
        server_name blog.mydomain2.com;
        location / {
            return 301 https://$host$request_uri;
        }
    }

    # HTTPS server block for blog.mydomain2.com
    server {
        listen 443 ssl;
        server_name blog.mydomain2.com;

        ssl_certificate /etc/letsencrypt/live/blog.mydomain2.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/blog.mydomain2.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;

        location / {
            proxy_pass http://ghost-blog2: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 https;
        }
    }
}

Save and exit the file.

Step 4: Start the Services

Start your services using Docker Compose: docker-compose up -d

Don't forget to set up automatic backups! Many server providers offer built-in backup features that can help you recover your data in case of unexpected issues.

Bonus: Automatically Renew Let's Encrypt SSL Certificates

Setting up automatic renewal for your Let's Encrypt SSL certificates ensures that your blog stays secure without manual intervention. Here's how to configure it:

Step 1: Update Let's Encrypt Configuration

  1. Check your Let's Encrypt renewal configuration:
cat /etc/letsencrypt/renewal/blog.mydomain1.com.conf
  1. Modify the file to use the webroot authenticator. Add or update the following lines:
# ... other config ...
authenticator = webroot
webroot_path = /var/www/certbot
# ... other config ...
  1. Repeat this process for all your certificates.

Step 2: Update Nginx Configuration

Modify your Nginx configuration to support the ACME challenge for SSL certificate renewal:

server {
    listen 80;
    server_name blog.mydomain1.com blog.mydomain2.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}
...

Step 3: Update Docker Compose

Ensure your Docker setup supports SSL renewal by sharing the webroot directory with the reverse proxy container:

  1. Edit your docker-compose.yml file and add the following volume to the reverse proxy service:
reverse-proxy:
  ...
  volumes:
    ...
    - /var/www/certbot:/var/www/certbot # This is to renew Let's Encrypt certificates automatically
  1. Restart the Docker containers to apply changes:
docker-compose down
docker-compose up -d

Step 4: Simulate Renewal

Run a dry-run of the certificate renewal to verify everything is configured correctly:

sudo certbot renew --dry-run

If the dry-run completes successfully, your certificates will renew automatically in the future.


Bonus: Useful Docker and Ubuntu Commands

Here are some useful commands to help you manage your setup and troubleshoot common issues:

View Logs for Services

Check the logs for specific containers to troubleshoot errors or confirm that services are running smoothly:

docker logs ghost-blog1    # View logs for the first Ghost blog
docker logs ghost-blog2    # View logs for the second Ghost blog
docker logs reverse-proxy  # View logs for the Nginx reverse proxy
docker logs mysql-ghost-db # View logs for the MySQL database

Stop All Services

Shut down all containers defined in your docker-compose.yml:

docker-compose down

Start or Restart All Services

Start the containers in the background:

docker-compose up -d

Rebuild the containers (if needed) and start them in the background:

docker-compose up --build -d

Check Running Containers

List all active Docker containers to confirm they are running as expected:

docker ps

Restart a Specific Container

Restart a single container, such as the reverse proxy:

docker-compose restart reverse-proxy

Install Nano Text Editor

apt update && apt install nano -y

Wrapping Up

You now have a robust setup for hosting multiple Ghost blogs on a single server. This configuration uses Docker, MySQL for data storage, and Nginx as a reverse proxy, all secured with SSL from Let’s Encrypt. You can scale this by adding more Ghost services and updating the Nginx configuration.

Related articles:

How to Host a Ghost Blog in a Subdirectory Instead of a Subdomain
Learn how to move your Ghost blog from a subdomain to a subdirectory to boost SEO and consolidate domain authority. Step-by-step guide using Docker and Cloudflare Workers.
Guide: Migrating Your Ghost Blog to a Docker Container
Learn how to migrate your Ghost blog from a traditional file system setup to a Docker container. Step-by-step guide for a seamless transition, ensuring all your content stays intact.