Self hosted Ghost blog

Overview
There is plenty of options to start blogging on the Web. Lot of existing services allowing to blog and also some solutions to self host blog on your own. And why to self-host you can ask? Just because it's free (although has limitations comparing to paid version) and you can have full control over the setup on your sever. I recommend self-hosting option for tech-savvy people.
The nice thing about Ghost is community, not that big as WordPress but still can find some answers or ask for help on the forum https://forum.ghost.org/
LLM comparison for self hosting options:
System | Type | Tech Stack | Best For | Strengths | Weaknesses |
---|---|---|---|---|---|
Ghost | Dynamic CMS | Node.js | Writers, blogs, newsletters, monetization | Fast, clean, built-in subscriptions & API | Smaller ecosystem |
WordPress | Dynamic CMS | PHP + MySQL | Blogs → full websites & e-commerce | Huge plugin/theme library, very flexible | Can be heavy/bloated |
Hugo | Static Generator | Go | Developer-friendly, fast blogs | Super fast, secure, no database needed | No built-in dynamic features |
Jekyll | Static Generator | Ruby | GitHub Pages, dev blogs | Works with GitHub Pages, Markdown + Git workflow | Slower builds, dev focus |
WriteFreely | Lightweight CMS | Go | Minimalist blogs, federated writing | Clean, lightweight, ActivityPub support | Very limited customization |
Plume | Federated CMS | Rust | Decentralized, social publishing | Federated, privacy-friendly | Early stage, small ecosystem |
⚡ Quick take:
- Ghost → Best for modern blogs/newsletters with subscriptions.
- WordPress → Best all-arounder if you want flexibility.
- Hugo/Jekyll → Best for developers who like static, fast sites.
- WriteFreely/Plume → Best for minimalists or Fediverse enthusiasts.
Installing
There are 2 ways of installing Ghost system on the VPS with Ubuntu. Either with Ghost CLI or Docker compose. Currently Docker compose is in "preview" but seems to be reliable and should become more mature in the future.
The Ghost CLI option seems to have multiple steps, it also requires to install stuff on the machine. I personally prefer using Docker which is more elegant, do not have to install dependencies directly on the machine and most of the time just works smooth and elegant.
Prerequisites
Pre-installed on VPS
- Ubuntu 22
- Nginx installed and configured - will not use Caddy
- Certbot
- domain configured with A record to point to server
Steps
Clone repository. There will be compose.yml
file which is Docker compose for setting up all needed services
git clone https://github.com/TryGhost/ghost-docker.git /opt/ghost && cd /opt/ghost
Copy example env configuration to .env file\
cp .env.example .env
setup all required envs in the file
DOMAIN
: this is your custom domain for your Ghost site e.g. mysite.comADMIN_DOMAIN
: a separate domain for your Ghost admin e.g. admin.mysite.com (Optional, recommended)DATABASE_ROOT_PASSWORD
: generate a random password withopenssl rand -hex 32
DATABASE_PASSWORD
: generate a random password usingopenssl rand -hex 32
- SMTP Email section : your chosen SMTP service’s configuration (See: email config docs)
In the configuration it was mentioned we use Nginx. Because of that it's better to not use the Caddy that is used with Ghost. It is totally fine to remove this section from docker-compose and just put proper proxying in the Nginx configuration.
location / {
proxy_pass http://127.0.0.1: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 $scheme;
}
2368
is the standard port, it should be exposed on host in the Docker compose configuration so Nginx can reach the container.
Changes into Docker compose file
Expose the Ghost service port
ports:
- "2368:2368"
expose:
- "127.0.0.1:${GHOST_PORT:-2368}:2368"
remove the Caddy service as Nginx is used
You might also encounter problem with not supported CPU architecture (CPU does not support x86-64-v2
) when running MySQL 8 which is required in newer versions of Ghost. In such case it is fine to use the Oracle image version that is built for this architecture. Update db
image to below
image: mysql:8.4.0-oraclelinux8
More details in
In the end run
docker compose pull
docker compose up -d
Services should start up in the order and Ghost should be exposed on the domain, under the domain.com/ghost
where you can finish the setup. Unless you specified different admin domain. It is recommended to configure admin account before opening service to public. You can do this with port forwarding and SSH to the VPS.
Updating Ghost
https://docs.ghost.org/install/docker#future-maintenance
Read before updates if there are any breaking changes or migrations needed.
Updating is quite simple.
Assuming the Ghost was run with docker compose from cloned repository provided by Ghost team and it's under the /opt/ghost
folder.
git clone https://github.com/TryGhost/ghost-docker.git /opt/ghost && cd /opt/ghost
Have to pull the changes from upstream. You might have some changes done locally to e.g. remove Caddy from docker-compose to use self-hosted Nginx. Can try simply rebase with autostashing git pull --rebase --autostash
Then run to update images and run the containers again using new images.
docker compose pull
docker compose down
docker compose up -d
Web analytics
It is not required to run the blog but very useful to observe some web analytics. The setup is well described on the Ghost page.
However I did not find it that easy how to set it up. I signed up with OAuth first and tried to create the workspace and project but there was number of steps to execute on the server. The docker compose run --rm tinybird-login
command tried to create new workspace but it stack, seems like the server process did not catch the workspace created on Tinybird account. I then installed the Tinybird CLI and use the command tb login
to log in but tb
was not recognized 🤷♂️
I then tried docker compose run --rm tinybird-login
again and on the web page selected the workspace created before. And it somehow worked, I got message the authentication was successful.