Ghost CMS Setup: Deploy Your Own Professional Publishing Platform in Docker
Complete guide to self-hosting Ghost CMS with Docker Compose. Step-by-step setup for database configurations, mail parameters, and reverse proxy settings.
Database Configuration and Backend Bindings
Ghost CMS supports two primary database configurations: SQLite for local development or lightweight personal logs, and MySQL 8 for robust production environments. In a Docker-based deployment, configuring these bindings requires setting specific environment variables on the Ghost container to map connection parameters.
MySQL 8 Production Binding
For production scaling, a dedicated MySQL service container is linked to the Ghost application service. The environment variables instruct the Ghost instance to use the MySQL client driver and establish a connection pool.
database__client: Must be set tomysqlto load the MySQL client adapter.database__connection__host: Points to the MySQL service hostname (typically matching the service name defined in yourdocker-compose.yml, such asdbormysql).database__connection__user: The database user authorized to write to the schema (e.g.,ghost).database__connection__password: The secure password matching the MySQL user password.database__connection__database: The target database name (e.g.,ghost_prod).database__connection__port: The port on which MySQL is listening (default:3306).
SQLite Development/Lightweight Binding
For low-resource hosting, SQLite resides directly within the application workspace, avoiding the memory overhead of a separate database server.
database__client: Must be set tosqlite3.database__connection__filename: The absolute path within the container where the SQLite database file will be stored. By default, this is/var/lib/ghost/content/data/ghost.db.- Note: Ensure that the directory containing this database path is backed by a persistent Docker volume to prevent data loss when container instances recycle.
Transactional Mail Configuration (SMTP)
Ghost relies on transactional email delivery to handle administrative tasks such as team member invitations, staff password resets, and user authentication links. Unlike newsletter bulk delivery (which is configured via Mailgun APIs directly in the Ghost admin panel), transactional emails require SMTP parameters injected at container initialization.
The following configuration variables map host, port, authentication, and secure socket settings:
mail__transport=SMTP
mail__options__host=smtp.mailgun.org
mail__options__port=587
mail__options__auth__user=postmaster@yourdomain.com
mail__options__auth__pass=your_secure_smtp_password
mail__options__secureConnection=false
Configuration Details
- mail__transport: Defines the mail delivery engine. Set this to
SMTP. - mail__options__host: The fully qualified domain name (FQDN) of your mail server or transactional relay provider (e.g., Mailgun, SendGrid, Amazon SES, or Postmark).
- mail__options__port: Use port
587for connection upgrade via STARTTLS (recommended) or port465for explicit SSL wrapping. - mail__options__auth__user: The SMTP login identifier.
- mail__options__auth__pass: The SMTP password or API credential token.
- mail__options__secureConnection: Set to
falsewhen using port 587 (enabling opportunistic TLS upgrading) ortruewhen forcing SSL socket connection on port 465.
Docker Compose Configuration (docker-compose.yml)
The standard deployment configuration encapsulates the Ghost container alongside a MySQL 8 database service within a isolated virtual network.
Create a directory on your VPS and define the following docker-compose.yml manifest:
version: '3.8'
services:
ghost:
image: ghost:5-alpine
container_name: ghost_app
restart: always
ports:
- "2368:2368"
environment:
# The canonical URL for your site. Must match public access address.
url: https://yourdomain.com
# Database Connections
database__client: mysql
database__connection__host: db
database__connection__user: ghost
database__connection__password: DB_USER_PASSWORD_SECURE
database__connection__database: ghost_prod
# Transactional SMTP Mail Settings
mail__transport: SMTP
mail__options__host: smtp.mailgun.org
mail__options__port: 587
mail__options__auth__user: postmaster@yourdomain.com
mail__options__auth__pass: SMTP_RELAY_PASSWORD_SECURE
mail__options__secureConnection: 'false'
volumes:
- ghost_content:/var/lib/ghost/content
depends_on:
- db
db:
image: mysql:8.0
container_name: ghost_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: DB_ROOT_PASSWORD_SECURE
MYSQL_DATABASE: ghost_prod
MYSQL_USER: ghost
MYSQL_PASSWORD: DB_USER_PASSWORD_SECURE
volumes:
- db_data:/var/lib/mysql
volumes:
ghost_content:
driver: local
db_data:
driver: local
Internal Setup and Volume Mapping
To preserve blog posts, uploaded media, themes, and application data, the containers require persistent storage bindings.
Data Directories inside the Container
- Ghost Content: Located at
/var/lib/ghost/content. It holds uploaded assets, active themes, system logs, dynamic configurations, and internal media. - MySQL Data: Located at
/var/lib/mysql. It stores table definitions, indexes, transaction logs, and user metadata.
Resolving Ownership and File Permission Conflicts
The official Ghost Docker image runs under an unprivileged system user (ghost:ghost, UID/GID 999). When using host volume mounts instead of named volumes, permissions must be explicitly aligned.
Run the following commands on the host after launching the containers to correct directory permissions if file creation issues occur:
# Correcting permissions for host-mounted content directories
sudo chown -R 999:999 /path/to/host/ghost/content
To run Docker Compose in the background:
docker compose up -d
To inspect startup logs and ensure the database migration has run successfully:
docker compose logs -f ghost
Nginx Reverse Proxy with SSL Termination
Because Ghost runs internally on port 2368, an external reverse proxy is required to secure public HTTP/HTTPS ports (80/443), manage SSL/TLS handshakes, and forward web requests to the underlying Docker containers.
Nginx Configuration Manifest
Save the virtual host block to /etc/nginx/sites-available/ghost:
server {
listen 80;
listen [::]:80;
server_name yourdomain.com;
# Certbot challenge path
location /.well-known/acme-challenge/ {
root /var/www/html;
}
# Redirect all HTTP requests to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com;
# SSL Certs paths managed by Certbot
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Robust cryptographic parameter protocols
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
# Strict Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Increase body limit for large image/media uploads
client_max_body_size 50M;
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;
# Forward traffic to the Docker mapped port
proxy_pass http://127.0.0.1:2368;
}
}
Enabling and Testing Nginx
To activate the virtual host, create a symlink to sites-enabled, test the syntax configuration, and reload the service:
sudo ln -s /etc/nginx/sites-available/ghost /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Obtaining SSL Certificates via Let's Encrypt Certbot
Execute the Certbot interactive client to request and renew certificate files:
sudo certbot certonly --webroot -w /var/www/html -d yourdomain.com
This ensures zero-downtime renewal using the webroot validator method configured in the port 80 location block.