How to Deploy Multiple Production-Ready Ghost Websites in 5 Minutes Using Docker
Why Ghost + Docker Is a Perfect Match
If you’re building multiple websites or blogs, Ghost is one of the best tools you can pick. It’s fast, modern and made for publishing. But installing Ghost manually on different servers can quickly turn into a headache — you have to deal with Node.js versions, databases, nginx, SSL certificates and so on.
That’s where Docker comes in. Instead of wasting time setting up everything by hand, you can spin up isolated Ghost containers for each site in just minutes.
In this article, I’ll show you how to:
- Run multiple production-ready Ghost websites at the same time.
- Keep everything separated (no config conflicts).
- Deploy in 5 minutes or less with Docker.
We’ll leverage two more tools to make things even easier:
- nginx-proxy → An automatic reverse proxy that routes traffic to the right container based on your domain.
- acme-companion → Automatically fetches and renews free SSL certificates from Let’s Encrypt.
We will be using MySQL as it's the only production-ready database officially supported by Ghost. (Ghost has a built-in SQLite option for non-production environment)
Prerequisites
Before we start, make sure you have:
- A server (your laptop/desktop/whatever) or VPS/VDS (Ubuntu/Debian works fine).
- Docker and Docker Compose installed.
- A domain name for each Ghost website you want to run.
That’s it. No complex setup needed.
Step 1: Create Project Folders
Let’s say you want to host two websites:
- chess-blog.com
- travel-blog.com
Create the following structure on your server:
project/
├── docker-compose.yml
├── chess-blog/
│ └── config.production.json
├── travel-blog/
│ └── config.production.json
└── db/
└── init/
└── init.sql
Step 2: Initialize Databases and Users
We’ll create a script to set up our databases, users and privileges automatically.
db/init/init.sql
-- create databases
CREATE DATABASE IF NOT EXISTS chess_blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS travel_blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- create users
CREATE USER IF NOT EXISTS 'chess_blog_db_user'@'%' IDENTIFIED BY 'chess_blog_db_user_password';
CREATE USER IF NOT EXISTS 'travel_blog_db_user'@'%' IDENTIFIED BY 'travel_blog_db_user_password';
-- grant privileges
GRANT ALL PRIVILEGES ON chess_blog_db.* TO 'chess_blog_db_user'@'%';
GRANT ALL PRIVILEGES ON travel_blog_db.* TO 'travel_blog_db_user'@'%';
FLUSH PRIVILEGES;
Tip: generate secure passwords with:
openssl rand -base64 12
Step 3: Configure Ghost for Each Website
Ghost requires a config.production.json file for each site. Copy it from /var/lib/ghost/config.production.json inside the Ghost container, then edit it.
We’ll also add email support using Gmail. For Gmail, you need to create an App Password (not your normal login password).
To get one:
- Enable 2FA on your Google account.
- Go to Google App Passwords (2-Step Verification → App passwords)
- Type the name of your app (e.g "chessblog") to create a new app specific password
- Put the generated pass into config.production.json as your-app-password in the mail configuration.
Here’s an example config:
{
"url": "http://localhost:2368",
"server": {
"port": 2368,
"host": "::"
},
"mail": {
"transport": "SMTP",
"options": {
"host": "smtp.gmail.com",
"port": 587,
"secure": false,
"auth": {
"user": "[email protected]",
"pass": "your-app-password"
}
}
},
"logging": {
"transports": ["file", "stdout"]
},
"process": "systemd",
"paths": {
"contentPath": "/var/lib/ghost/content"
}
}
Step 4: Create docker-compose.yml
Here’s the full Docker Compose file for two Ghost websites, a shared MySQL database, nginx reverse proxy and automatic SSL.
services:
# nginx reverse proxy
nginx-proxy:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
# The vhost and conf volumes are only required
# if you plan to obtain standalone certificates
# - vhost:/etc/nginx/vhost.d
# - conf:/etc/nginx/conf.d
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- nginx-proxy-network
# acme companion for automatic SSL
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
environment:
- [email protected]
volumes_from:
- nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- nginx-proxy-network
# ghost chess blog
ghost-chess-blog:
image: ghost:5-alpine
container_name: ghost-chess-blog
restart: always
environment:
VIRTUAL_HOST: chess-blog.com
LETSENCRYPT_HOST: chess-blog.com
LETSENCRYPT_EMAIL: [email protected]
database__client: mysql
database__connection__host: mysql
database__connection__user: chess_blog_db_user
database__connection__password: chess_blog_db_user_password
database__connection__database: chess_blog_db
volumes:
- ./chess-blog/content:/var/lib/ghost/content
- ./chess-blog/config.production.json:/var/lib/ghost/config.production.json
depends_on:
- mysql
networks:
- nginx-proxy-network
# ghost travel blog
ghost-travel-blog:
image: ghost:5-alpine
container_name: ghost-travel-blog
restart: always
environment:
VIRTUAL_HOST: travel-blog.com
LETSENCRYPT_HOST: travel-blog.com
LETSENCRYPT_EMAIL: [email protected]
database__client: mysql
database__connection__host: mysql
database__connection__user: travel_blog_db_user
database__connection__password: travel_blog_db_user_password
database__connection__database: travel_blog_db
volumes:
- ./travel-blog/content:/var/lib/ghost/content
- ./travel-blog/config.production.json:/var/lib/ghost/config.production.json
depends_on:
- mysql
networks:
- nginx-proxy-network
# MySQL database for both blogs
mysql:
image: mysql:8.0
container_name: ghost-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootsecret
volumes:
- ./db/init:/docker-entrypoint-initdb.d
- ./db/mysql_data:/var/lib/mysql
networks:
- nginx-proxy-network
networks:
nginx-proxy-network:
external: true
volumes:
html:
certs:
acme:
# vhost:
# conf:
Note: we store ghost's content, configurations and database data on the host disk to prevent loss when containers are deleted or recreated and to allows you to edit the configurations easily on your host.
Step 5: Start Ghost with Docker Compose
First, create an external Docker network (so containers can talk to each other securely):
docker network create nginx-proxy
Then start everything:
docker compose up -d

Step 6: Verify Your Websites
If your DNS is set correctly, your websites will be available at:
Ghost admin panels:
https://chess-blog.com/ghost/https://travel-blog.com/ghost/
On first login, Ghost will ask you to create a super admin account.
Tip: If you don’t have DNS set up yet, add this to your local /etc/hosts:
cat /etc/hosts
127.0.0.1 chess-blog.com
127.0.0.1 travel-blog.com
By default the database is not accessible from the host, but you can connect to it from within the container:
docker exec -it ghost-mysql mysql -u root -p
Use the docker-compose.yml as a reference — all passwords are specified there.
Why This Setup Rocks
- Speed: You can create 5–10 Ghost production-ready websites in under 5 minutes.
- Isolation: Each site runs in its own container (no conflicts).
- Scalability: Want to move one website to another server? Just copy its folder and run
docker compose up -d. - Flexibility: You can later add caching, CDN, or Ezoic/AdSense easily.
Final Thoughts
If you’re running multiple projects, using Docker for Ghost is a no-brainer. You save time, keep things clean and scale effortlessly.
Next step? Try deploying 3–5 websites at once, maybe for different niches — AI, travel, food or personal notes. Each will be up and running in minutes.