# Ghost

Publishing platform for blogs and paid newsletters, run with Docker Compose.

- Replaces: [Medium](https://openlogs.fyi/alternatives/medium), [Substack](https://openlogs.fyi/alternatives/substack)
- Summary: Paid newsletter and blog publishing services, such as Substack and Medium.
- Stack: TypeScript (language), MySQL (database)
- Deployment: docker_compose
- License: MIT (osi) [source](https://github.com/TryGhost/Ghost/blob/db90cf0f34cc09b88e3ea17ab2aa9fb5997467ec/LICENSE)
- Business model: unknown. The README at the pinned commit does not state a business model, so it stays unknown. ([source](https://github.com/TryGhost/Ghost/blob/db90cf0f34cc09b88e3ea17ab2aa9fb5997467ec/README.md), read 2026-10-22)
- 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.
- Requirements: not stated
- Repository: [TryGhost/Ghost](https://github.com/TryGhost/Ghost)

## Install

1. Docker with the Compose plugin and git ([source](https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx))

   ```text
   A host with Docker, the Docker Compose plugin and git. This entry needs a checkout of the project repository.
   ```

2. Get the source at commit fcaf12f ([source](https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx))

   ```bash
   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
   ```

3. The compose file in the checkout: compose.yml (shown here for reading) ([source](https://github.com/TryGhost/ghost-docker/blob/fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f/compose.yml))

   ```yaml
   ---
   # 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:
   ```

4. Copy .env.example to .env ([source](https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx))

   ```bash
   cp .env.example .env
   ```

5. Copy caddy/Caddyfile.example to caddy/Caddyfile ([source](https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx))

   ```bash
   cp caddy/Caddyfile.example caddy/Caddyfile
   ```

6. What the compose file check ran with ([source](https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx))

   ```text
   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.
   ```

7. Start it ([source](https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx))

   ```bash
   docker compose up -d
   ```

8. Open the app ([source](https://github.com/TryGhost/ghost-docker/blob/fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f/compose.yml))

   ```text
   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. ([source](https://github.com/TryGhost/ghost-docker/blob/fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f/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. ([source](https://github.com/TryGhost/ghost-docker/blob/fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f/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). ([source](https://github.com/TryGhost/ghost-docker/blob/fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f/compose.yml), checked 2026-10-22)

### Signals

Computed from a GitHub snapshot taken 2026-10-22.

- Open issues and pull requests: 224
- Stars: 55588, forks: 11995
- Last push: 2026-10-22 (0 days ago)
- Latest release: v6.72.0 on 2026-10-20 (2 days ago)

## Sources

- [Repository: https://github.com/TryGhost/Ghost](https://github.com/TryGhost/Ghost)
- [License: https://github.com/TryGhost/Ghost/blob/db90cf0f34cc09b88e3ea17ab2aa9fb5997467ec/LICENSE](https://github.com/TryGhost/Ghost/blob/db90cf0f34cc09b88e3ea17ab2aa9fb5997467ec/LICENSE)
- [Business model: https://github.com/TryGhost/Ghost/blob/db90cf0f34cc09b88e3ea17ab2aa9fb5997467ec/README.md](https://github.com/TryGhost/Ghost/blob/db90cf0f34cc09b88e3ea17ab2aa9fb5997467ec/README.md)
- [Compose file: https://github.com/TryGhost/ghost-docker/blob/fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f/compose.yml](https://github.com/TryGhost/ghost-docker/blob/fcaf12ff693b0d1fe522d7b144c0d9102aae9e9f/compose.yml)
- [Install step: https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx](https://github.com/TryGhost/Docs/blob/834eb3b22d653c58e4b2f514920ec6ff7b9e73c0/install/docker.mdx)

---
Canonical: https://openlogs.fyi/tools/ghost  
Last updated: 2026-10-22
