Setup your own blog with Ghost and Docker
This tutorial walks you through setting up a professional Ghost blog using Docker. We'll go beyond the basic docker run command to cover essential configurations for long-term stability and email integration.

tldr; The decision to use Docker is simply a matter of personal preference. While Docker isn't simple, it's a valuable investment for long-term site maintenance, especially when it comes to upgrading your site with the latest libraries and fixes. It will require some effort to handle the initial setup and troubleshooting, but later on, all you'll need to do is rundocker pull
anddocker compose up
. So, it's definitely worth a try, right?
Cloud provider
A decent machine is required to run Docker smoothly. I think it would need at least 2 vCPUs and 2GB of RAM for a production Ghost site.
I've spent countless years running websites, from static sites to WordPress and Ghost. Throughout those years, I've also tried several services like AWS, DigitalOcean, and Linode, and I'm currently on Hetzner. The order of these chosen providers already implies my suggestion as of the time of this post.
Mail provider
Although Mailgun is recommended by Ghost team and the UI, I would recommend to use Mailtrap.io as the process to setup one is quite straightforward. You'll need to provide your credit/debit card for verification but it won't cost you anything. (I do not know anything about Mailtrap before setting up my own host at this address).
tldr; I was unable to verify my mobile number with Mailgun. After using sms-pool for one in US, it blocked my account. So don't bother with it if you don't have plan to pay a subscription.
Docker setup
You can definitely ask an AI about steps to setup docker environment on a cloud host. For me, I just followed the steps at https://docs.docker.com/engine/install/ubuntu/
compose.yml
networks:
ghost_internal:
name: ghost_internal
internal-network:
external: true
services:
ghost:
image: ghost:5
container_name: ghost
restart: unless-stopped
environment:
# URL where your Ghost site will be accessible
url: https://idealweek.net
# Database config
database__client: mysql
database__connection__host: db
database__connection__user: ghost
database__connection__password: <replace_with_your_db_pass>
database__connection__database: ghost_prod
deploy:
resources:
limits:
memory: 512M
depends_on:
- db
volumes:
- /volumes/ghost:/var/lib/ghost/content
- /volumes/conf.d/ghost.config.production.json:/var/lib/ghost/config.production.json
networks:
- ghost_internal
- internal-network
db:
image: mysql:8
container_name: ghost_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_pass
MYSQL_DATABASE: ghost_prod
MYSQL_USER: ghost
MYSQL_PASSWORD: <replace_with_your_db_pass>
deploy:
resources:
limits:
memory: 512M
volumes:
- /volumes/ghost-db:/var/lib/mysql
networks:
- ghost_internal
While this may all seem straightforward, there are a few important points to consider:
- I prefer non-Alpine images because Alpine images often require manual library installations for more complex integrations.
- It is highly recommended that you map your site's content (
/var/lib/ghost/content
) to a directory on your host machine. I've had many bad experiences with volume setups that have suddenly become corrupted. The same goes for your database content (/var/lib/mysql
). - Limit the RAM for your MySQL instance; this is VERY important! The database is known to be a memory hog, so if you don't set a limit, it will consume as much as it can. This can eventually lead to the host OS killing the process.
config.production.json
As defined in the compose.yml
, it is mapped to host file so we can update easily. Here is a sample content:
{
"url": "http://localhost:2368",
"server": {
"port": 2368,
"host": "::"
},
"mail": {
"from": "'Ideal Week' <no-reply@idealweek.net>",
"transport": "SMTP",
"options": {
"service": "Mailtrap",
"host": "live.smtp.mailtrap.io",
"port": 587,
"auth": {
"user": "api",
"pass": "<replace_with_your_api_key>"
}
}
},
"logging": {
"transports": [
"file",
"stdout"
]
},
"process": "systemd",
"security": {
"staffDeviceVerification": true
},
"paths": {
"contentPath": "/var/lib/ghost/content"
}
}
Bonus
Code syntax highlighting with Prism
Add the snippet below to Settings → Code injection → Site header:
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/autoloader/prism-autoloader.min.js" defer></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism.min.css">
Katex integration for Mathematical formula
Add the snippet below to Settings → Code injection → Site header:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css" integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ" crossorigin="anonymous">
<!-- The loading of KaTeX is deferred to speed up page rendering -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js" integrity="sha384-VQ8d8WVFw0yHhCk5E8I86oOhv48xLpnDZx5T9GogA/Y84DcCKWXDmSDfn13bzFZY" crossorigin="anonymous"></script>
<!-- To automatically render math in text elements, include the auto-render extension: -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js" integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
- Inline formula will be between
\(
and\)
, for ex:\( x ^{2} \)
. I intentionally add a space betweenx
and ^ because Ghost will process the ^ character and the formula won't display correctly. - Center large display the formula by
\[
and\]
- Use https://latexeditor.lagrida.com/ for composing a formula with ease.
Comments ()