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
40 changes: 40 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,43 @@ jobs:
- name: Show service logs on failure
if: failure()
run: docker ps -a && docker compose ls || true

install:
name: Installer, from a candidate release
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: "22"

# The installer must work for an operator who is not root and is not in
# the docker group in the way the tests assume; access is established by
# asking the daemon, so this only confirms it can be asked.
- name: Confirm the daemon answers as this user
run: docker info --format '{{.ServerVersion}}'

# 80 and 443 are used by the existing-proxy and production cases, and
# must be free before they run.
- name: Confirm the ingress ports are free
run: |
set -eu
for port in 80 443; do
if (exec 3<>/dev/tcp/127.0.0.1/"$port") 2>/dev/null; then
echo "port $port is already in use on this runner" >&2
exit 1
fi
done

- name: Install local and production sites from a candidate release
env:
GD_TEST_INSTALL: "1"
run: node --test --test-timeout=1800000 tests/install-e2e.test.mjs

- name: Show what was left behind on failure
if: failure()
run: |
docker ps -a
docker compose ls || true
46 changes: 43 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ one-shot jobs (`activitypub-migrate`, `tinybird-*`) keep `restart: "no"`.
## Common Commands

```bash
# Installation
curl -fsSL .../bootstrap.sh | bash -s -- --domain example.com # release-selecting shim
./install.sh --local --no-prompt --no-start # checkout-owned installer
scripts/site.sh check # doctor: config, health, DB, ingress
scripts/site.sh list # every managed container on this host

# Core operations
docker compose up -d # Start the services for the selected mode
docker compose down # Stop all services
Expand Down Expand Up @@ -71,6 +77,7 @@ scripts/caddy.sh apply # Render, validate, install, reload, ver
# docker tests skip without a daemon)
node --test --test-timeout=120000 tests/*.test.mjs
GD_TEST_INGRESS=1 node --test --test-timeout=900000 tests/ingress.test.mjs
GD_TEST_INSTALL=1 node --test --test-timeout=1800000 tests/install-e2e.test.mjs
```

## Configuration
Expand All @@ -97,14 +104,20 @@ safely. Never source an env file; use `scripts/lib/env.sh`.
- **Data persistence**: `UPLOAD_LOCATION` and `MYSQL_DATA_LOCATION`

### Key files
- `bootstrap.sh` — curl-able release-selecting shim; bootstrap logic only
- `install.sh` — checkout-owned installer. One checkout is one site
- `.env` / `.env.example` — operator configuration
- `ghost.env` / `ghost.env.example` — application configuration
- `.ghost-docker.json` — generated installation metadata (schema v1, written from S2)
- `.ghost-docker.json` — generated installation metadata (schema v1, read and
written by `scripts/lib/meta.sh`; a missing file means "pre-metadata install",
not a broken site)
- `compose.yml` — service definitions
- `caddy/Caddyfile` — tracked generic entry point; site routes are generated
into `caddy/sites/`, operator routes live in `caddy/custom/`, global options
in `caddy/global/`
- `scripts/lib/*.sh` — shared helpers (env, fs, compose, config, caddy)
- `scripts/lib/*.sh` — shared helpers (env, fs, compose, config, caddy, meta,
preflight, install)
- `scripts/site.sh` — `list`, `check`/doctor, `info`
- `mysql-init/create-multiple-databases.sh` — MySQL multi-database initialization

## Migration from Ghost CLI
Expand All @@ -126,10 +139,37 @@ The repository includes comprehensive migration tools:
by default. This is the only host Node dependency, and `install.sh --import`
removes it

## Installer

`install.sh` is checkout-owned and installs into its own directory; `--dir`
elsewhere is refused. `bootstrap.sh` selects a release by semver order (never
lexically), clones it, and `exec`s that checkout's installer. Ghost versions are
resolved to an exact tag by asking the pulled image for its own `GHOST_VERSION`,
`GHOST_CONTENT` and `GHOST_INSTALL`; the digest goes into `.ghost-docker.json`.

Rules that must not regress:

- Installation never stops or reconfigures anything already running. A chosen
port moves out of the way; an explicitly requested busy port is an error, and
so is an occupied 80/443 in production.
- Every prompt reads `/dev/tty` and has a flag or environment-variable
equivalent. No prompt has a silent default.
- Docker access is established by asking the daemon, never from `docker` group
membership. Read-only probes have deadlines so a wedged daemon is reported
rather than hung on.
- Host tools are `docker` + `jq` (+ `git` for the bootstrap) plus the POSIX
utilities in `GD_HOST_UTILITIES`; `tests/install-e2e.test.mjs` installs with a
`PATH` of exactly that list.
- Options for steps that have not landed (`--import`, `--with supervisor`,
`--image-registry`, `--ghost-channel`, `--without`) exit 3 naming the step,
not as unknown options.

See `docs/install.md`.

## Development Workflow

1. Copy `.env.example` to `.env` and `ghost.env.example` to `ghost.env`
(both mode `0600`)
(both mode `0600`) — or let `install.sh` do it
2. Configure the required variables for the mode; run `scripts/config.sh validate`
3. Production only: `scripts/caddy.sh apply`
4. Run `docker compose up -d`
Expand Down
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ Configuration to run Ghost and its services with Docker Compose.

Requires **bash**, **Docker Engine 25.0+**, **Docker Compose v2.24+** and **jq**.

## Install

```sh
# A production site with HTTPS
curl -fsSL https://ghost.org/docker/bootstrap.sh | bash -s -- --domain example.com

# A local development site
curl -fsSL https://ghost.org/docker/bootstrap.sh | bash -s -- --local
```

`bootstrap.sh` selects a release, clones it, and runs that checkout's
`install.sh`, which does everything else: preflight, an exact Ghost version pin,
generated passwords, configuration, routing, and verifying that the site answers
through its own ingress before it says it is installed.

Every prompt has a flag, so `--no-prompt` is fully scriptable. Installation
never stops or reconfigures anything already running on this host: a port that
is in use is an error naming what holds it. See [docs/install.md](docs/install.md).

```sh
scripts/site.sh check # diagnose this site
scripts/site.sh list # every ghost-docker container on this host
```

## Configuration

Two files, deliberately separate:
Expand All @@ -19,8 +43,10 @@ cp ghost.env.example ghost.env && chmod 0600 ghost.env
scripts/config.sh validate
```

See [docs/configuration.md](docs/configuration.md) for the full contract:
value encoding, site modes, profiles, lifecycle, service aliases and metadata.
Both are written for you by `install.sh`; the examples are for hand-built
sites and for reference. See [docs/configuration.md](docs/configuration.md) for
the full contract: value encoding, site modes, profiles, lifecycle, service
aliases and metadata.

## Site modes

Expand Down Expand Up @@ -109,11 +135,14 @@ dependencies and no `package.json`: Node is a development requirement only, and
the repository is not a Node package.

```sh
# helpers, mode matrix, Caddy routes
# helpers, mode matrix, Caddy routes, installer decisions, metadata
node --test --test-timeout=120000 tests/*.test.mjs

# local and production ingress, against real containers
GD_TEST_INGRESS=1 node --test --test-timeout=900000 tests/ingress.test.mjs

# real installations from a candidate release built out of the working tree
GD_TEST_INSTALL=1 node --test --test-timeout=1800000 tests/install-e2e.test.mjs
```

Docker-dependent tests are skipped when no daemon is reachable. Set
Expand Down
Loading