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
9 changes: 7 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,17 @@ DOMAIN="example.com"
# Optional `www.` redirect target rendered into the generated Caddy routes.
# WWW_REDIRECT="www.example.com"

# Exact Ghost image pin, resolved at installation. The `next` variants install
# Ghost directly under /home/ghost rather than the older
# Requested Ghost repository/tag. Installation pins the resolved artifact below.
# The `next` variants install Ghost directly under /home/ghost rather than the older
# /var/lib/ghost/versions/<v> layout.
GHOST_IMAGE="ghost"
GHOST_VERSION="6-next-alpine"

# Written by the installer as ghost@sha256:...; overrides the repository/tag
# above for both Ghost and Tinybird sync. Updates must replace this pin too.
# Without it, manually configured sites use GHOST_IMAGE:GHOST_VERSION.
# GHOST_IMAGE_REF="ghost@sha256:..."

# Paths inside the Ghost image. The defaults match the `next` variants. Pinning
# a GHOST_VERSION with the older layout means setting both of these to
# /var/lib/ghost/content and /var/lib/ghost/current/core/server/data/tinybird.
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/shellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- main
- next
- renovate/*

# The shellcheck version is pinned so CI and a developer's machine agree.
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- main
- next
- renovate/*

env:
Expand All @@ -27,6 +28,7 @@ jobs:
run: |
set -eu
jq --version
curl --version
docker version
docker compose version

Expand Down Expand Up @@ -84,6 +86,7 @@ jobs:
docker --version
docker compose version
jq --version
curl --version

- name: Run the helper tests under bash 3.2
env:
Expand Down
11 changes: 6 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ The repository includes comprehensive migration tools:
`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`.
resolved to a digest pin by asking the pulled image for its own `GHOST_VERSION`,
`GHOST_CONTENT` and `GHOST_INSTALL`; `GHOST_IMAGE_REF` pins both Ghost and
Tinybird sync, and the digest also goes into `.ghost-docker.json`.

Rules that must not regress:

Expand All @@ -157,8 +158,8 @@ Rules that must not regress:
- 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
- Host tools are `docker`, `jq`, `curl` (+ `git` for bootstrap), plus
the Linux/macOS 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,
Expand Down Expand Up @@ -195,7 +196,7 @@ land as stacked pull requests in the dependency order given in §3.
## Important Notes

- Runtime prerequisites: `bash`, Docker Engine 25.0.0, Docker Compose v2.24.0,
and `jq` (used by the helpers for JSON). `install.sh` verifies
`jq` (JSON) and `curl` (ingress probes). `install.sh` verifies
them in preflight; `scripts/migrate.sh` already required `jq`
- Node.js is a development/test requirement only, never needed to run a site.
The exception is the legacy `scripts/migrate.sh`, retired by `install.sh --import`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Configuration to run Ghost and its services with Docker Compose.

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

## Install

Expand Down
119 changes: 15 additions & 104 deletions bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
# that list.
#
# This file contains bootstrap logic only. Installation logic belongs to the
# release, so that a site is always installed by the code it is pinned to. This
# shim carries its own version comparison rather than sourcing the repository's
# helpers, because it runs before there is a checkout to source them from.
# release, so that a site is always installed by the code it is pinned to. Release
# selection uses jq directly because there is no checkout to source yet.
set -euo pipefail

GD_BOOTSTRAP_REPO=${GD_BOOTSTRAP_REPO:-https://github.com/TryGhost/ghost-docker.git}
Expand Down Expand Up @@ -69,108 +68,20 @@ _timeout() {

# --- Release selection -----------------------------------------------------

# _semver_cmp A B -> -1, 0 or 1
#
# Full semver ordering, including prereleases: 1.2.0-beta.2 sorts before
# 1.2.0, and 1.9.0 after 1.10.0 would be wrong. Lexical sorting gets both
# backwards, which is why this is spelled out.
_semver_cmp() {
local a=${1#v} b=${2#v} ac bc ap bp i x y
ac=${a%%-*}
bc=${b%%-*}
case $a in *-*) ap=${a#*-} ;; *) ap="" ;; esac
case $b in *-*) bp=${b#*-} ;; *) bp="" ;; esac

local -a av bv
IFS=. read -ra av <<<"$ac"
IFS=. read -ra bv <<<"$bc"
for ((i = 0; i < 3; i++)); do
x=${av[i]:-0}
y=${bv[i]:-0}
[[ $x =~ ^[0-9]+$ ]] || x=0
[[ $y =~ ^[0-9]+$ ]] || y=0
((10#$x > 10#$y)) && {
printf '1\n'
return
}
((10#$x < 10#$y)) && {
printf -- '-1\n'
return
}
done

# A release outranks any prerelease of the same version.
[[ -z $ap && -z $bp ]] && {
printf '0\n'
return
}
[[ -z $ap ]] && {
printf '1\n'
return
}
[[ -z $bp ]] && {
printf -- '-1\n'
return
}

local -a ai bi
IFS=. read -ra ai <<<"$ap"
IFS=. read -ra bi <<<"$bp"
for ((i = 0; i < ${#ai[@]} || i < ${#bi[@]}; i++)); do
x=${ai[i]:-}
y=${bi[i]:-}
[[ -z $x ]] && {
printf -- '-1\n'
return
}
[[ -z $y ]] && {
printf '1\n'
return
}
if [[ $x =~ ^[0-9]+$ && $y =~ ^[0-9]+$ ]]; then
((10#$x > 10#$y)) && {
printf '1\n'
return
}
((10#$x < 10#$y)) && {
printf -- '-1\n'
return
}
else
[[ $x > $y ]] && {
printf '1\n'
return
}
[[ $x < $y ]] && {
printf -- '-1\n'
return
}
fi
done
printf '0\n'
}

# _latest_release CHANNEL
# The newest tag on a channel. Stable is vX.Y.Z; beta also considers
# vX.Y.Z-beta.N, and still prefers a release over a prerelease of the same
# version. Selection is by semver order, never by the order the remote listed
# the tags in.
# Only the release formats we publish are accepted. Sort numeric components
# with jq (already a prerequisite), independent of locale and Git user config.
# A stable release follows every beta of the same version.
_latest_release() {
local want=$1 line tag best=""
local stable='^v[0-9]+\.[0-9]+\.[0-9]+$'
local prerelease='^v[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$'

while IFS= read -r line; do
tag=${line##*refs/tags/}
tag=${tag%'^{}'}
[[ $tag =~ $stable ]] || { [[ $want == beta && $tag =~ $prerelease ]] || continue; }
if [[ -z $best ]] || [[ $(_semver_cmp "$tag" "$best") == 1 ]]; then
best=$tag
fi
done < <(git ls-remote --tags "$GD_BOOTSTRAP_REPO" 2>/dev/null)

[[ -n $best ]] || return 1
printf '%s\n' "$best"
local refs
refs=$(git ls-remote --tags --refs "$GD_BOOTSTRAP_REPO") || return 1
printf '%s\n' "$refs" | jq -Rser --arg channel "$1" '
[split("\n")[]
| capture("refs/tags/(?<tag>v(?<major>[0-9]+)\\.(?<minor>[0-9]+)\\.(?<patch>[0-9]+)(?:-beta\\.(?<beta>[0-9]+))?)$")
| select($channel == "beta" or .beta == null)
| .order = [(.major|tonumber), (.minor|tonumber), (.patch|tonumber),
(if .beta == null then 1 else 0 end), ((.beta // "0")|tonumber)]]
| sort_by(.order) | last | .tag // empty'
}

# Sourcing this file with GD_BOOTSTRAP_SOURCED=1 defines the helpers above and
Expand Down Expand Up @@ -236,7 +147,7 @@ if [[ -e $dir ]]; then
fi

missing=""
for cmd in git docker jq; do
for cmd in git docker jq curl; do
command -v "$cmd" >/dev/null 2>&1 || missing="$missing $cmd"
done
[[ -z $missing ]] || die "these are required and not installed:$missing"
Expand Down
4 changes: 2 additions & 2 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ x-site-labels: &site-labels
services:
ghost:
# Do not alter this without updating the Tinybird Sync container as well
image: ${GHOST_IMAGE:-ghost}:${GHOST_VERSION:-6-next-alpine}
image: ${GHOST_IMAGE_REF:-${GHOST_IMAGE:-ghost}:${GHOST_VERSION:-6-next-alpine}}
restart: ${RESTART_POLICY:-unless-stopped}
profiles: [local, production]
labels:
Expand Down Expand Up @@ -269,7 +269,7 @@ services:

tinybird-sync:
# Do not alter this without updating the Ghost container as well
image: ${GHOST_IMAGE:-ghost}:${GHOST_VERSION:-6-next-alpine}
image: ${GHOST_IMAGE_REF:-${GHOST_IMAGE:-ghost}:${GHOST_VERSION:-6-next-alpine}}
restart: "no"
profiles: [analytics]
labels:
Expand Down
17 changes: 13 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,13 @@ Runtime, on the server:
- Docker Engine 25.0.0 — for `healthcheck.start_interval`
- Docker Compose v2.24.0 — for `env_file` `required` and `depends_on` `required`
- `jq` — used by the helpers for JSON, including `.ghost-docker.json`
- `curl` — HTTP/HTTPS ingress probes with bounded connection and request times

`install.sh` verifies all three during preflight, and `bootstrap.sh` also needs
`install.sh` verifies these tools during preflight, and `bootstrap.sh` also needs
`git`. `scripts/migrate.sh` already required `jq`, so this is not a new
prerequisite for existing servers.

Every other host utility the scripts invoke is POSIX and is listed in
Other host utilities use supported Linux/macOS interfaces and are listed in
`GD_HOST_UTILITIES` in `scripts/lib/preflight.sh`. That list is the tool
contract: `tests/install-e2e.test.mjs` runs a complete installation with a
`PATH` built from exactly it, so a GNU-only or otherwise unusual dependency
Expand Down Expand Up @@ -294,7 +295,7 @@ migration is owned by the stack updater (S6); the changes it has to handle are:
keeps the Compose and operator settings and is no longer passed into the
Ghost container.
- `COMPOSE_PROFILES` must gain a site mode (`production` for an existing
server), and `SITE_MODE`, `URL`, `PROJECT_DIR` and an exact `GHOST_VERSION`
server), and `SITE_MODE`, `URL`, `PROJECT_DIR` and an exact `GHOST_IMAGE_REF`
pin must be added.

`scripts/migrate.sh` still migrates a Ghost-CLI installation and has been
Expand All @@ -316,5 +317,13 @@ That was changed during S1, deliberately, and §1 of
240 lines and bought nothing an operator can see.

Node.js is **not** a runtime requirement. It is used only to run the test
suite. `install.sh` verifies `docker`, `docker compose` and `jq` during
suite. `install.sh` verifies `docker`, `docker compose`, `jq` and `curl` during
preflight, and does not require Node.

## Installed image pins

The installer writes `GHOST_IMAGE_REF=ghost@sha256:...`. This is the authoritative
reference for Ghost and Tinybird sync, so a later pull cannot move the site to a
new image. `GHOST_IMAGE` and `GHOST_VERSION` record the requested repository/tag;
without `GHOST_IMAGE_REF`, they remain the fallback for manually configured sites.
Image-changing operations must update the pin and recorded metadata together.
Loading