Ghost
Publishing platform for blogs and paid newsletters, run with Docker Compose.
- Replaces
-
Paid newsletter and blog publishing services, such as Substack and Medium.
- Difficulty
-
Difficulty: Hard
9 services (1 database or broker: mysql), 9 volume mounts, 4 env values to set, no elevated host access. Heuristic score 8 of 11: hard. A source checkout is needed, so the rating is at least medium.
- Stack
TypeScript (language), MySQL (database)
- Deployment
docker_compose
- Requirements
Not stated.
- License
-
MIT(OSI-approved open source) - Business model
-
Unknown
The README at the pinned commit does not state a business model, so it stays unknown.
Source: github.com/TryGhost/Ghost/blob/db90cf0/README.md, read on .
- Repository
GitHub stats
- Stars
- 55,588
- Forks
- 11,995
- Open issues and pull requests
- 224
- Last push
- 2026-10-22
- Latest release
- v6.72.0
- Released
- 2026-10-20
From the GitHub API on . These numbers are a snapshot, not live. GitHub detected the license as MIT.
Compose file
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/main/schema/compose-spec.json
services:
caddy:
image: caddy:2.10.2-alpine@sha256:953131cfea8e12bfe1c631a36308e9660e4389f0c3dfb3be957044d3ac92d446
restart: always
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
environment:
DOMAIN: ${DOMAIN:?DOMAIN environment variable is required}
ADMIN_DOMAIN: ${ADMIN_DOMAIN:-}
ACTIVITYPUB_TARGET: ${ACTIVITYPUB_TARGET:-https://ap.ghost.org}
volumes:
- ./caddy:/etc/caddy
- caddy_data:/data
- caddy_config:/config
depends_on:
- ghost
networks:
- ghost_network
ghost:
# Do not alter this without updating the Tinybird Sync container as well
image: ghost:${GHOST_VERSION:-6-alpine}
restart: always
# This is required to import current config when migrating
env_file:
- .env
environment:
NODE_ENV: production
url: https://${DOMAIN:?DOMAIN environment variable is required}
admin__url: ${ADMIN_DOMAIN:+https://${ADMIN_DOMAIN}}
database__client: mysql
database__connection__host: db
database__connection__user: ${DATABASE_USER:-ghost}
database__connection__password: ${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}
database__connection__database: ghost
tinybird__tracker__endpoint: https://${DOMAIN:?DOMAIN environment variable is required}/.ghost/analytics/api/v1/page_hit
tinybird__adminToken: ${TINYBIRD_ADMIN_TOKEN:-}
tinybird__workspaceId: ${TINYBIRD_WORKSPACE_ID:-}
tinybird__sync_auth_key: ${TINYBIRD_SYNC_AUTH:-}
tinybird__tracker__datasource: analytics_events
tinybird__stats__endpoint: ${TINYBIRD_API_URL:-https://api.tinybird.co}
volumes:
- ${UPLOAD_LOCATION:-./data/ghost}:/var/lib/ghost/content
depends_on:
db:
condition: service_healthy
tinybird-sync:
condition: service_completed_successfully
required: false
tinybird-deploy:
condition: service_completed_successfully
required: false
activitypub:
condition: service_started
required: false
networks:
- ghost_network
db:
image: mysql:8.0.44@sha256:f37951fc3753a6a22d6c7bf6978c5e5fefcf6f31814d98c582524f98eae52b21
restart: always
expose:
- "3306"
environment:
MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD:?DATABASE_ROOT_PASSWORD environment variable is required}
MYSQL_USER: ${DATABASE_USER:-ghost}
MYSQL_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}
MYSQL_DATABASE: ghost
MYSQL_MULTIPLE_DATABASES: activitypub
volumes:
- ${MYSQL_DATA_LOCATION:-./data/mysql}:/var/lib/mysql
- ./mysql-init:/docker-entrypoint-initdb.d
healthcheck:
test: mysqladmin ping -p$$MYSQL_ROOT_PASSWORD -h 127.0.0.1
interval: 1s
start_period: 30s
start_interval: 10s
retries: 120
networks:
- ghost_network
traffic-analytics:
image: ghost/traffic-analytics:1.0.498@sha256:22aeb87aaf288128edb11c9da40e50098e84261d2666050dc4a4892f66d554b9
restart: always
expose:
- "3000"
volumes:
- traffic_analytics_data:/data
environment:
NODE_ENV: production
PROXY_TARGET: ${TINYBIRD_API_URL:-https://api.tinybird.co}/v0/events
SALT_STORE_TYPE: ${SALT_STORE_TYPE:-file}
SALT_STORE_FILE_PATH: /data/salts.json
TINYBIRD_TRACKER_TOKEN: ${TINYBIRD_TRACKER_TOKEN:-}
TINYBIRD_SYNC_AUTH: ${TINYBIRD_SYNC_AUTH:-}
LOG_LEVEL: debug
profiles: [analytics]
networks:
- ghost_network
activitypub:
image: ghcr.io/tryghost/activitypub:1.2.15@sha256:24b48f68837f01b7adec5eaf8519af504189c441656e9c12ef0976d7953f8935
restart: always
expose:
- "8080"
volumes:
- ${UPLOAD_LOCATION:-./data/ghost}:/opt/activitypub/content
environment:
# See https://github.com/TryGhost/ActivityPub/blob/main/docs/env-vars.md
NODE_ENV: production
MYSQL_HOST: db
MYSQL_USER: ${DATABASE_USER:-ghost}
MYSQL_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}
MYSQL_DATABASE: activitypub
LOCAL_STORAGE_PATH: /opt/activitypub/content/images/activitypub
LOCAL_STORAGE_HOSTING_URL: https://${DOMAIN}/content/images/activitypub
depends_on:
db:
condition: service_healthy
activitypub-migrate:
condition: service_completed_successfully
profiles: [activitypub]
networks:
- ghost_network
# Supporting Services
tinybird-login:
build:
context: ./tinybird
dockerfile: Dockerfile
working_dir: /home/tinybird
command: /usr/local/bin/tinybird-login
volumes:
- tinybird_home:/home/tinybird
- tinybird_files:/data/tinybird
profiles: [analytics]
networks:
- ghost_network
tty: false
restart: no
tinybird-sync:
# Do not alter this without updating the Ghost container as well
image: ghost:${GHOST_VERSION:-6-alpine}
command: >
sh -c "
if [ -d /var/lib/ghost/current/core/server/data/tinybird ]; then
rm -rf /data/tinybird/*;
cp -rf /var/lib/ghost/current/core/server/data/tinybird/* /data/tinybird/;
echo 'Tinybird files synced into shared volume.';
else
echo 'Tinybird source directory not found.';
fi
"
volumes:
- tinybird_files:/data/tinybird
depends_on:
tinybird-login:
condition: service_completed_successfully
networks:
- ghost_network
profiles: [analytics]
restart: no
tinybird-deploy:
build:
context: ./tinybird
dockerfile: Dockerfile
working_dir: /data/tinybird
command: >
sh -c "
tb-wrapper --cloud deploy
"
volumes:
- tinybird_home:/home/tinybird
- tinybird_files:/data/tinybird
depends_on:
tinybird-sync:
condition: service_completed_successfully
profiles: [analytics]
networks:
- ghost_network
tty: true
activitypub-migrate:
image: ghcr.io/tryghost/activitypub-migrations:1.2.15@sha256:c6c6f9160b39bfcdb1fad0a7fca8bac05ad45a7393290a2e9cdcc4b6f1fd00be
environment:
MYSQL_DB: mysql://${DATABASE_USER:-ghost}:${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}@tcp(db:3306)/activitypub
networks:
- ghost_network
depends_on:
db:
condition: service_healthy
profiles: [activitypub]
restart: no
volumes:
caddy_data:
caddy_config:
tinybird_files:
tinybird_home:
traffic_analytics_data:
networks:
ghost_network:
From a separate packaging repository: github.com/TryGhost/ghost-docker/blob/fcaf12f/compose.yml.
Compose file from TryGhost/ghost-docker, used inside a checkout of that repository (documented setup B).
Install
- Docker with the Compose plugin and git (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
A host with Docker, the Docker Compose plugin and git. This entry needs a checkout of the project repository.
- Get the source at commit fcaf12f (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
git init ghost-docker cd ghost-docker git remote add origin https://github.com/TryGhost/ghost-docker.git git sparse-checkout init --cone git sparse-checkout set caddy mysql-init tinybird git fetch --depth 1 --filter=blob:none origin fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f git checkout FETCH_HEAD - The compose file in the checkout: compose.yml (shown here for reading) (github.com/TryGhost/ghost-docker/blob/fcaf12f/compose.yml)
Use the compose file shown above.
- Copy .env.example to .env (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
cp .env.example .env - Copy caddy/Caddyfile.example to caddy/Caddyfile (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
cp caddy/Caddyfile.example caddy/Caddyfile - What the compose file check ran with (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
The compose file check ran in a checkout of TryGhost/ghost-docker at fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f (a sparse checkout: the top-level files and the folders caddy, mysql-init, tinybird), after .env.example was copied to .env and caddy/Caddyfile.example was copied to caddy/Caddyfile. The Docker install guide tells the reader to clone the ghost-docker repository, copy .env.example to .env and the Caddyfile example to caddy/Caddyfile, edit the domain and passwords, then run docker compose pull and docker compose up -d.
- Start it (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
docker compose up -d - Open the app (github.com/TryGhost/ghost-docker/blob/fcaf12f/compose.yml)
Open http://localhost:80. The compose file publishes host port 80 for the caddy service. The docs say to open the domain that is set as DOMAIN in .env. Caddy publishes ports 80 and 443.
Compose file check: config_ok on 2026-10-22.
Gotchas and maintenance
Gotchas
- Some services build from source. These services have a build section and no image: tinybird-login, tinybird-deploy. They need the project source next to the compose file. (github.com/TryGhost/ghost-docker/blob/fcaf12f/compose.yml, checked 2026-10-22)
- The compose file publishes host ports. Host ports: 80 (caddy), 443 (caddy). A port without a host address listens on all host network interfaces. (github.com/TryGhost/ghost-docker/blob/fcaf12f/compose.yml, checked 2026-10-22)
- The compose file mounts host folders. Host paths mounted into containers: ./caddy at /etc/caddy (caddy), ${UPLOAD_LOCATION at -./data/ghost} (ghost), ${MYSQL_DATA_LOCATION at -./data/mysql} (db), ./mysql-init at /docker-entrypoint-initdb.d (db), ${UPLOAD_LOCATION at -./data/ghost} (activitypub). (github.com/TryGhost/ghost-docker/blob/fcaf12f/compose.yml, checked 2026-10-22)
Signals
- Open issues and pull requests: 224
- Last push: 2026-10-22 (0 days before this page was built)
- Latest release: v6.72.0 on 2026-10-20 (2 days before this page was built)