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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ jobs:
- name: Verify the fresh install serves Flarum
run: curl -fsS http://localhost/ | grep -qi 'flarum' || { echo "::error::no Flarum markers in response"; exit 1; }

# A green install proves nothing about WHERE Flarum came from: this runner
# has network, so the old Packagist path would pass every check above just
# as well. Assert the offline path was actually taken, or the pin quietly
# stops being load-bearing the first time the guard breaks.
- name: Verify it installed from the baked skeleton, not from Packagist
run: |
logs="$(docker compose logs flarum)"
if ! echo "$logs" | grep -q 'from the baked skeleton'; then
echo "::error::fresh install did not seed from the baked skeleton"
echo "$logs" | tail -60
exit 1
fi
if echo "$logs" | grep -q 'composer create-project flarum/flarum'; then
echo "::error::fresh install resolved Flarum over the network"
exit 1
fi
echo "$logs" | grep 'from the baked skeleton'

- name: Plant a marker in the database
run: |
docker compose exec -T mariadb mariadb -uroot -p"${DB_ROOT_PASS}" flarum \
Expand Down
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ RUN curl -sS --retry 5 --retry-delay 2 --retry-connrefused \
https://getcomposer.org/installer \
| php -- --install-dir=/usr/local/bin --filename=composer

# ── The pinned Flarum skeleton ───────────────────────────────────────────────
# Flarum itself, resolved at BUILD time from the committed lock rather than
# from Packagist when a container first starts. Build-time network is fine and
# reproducible — the lock decides what gets installed. Runtime network is not:
# it made the same image tag produce different forums a week apart and left no
# offline install path at all. See issue #2 and skeleton/README.md.
#
# create-project supplies the skeleton's OWN files (index.php, the flarum CLI,
# public/, storage/) which are part of the flarum/flarum package and never
# appear in vendor/. --no-install skips dependency resolution, because the
# committed lock replaces its composer.json immediately below and decides that.
ARG FLARUM_VERSION=2.0.0-rc.5
ENV FLARUM_SKELETON=/opt/flarum-skeleton
ENV FLARUM_SKELETON_VERSION=${FLARUM_VERSION}

RUN COMPOSER_HOME=/tmp/composer composer create-project \
"flarum/flarum:${FLARUM_VERSION}" "$FLARUM_SKELETON" \
--stability=beta --no-install --no-interaction --no-progress

COPY skeleton/composer.json skeleton/composer.lock ${FLARUM_SKELETON}/

# Fail the build rather than ship a skeleton whose files and lock disagree: the
# ARG picks the package that supplies the root files, the lock picks everything
# else, and nothing else would notice them drifting apart.
RUN set -eu; \
locked="$(php -r '$l = json_decode(file_get_contents(getenv("FLARUM_SKELETON")."/composer.lock"), true); foreach ($l["packages"] as $p) { if ($p["name"] === "flarum/core") { echo ltrim($p["version"], "v"); exit; } } exit(1);')"; \
if [ "$locked" != "${FLARUM_VERSION}" ]; then \
echo "FLARUM_VERSION=${FLARUM_VERSION} but skeleton/composer.lock pins flarum/core $locked" >&2; \
exit 1; \
fi; \
cd "$FLARUM_SKELETON"; \
COMPOSER_HOME=/tmp/composer composer install \
--no-dev --no-interaction --no-progress --optimize-autoloader; \
rm -rf /tmp/composer

# Baked config + boot scripts (no runtime download).
COPY nginx.conf /etc/nginx/conf.d/flarum.conf
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ Two things set this apart:
is tested rather than assumed — including restoring a backup from an older
Flarum into a newer one.

Everything is baked into the image: nginx, php-fpm, Composer, all required PHP
extensions, and the boot script. There is **no runtime download** of a setup
script, no external services, and no telemetry.
**Flarum itself is in the image, at a pinned version.** It is not fetched from
Packagist when your container starts, so the same image tag always installs the
same forum, a first boot needs no internet access, and an upstream outage cannot
break a new install. The exact version set lives in
[`skeleton/`](skeleton/) and moves only in a reviewed pull request.

Everything else is baked in too: nginx, php-fpm, Composer, all required PHP
extensions, and the boot script. No external services, and no telemetry.

## What you get

Expand Down
37 changes: 34 additions & 3 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,24 @@ REDIS_HOST=$(clean "${REDIS_HOST:-valkey}")
REDIS_PORT=$(clean "${REDIS_PORT:-6379}")
REDIS_PASSWORD=$(clean "${REDIS_PASSWORD:-}")

# Whether the operator pinned a version themselves. Checked BEFORE defaulting,
# because the default is exactly what "they said nothing" looks like afterwards.
# Setting TARGET_FLARUM_VERSION is the deliberate opt-out from the baked
# skeleton: it means "go and resolve this from Packagist instead", which is the
# old behaviour and still supported.
TARGET_FLARUM_VERSION_SET=$([ -n "${TARGET_FLARUM_VERSION:-}" ] && echo true || echo false)
TARGET_FLARUM_VERSION=$(clean "${TARGET_FLARUM_VERSION:-^2.0}")

# Seed from the image unless asked otherwise. The guard on composer.json keeps
# this correct for an image built before the skeleton existed, where the
# directory is simply absent.
FLARUM_SKELETON="${FLARUM_SKELETON:-/opt/flarum-skeleton}"
FLARUM_SKELETON_VERSION="${FLARUM_SKELETON_VERSION:-unknown}"
if [ "$TARGET_FLARUM_VERSION_SET" = "false" ] && [ -f "$FLARUM_SKELETON/composer.json" ]; then
FLARUM_FROM_SKELETON=true
else
FLARUM_FROM_SKELETON=false
fi
REALTIME_ENABLED=$(echo "${REALTIME_ENABLED:-true}" | tr '[:upper:]' '[:lower:]')

# Restore-on-deploy: drop a backup in the mounted /restore dir (database.sql[.gz]
Expand Down Expand Up @@ -182,9 +199,23 @@ if [ ! -f "$CONFIG_FILE" ]; then

TMP_INSTALL="$WORKDIR/storage/flarum_stage"
rm -rf "$TMP_INSTALL" && mkdir -p "$TMP_INSTALL" && chown www-data:www-data "$TMP_INSTALL"
log "composer create-project flarum/flarum:${TARGET_FLARUM_VERSION}..."
composer_as_www create-project "flarum/flarum:${TARGET_FLARUM_VERSION}" --stability=beta "$TMP_INSTALL" --no-interaction \
>> "$LOG_DIR/composer_install.log" 2>&1 || die "composer create-project failed — see $LOG_DIR/composer_install.log"

# Seed from the skeleton baked into the image. No Packagist, no resolution,
# and the same image tag always produces the same forum — which was the
# whole point of issue #2. Falls back to resolving over the network only
# when the operator deliberately asked for a different version, or when the
# image predates the skeleton.
if [ "$FLARUM_FROM_SKELETON" = "true" ]; then
log "Seeding Flarum ${FLARUM_SKELETON_VERSION} from the baked skeleton (no network)."
cp -a "$FLARUM_SKELETON/." "$TMP_INSTALL/" \
|| die "could not copy the baked skeleton from $FLARUM_SKELETON"
chown -R www-data:www-data "$TMP_INSTALL"
else
log "composer create-project flarum/flarum:${TARGET_FLARUM_VERSION}..."
composer_as_www create-project "flarum/flarum:${TARGET_FLARUM_VERSION}" --stability=beta "$TMP_INSTALL" --no-interaction \
>> "$LOG_DIR/composer_install.log" 2>&1 || die "composer create-project failed — see $LOG_DIR/composer_install.log"
fi

cp -an "$TMP_INSTALL/." "$WORKDIR/" && rm -rf "$TMP_INSTALL"
chown -R www-data:www-data "$WORKDIR"
find "$WORKDIR" -type d -exec chmod 775 {} + ; find "$WORKDIR" -type f -exec chmod 664 {} +
Expand Down
Loading