In this post, we’ll demonstrate how to securely set up a Ghost blog using Docker Compose with a focus on network isolation. The stack will consist of three containers: Ghost, MySQL (version 8), and Caddy (as a reverse proxy with automatic SSL certificates provided by Let’s Encrypt). We’ll ensure that only ports 80 and 443 are exposed by Caddy, while MySQL remains isolated on an internal network.
Network Design
- web network: This public network will host Caddy, allowing external access only through Caddy.
- internal network: This private network will be used exclusively for communication between the Ghost and MySQL containers, with no external exposure.
Docker Compose Configuration
We’ll define everything in a docker-compose.yml file. This file will configure the services, networks, and volumes required for the setup.
- Create the
docker-compose.ymlfile:
services:
mysql:
image: mysql:8
container_name: mysql
networks:
- internal
environment:
MYSQL_ROOT_PASSWORD: your_root_password
MYSQL_DATABASE: ghost_db
MYSQL_USER: ghost_user
MYSQL_PASSWORD: ghost_password
volumes:
- mysql_data:/var/lib/mysql
restart: always
ghost:
image: ghost
container_name: ghost
networks:
- internal
environment:
url: https://yourdomain.com
database__client: mysql
database__connection__host: mysql
database__connection__user: ghost_user
database__connection__password: ghost_password
database__connection__database: ghost_db
volumes:
- ghost_content:/var/lib/ghost/content
restart: always
depends_on:
- mysql
caddy:
image: caddy
container_name: caddy
networks:
- web
- internal
ports:
- "0.0.0.0:80:80" # bind only to IPv4
- "0.0.0.0:443:443" # bind only to IPv4
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
restart: always
depends_on:
- ghost
networks:
web:
external: true
internal:
external: true
volumes:
mysql_data:
ghost_content:
caddy_data:
caddy_config:
- Create the Caddyfile: This file will define the reverse proxy rules for Caddy to forward requests to the Ghost container.
yourdomain.com {
header {
# enable HSTS
Strict-Transport-Security max-age=31536000;
# disable clients from sniffing the media type
X-Content-Type-Options nosniff
# clickjacking protection
X-Frame-Options DENY
# keep referrer data off of HTTP connections
Referrer-Policy no-referrer-when-downgrade
# Content-Security-Policy: default-src 'self'
}
reverse_proxy ghost:2368
}
- Start the Services: Run the following command to spin up the services:
docker-compose up -d
Breakdown of the Docker Compose Configuration
- Networks:
web: The Caddy and Ghost containers are part of this network, allowing external access to the Ghost blog only via Caddy.internal: The Ghost and MySQL containers are part of this network, isolating MySQL so that only Ghost can access it.
- MySQL:
The MySQL container is placed exclusively in the
internalnetwork, ensuring that it is not exposed to the public. Only the Ghost container can access MySQL via this network. - Ghost:
The Ghost container is connected to both
webandinternalnetworks. It needs access to theinternalnetwork to communicate with the MySQL container, and access to thewebnetwork so that Caddy can reverse proxy external traffic to it. - Caddy:
The Caddy container is responsible for handling traffic from ports 80 (HTTP) and 443 (HTTPS) and forwarding it to the Ghost container. It is connected only to the
webnetwork, ensuring that it can’t access internal services like MySQL.
Security Considerations
- Port exposure: Only ports 80 and 443 are exposed to the public, through Caddy. The MySQL container remains isolated and inaccessible from the public internet.
- Network isolation: By separating the
webandinternalnetworks, we prevent unauthorized access to the MySQL database while allowing necessary communication between Ghost and MySQL. - SSL/TLS: Caddy automatically manages SSL certificates, ensuring all public communication with the blog is encrypted.
By using Docker Compose and network isolation, this setup ensures that your Ghost blog is secure and only exposes necessary services to the public. All sensitive internal communication (e.g., between Ghost and MySQL) is safely confined to the internal network.