Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
# --- Site mode -------------------------------------------------------------
# Exactly one site mode must be selected: `local` or `production`.
# Optional per-site profiles are added to the same list: `analytics`,
# `activitypub`. Profiles are additive; adding an optional profile never
# changes the site mode.
# `activitypub`, and for a local site `mailpit`. Profiles are additive; adding
# an optional profile never changes the site mode.
COMPOSE_PROFILES="production"
SITE_MODE="production"

Expand Down Expand Up @@ -92,6 +92,7 @@ DATABASE_EXTRA_DATABASES="activitypub"
# --- Data locations -------------------------------------------------------
UPLOAD_LOCATION="./data/ghost"
MYSQL_DATA_LOCATION="./data/mysql"
# MAILPIT_DATA_LOCATION="./data/mailpit"

# --- Container logs -------------------------------------------------------
# Container logs are capped so a long-running site cannot fill the disk.
Expand All @@ -109,6 +110,16 @@ LOG_MAX_FILE="3"
#
# ACTIVITYPUB_DATABASE_NAME="activitypub"

# --- Mailpit (optional, local sites only) -------------------------------------
# Add `mailpit` to COMPOSE_PROFILES of a local site to catch the mail Ghost
# sends in a web inbox, published on the loopback interface only. Ghost sends
# to it when ghost.env's mail__* settings name it; `install --with mailpit`
# writes them. Never in production: it would catch the site's real mail.
#
# Resource cost: one long-running Go service (~20-30 MB RSS).
#
# MAILPIT_PORT="8025"

# --- Analytics (optional, per-site) ---------------------------------------
# Add `analytics` to COMPOSE_PROFILES. Tinybird credentials, workspace
# selection and schema deployment belong to this site; see TINYBIRD.md.
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ delivers it. `docs/install.md` describes what exists.
(`src/verify.ts`): `127.0.0.1` in the manager is the manager, and it cannot
reach the host's ports.

- `compose.yml` — Ghost, MySQL, Caddy, optional analytics and ActivityPub.
- `compose.yml` — Ghost, MySQL, Caddy, optional analytics and ActivityPub,
and for local sites Mailpit (`--with mailpit`; validation keeps it out of
production).
Site mode is `local` or `production`, selected in `COMPOSE_PROFILES`;
optional profiles are additive. Long-running services take
`RESTART_POLICY`; one-shot jobs keep `restart: "no"`.
Expand Down
43 changes: 42 additions & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
# Site modes are selected through COMPOSE_PROFILES and are additive:
# local ghost + db, Ghost published on 127.0.0.1:${GHOST_PORT}
# production ghost + db + caddy (HTTPS ingress)
# Optional, per-site profiles: analytics, activitypub.
# Optional, per-site profiles: analytics, activitypub, and for local sites
# mailpit.
# `supervisor` is reserved for the upgrade supervisor and defines no service yet.

x-logging: &default-logging
Expand Down Expand Up @@ -94,6 +95,9 @@ services:
activitypub:
condition: service_started
required: false
mailpit:
condition: service_healthy
required: false
networks:
ghost_network:
aliases:
Expand Down Expand Up @@ -219,6 +223,43 @@ services:
aliases:
- activitypub-${COMPOSE_PROJECT_NAME:-ghost}

# Local sites only: catches the mail Ghost sends (staff invites, password
# resets, member sign-in links) and shows it in a web inbox. Newsletters go
# through Mailgun's API, not SMTP, so they never arrive here.
mailpit:
image: axllent/mailpit:v1.31.4@sha256:b68349e3a014b90c5610bfb26b2ae36f3892d7b8cf25ee140c6c71c98d2fcf48
restart: ${RESTART_POLICY:-unless-stopped}
profiles: [mailpit]
labels:
<<: *site-labels
org.ghost.docker.role: mailpit
org.ghost.docker.lifecycle: long-running
logging: *default-logging
# The inbox, on the loopback interface only, like Ghost. SMTP stays on the
# site's network.
ports:
- "127.0.0.1:${MAILPIT_PORT:-8025}:8025"
expose:
- "1025"
environment:
MP_DATABASE: /data/mailpit.db
# Ghost may be configured with SMTP credentials; any are accepted.
MP_SMTP_AUTH_ACCEPT_ANY: "true"
MP_SMTP_AUTH_ALLOW_INSECURE: "true"
volumes:
- ${MAILPIT_DATA_LOCATION:-./data/mailpit}:/data
healthcheck:
test: [CMD, /mailpit, readyz]
interval: 30s
timeout: 10s
start_period: 30s
start_interval: 1s
retries: 5
networks:
ghost_network:
aliases:
- mailpit-${COMPOSE_PROJECT_NAME:-ghost}

# Supporting one-shot jobs.
#
# These are not long-running services: they keep `restart: "no"` so a completed
Expand Down
5 changes: 5 additions & 0 deletions docs/bundle-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ overridden:
- `process`, `logging__transports`, `logging__path`
- upgrade-adapter controls

With `install --import --with mailpit`, the importer also drops the source's
mail transport (`mail__transport`, `mail__options__*`) and writes the site's
Mailpit in its place, so a local copy never sends real mail. `mail__from` is
kept.

Public and admin URLs are mapped deliberately into `.env` (`URL`,
`ADMIN_URL`; the domains are their hosts), preserving supported path and port semantics or
rejecting an unsupported URL with a clear message. Operator overrides of
Expand Down
16 changes: 12 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ Adding one never changes the site mode:
| --- | --- | --- |
| `analytics` | `traffic-analytics` plus the Tinybird one-shot jobs | one long-running Node service (~100-200 MB RSS) and a Tinybird workspace |
| `activitypub` | `activitypub`, `activitypub-migrate` | one long-running Node service (~150-250 MB RSS), one migration job per start, one extra MySQL database |
| `mailpit` | `mailpit`, its inbox on `127.0.0.1:${MAILPIT_PORT}` | one long-running Go service (~20-30 MB RSS); its messages in `${MAILPIT_DATA_LOCATION}` |

`mailpit` is for local sites only: validation rejects it beside `production`,
where it would catch the site's real mail. Selecting the profile only runs
Mailpit; Ghost sends to it because `ghost.env` says so (`install --with
mailpit` writes those settings; see [install.md](install.md#optional-services)).

`supervisor` is reserved for the upgrade supervisor and currently defines no
service. Any other profile name is rejected by validation.
Expand All @@ -149,16 +155,17 @@ updates may touch `compose.yml`.
## Lifecycle

Long-running services (`ghost`, `db`, `caddy`, `traffic-analytics`,
`activitypub`) use `restart: ${RESTART_POLICY:-unless-stopped}`.
`activitypub`, `mailpit`) use `restart: ${RESTART_POLICY:-unless-stopped}`.

One-shot jobs (`activitypub-migrate`, `tinybird-login`, `tinybird-sync`,
`tinybird-deploy`) keep `restart: "no"`. A completed or failed job stays
stopped; the restart policy is not migration orchestration and is not
readiness.

`ghost` and `db` have real health checks: Ghost's probe requires the Admin API
to answer, and MySQL's probe requires a real client connection to the
application database. A running container or a redirect is not readiness.
`ghost`, `db` and `mailpit` have real health checks: Ghost's probe requires
the Admin API to answer, MySQL's probe requires a real client connection to
the application database, and Mailpit's asks its own readiness endpoint. A
running container or a redirect is not readiness.

Container logs are capped (`LOG_MAX_SIZE`, `LOG_MAX_FILE`) so a long-running
site cannot fill the disk.
Expand All @@ -171,6 +178,7 @@ Each service has a unique alias suffixed with the project name:
- `db-${COMPOSE_PROJECT_NAME}:3306`
- `traffic-analytics-${COMPOSE_PROJECT_NAME}:3000`
- `activitypub-${COMPOSE_PROJECT_NAME}:8080`
- `mailpit-${COMPOSE_PROJECT_NAME}:1025` (SMTP)

Generated proxy routes and helper clients use these, never the bare service
name. `COMPOSE_PROJECT_NAME` is the site's stable identity and is kept
Expand Down
34 changes: 28 additions & 6 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ directory, and day-to-day operation is plain `docker compose`.
```text
ghost-docker install [--local | --domain example.com [--admin-domain admin.example.com]
[--email ops@example.com]]
[--port 2368] [--version 6.3.1] [--with activitypub]
[--port 2368] [--version 6.3.1] [--with activitypub,mailpit]
[--no-prompt] [--no-start]
ghost-docker install --import BUNDLE [--port 2368] [--no-prompt] [--no-start]
ghost-docker install --import BUNDLE [--port 2368] [--with mailpit] [--no-prompt] [--no-start]
```

| Option | Meaning |
Expand All @@ -43,7 +43,7 @@ ghost-docker install --import BUNDLE [--port 2368] [--no-prompt] [--no-start]
| `--email EMAIL` | The ACME account email. Production only; see below. |
| `--port PORT` | The loopback port Ghost is published on, in both modes. Omitted: the first at or above 2368 that is free (see [Ports](#ports-and-your-existing-proxy)). |
| `--version VERSION` | A Ghost version (`6.3.1`, which means `6.3.1-next-alpine`) or a full image tag (`6-alpine`). Resolved to an exact digest. |
| `--with LIST` | `activitypub`. `analytics` is added after installation; see below. |
| `--with LIST` | `activitypub`, and `mailpit` for a local site. `analytics` is added after installation; see below. |
| `--no-prompt` | Never ask: every input must then be an option. |
| `--no-start` | Write the configuration and routes; create no containers. |
| `--import BUNDLE` | Import a local Ghost-CLI site from the bundle `ghost migrate-export` made; see [Importing a Ghost-CLI site](#importing-a-ghost-cli-site). |
Expand Down Expand Up @@ -75,6 +75,21 @@ job and its database; it needs nothing else.
It also sets `labs__publicAPI` in `ghost.env`, which ActivityPub and
analytics both need.

`--with mailpit`, for a local site only, runs [Mailpit](https://mailpit.axllent.org)
beside Ghost and sends Ghost's mail to it: staff invites, password resets and
member sign-in links land in a web inbox at `http://127.0.0.1:MAILPIT_PORT`
instead of going nowhere. `install` writes the SMTP settings into `ghost.env`
(`mail__transport`, `mail__options__host` set to the site's
`mailpit-${COMPOSE_PROJECT_NAME}` alias, `mail__options__port`,
`mail__options__secure`); they are yours from then on, like any other line
there. The inbox is published on the loopback interface only, on the first port
at or above 8025 that is free, chosen the way Ghost's is, and kept in
`data/mailpit` across restarts. Newsletters are sent through Mailgun's API,
not SMTP, so Mailpit never sees them. A production site cannot select it:
`--domain` with `--with mailpit` is a usage error, and `config validate`
rejects `mailpit` beside `production` in `COMPOSE_PROFILES`, because it would
catch the site's real mail.

Analytics is set up after installation: its Tinybird login is interactive, in a
browser, and Ghost waits for the Tinybird jobs to finish, so a site cannot start
with it before that login. Add it to an installed site: set the tokens with
Expand All @@ -89,7 +104,8 @@ with it before that login. Add it to an installed site: set the tokens with

1. **Refuses what it should not touch**, before anything else: a directory
that already holds a site (`.env` or `.ghost-docker.json`), stack files the
payload would write over, and non-empty `data/ghost` or `data/mysql`.
payload would write over, and non-empty `data/ghost`, `data/mysql` or
`data/mailpit`.
2. **Preflight**: everything `./ghost-docker doctor` checks (Docker and
Compose versions, platform and architecture, the site directory, file
ownership, and that a sibling container sees the directory at the same
Expand Down Expand Up @@ -205,7 +221,12 @@ Bundles are accepted as a directory, a `.tgz`, a plain `.tar`, or a `.zip`
needed beyond the launcher's own requirements.

Mail settings travel with the configuration. A local site set up to send
through a real mail service will send through it from Docker too.
through a real mail service will send through it from Docker too, unless
`--with mailpit` is given: then the source's `mail__transport` and
`mail__options__*` are left out (and named, as above), Mailpit's are written
instead, and the imported copy's mail, to real members included, goes to the
inbox. `mail__from` is kept. `--with mailpit` is the only optional service an
import takes; enable the others afterwards.

Refused, each with a message that says so:

Expand Down Expand Up @@ -314,6 +335,7 @@ its own containers, and each answer is reported for what it is:
| ghost | Its health check, which `up --wait` already required: the Admin API answers inside the container. | `ok` or `ERROR` |
| caddy | From the ghost container, over the site's network, Caddy is asked for `http://` and the host of `URL` (and of `ADMIN_URL`), and must redirect to HTTPS, which it does only for a name it serves. | `ok` or `ERROR` |
| https | Whether Caddy holds a certificate for the domain. **serving** names the issuer. **pending** means there is none yet: Caddy obtains one once the domain's DNS reaches this host, `./ghost-docker check` reports the change, and `docker compose logs caddy` shows each attempt and why it failed. | `ok` or `note` |
| mailpit | With `--with mailpit`: its health check, and from the ghost container, over the site's network, its SMTP server greets on `mailpit-${COMPOSE_PROJECT_NAME}:1025`, where Ghost sends mail. | `ok` or `ERROR` |
| published ports | The ports Docker says it published. A container cannot reach the host's own loopback interface, so they are not checked from there: opening the URL is that check. | `note` |

A production site installed before its DNS points at the host therefore passes
Expand All @@ -325,7 +347,7 @@ offered; for a private name, put `tls internal` in a `caddy/custom/` file.

```bash
./ghost-docker check # diagnose this site: host, configuration, services, database, ingress
./ghost-docker info # the recorded installation metadata
./ghost-docker info # the recorded installation metadata, and where Mailpit's inbox is
./ghost-docker list # every ghost-docker container on this host, stopped ones included
./ghost-docker config get|set|validate
```
Expand Down
Loading
Loading