OpenLogs

All tools

Ghost

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

Replaces

Paid newsletter and blog publishing services, such as Substack and Medium.

All open-source alternatives to Medium, Substack

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)

Read from github.com/TryGhost/Ghost/blob/db90cf0/LICENSE

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

TryGhost/Ghost

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

Config check passed docker compose config exited 0 on this exact file on 2026-10-22.
compose.yml Version 1
---
# 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

  1. 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.

  2. 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
  3. 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.

  4. Copy .env.example to .env (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
    cp .env.example .env
  5. Copy caddy/Caddyfile.example to caddy/Caddyfile (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
    cp caddy/Caddyfile.example caddy/Caddyfile
  6. 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.

  7. Start it (github.com/TryGhost/Docs/blob/834eb3b/install/docker.mdx)
    docker compose up -d
  8. 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

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)