feat(docker): guide clone installation and validate paired update artifacts
This commit is contained in:
1 parent
a4266f297c
commit
8f55ff2d17
39 files changed
+473
-73
No files matched your search
@@ -42,7 +42,8 @@ printf '%s\n' 'EpicNext-Cms installation' 'Requires an existing Habbo database a
|
||||
read_value 'Public CMS URL (e.g. https://hotel.example)' "${CMS_PUBLIC_URL:-}"
|
||||
public_url="${REPLY%/}"
|
||||
http_url "$public_url" && [[ "$public_url" != *'?'* ]] || fail "Enter an HTTP(S) URL without credentials, query or fragment."
|
||||
read_value 'Delivery method: prebuilt or source' "${CMS_INSTALL_MODE:-prebuilt}"
|
||||
printf '%s\n' 'Source builds need repository access and public build dependencies. Prebuilt images additionally need registry package access (docker login on this host for private packages).'
|
||||
read_value 'Delivery method: prebuilt or source' "${CMS_INSTALL_MODE:-source}"
|
||||
mode="$REPLY"
|
||||
[[ "$mode" = prebuilt || "$mode" = source ]] || fail "Choose prebuilt or source."
|
||||
if [[ "$mode" = prebuilt ]]; then
|
||||
|
||||
+11
-13
@@ -2,7 +2,7 @@
|
||||
# ==============================================================================
|
||||
# docker-preflight.sh — Verify a VPS is ready to run the Dockerized EpicNext-CMS.
|
||||
#
|
||||
# Lets any supplied .env drive the checks. Checks (all idempotent, none mutating):
|
||||
# Does not execute dotenv. Endpoint probes use explicit environment overrides. Checks (all idempotent, none mutating):
|
||||
# 1. Docker + compose available and usable.
|
||||
# 2. Required runtime dirs exist (RW for UID 33 where the CMS writes).
|
||||
# 3. Volume owners are UID/GID 33 (www-data) on the host.
|
||||
@@ -73,17 +73,15 @@ for d in "${RW_DIRS[@]}"; do
|
||||
continue
|
||||
fi
|
||||
if [ ! -d "$d" ]; then err "not a directory: $d"; continue; fi
|
||||
# Test write as the target owner via a temp file drop (as root), then remove.
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
if touch "$d/.preflight-write-test" 2>/dev/null; then
|
||||
rm -f "$d/.preflight-write-test"
|
||||
ok "writable: $d"
|
||||
else
|
||||
err "not writable: $d (needs owner 33:33)"
|
||||
[ "$FIX" -eq 1 ] && { chown -R 33:33 "$d" && say " chown -R 33:33 $d" || err " chown failed"; }
|
||||
fi
|
||||
# Check the actual runtime identity, never root's ability to write.
|
||||
if [ "$(id -u)" -eq 33 ]; then
|
||||
if [ -w "$d" ] && [ -r "$d" ]; then ok "runtime access: $d"; else err "runtime access denied: $d"; fi
|
||||
elif [ "$(id -u)" -eq 0 ] && command -v setpriv >/dev/null 2>&1; then
|
||||
if setpriv --reuid=33 --regid=33 --clear-groups sh -c 'test -r "$1" && test -w "$1"' sh "$d"; then
|
||||
ok "runtime access as UID/GID 33: $d"
|
||||
else err "runtime access denied for UID/GID 33: $d"; fi
|
||||
else
|
||||
if [ -w "$d" ]; then ok "writable: $d"; else err "not writable: $d"; fi
|
||||
wrn "runtime access unverified: $d; updater checks candidate container access before migrations"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -102,8 +100,8 @@ done
|
||||
doing "4. Configuration + required services (.env, host network)"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
ok ".env present: $ENV_FILE"
|
||||
### shellcheck disable=SC1090
|
||||
set -a; . "$ENV_FILE"; set +a
|
||||
say "Dotenv is not executed. Port checks below use explicit shell overrides/defaults, not DATABASE_URL or REDIS_URL."
|
||||
say "The updater validates real candidate configuration and mounted storage before migrations."
|
||||
else
|
||||
err ".env missing: $ENV_FILE (copy your production env here)"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"import {validateRuntime} from './scripts/docker-start.mjs';try{validateRuntime({});process.exit(1)}catch(error){if(!error.message.startsWith('Invalid runtime configuration'))throw error}",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
@@ -27,7 +27,10 @@ export function validateRuntime(settings) {
|
||||
throw new Error(`Invalid runtime configuration: ${invalid.join(", ")}`);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
) {
|
||||
try {
|
||||
validateRuntime(process.env);
|
||||
const child = spawn(process.execPath, ["server.js"], { stdio: "inherit" });
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Parse before opening locks, logs or contacting Git/Docker.
|
||||
parse_update_options() {
|
||||
UPDATE_SKIP_PULL=0
|
||||
UPDATE_APP_DIGEST=""
|
||||
UPDATE_MIGRATIONS_DIGEST=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-pull) UPDATE_SKIP_PULL=1; shift ;;
|
||||
--app-digest|--migrations-digest)
|
||||
[[ $# -ge 2 && "$2" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo 'Expected a sha256 digest with 64 lowercase hex characters.' >&2; return 1; }
|
||||
if [[ "$1" = --app-digest ]]; then UPDATE_APP_DIGEST="$2"; else UPDATE_MIGRATIONS_DIGEST="$2"; fi
|
||||
shift 2 ;;
|
||||
*) echo 'Usage: bash cms update [--skip-pull] [--app-digest sha256:HEX --migrations-digest sha256:HEX]' >&2; return 1 ;;
|
||||
esac
|
||||
done
|
||||
[[ -z "$UPDATE_APP_DIGEST" && -z "$UPDATE_MIGRATIONS_DIGEST" || -n "$UPDATE_APP_DIGEST" && -n "$UPDATE_MIGRATIONS_DIGEST" ]] || { echo 'Supply both application and migrations digests.' >&2; return 1; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
source "$(dirname "$0")/docker-update-options.sh"
|
||||
expect_failure() { if (parse_update_options "$@") >/dev/null 2>&1; then echo "unexpected accepted arguments: $*" >&2; exit 1; fi; }
|
||||
parse_update_options
|
||||
[[ "$UPDATE_SKIP_PULL" = 0 && -z "$UPDATE_APP_DIGEST" ]]
|
||||
expect_failure --unknown
|
||||
expect_failure --app-digest
|
||||
expect_failure --skip-pull value
|
||||
expect_failure --app-digest sha256:bad --migrations-digest sha256:bad
|
||||
digest="sha256:$(printf '%064d' 1)"
|
||||
expect_failure --app-digest "$digest"
|
||||
expect_failure --migrations-digest "$digest"
|
||||
parse_update_options --skip-pull --app-digest "$digest" --migrations-digest "$digest"
|
||||
[[ "$UPDATE_SKIP_PULL" = 1 && "$UPDATE_APP_DIGEST" = "$digest" && "$UPDATE_MIGRATIONS_DIGEST" = "$digest" ]]
|
||||
echo 'Docker update option tests passed'
|
||||
+23
-10
@@ -3,6 +3,8 @@
|
||||
set -Eeuo pipefail
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$DIR"
|
||||
source "$DIR/scripts/docker-update-options.sh"
|
||||
parse_update_options "$@" || exit 1
|
||||
exec 9>"$DIR/.deploy.lock"
|
||||
flock -w 1800 9
|
||||
LOG_FILE="${LOG_FILE:-$DIR/logs/docker-update.log}"
|
||||
@@ -61,17 +63,20 @@ if [ "$(docker inspect --format '{{.State.Running}}' epicnext-cms-app 2>/dev/nul
|
||||
die "This host is managed by CI (epicnext-cms-app). Update through CI, not a second Compose deployment."
|
||||
fi
|
||||
[[ -z "$(git status --porcelain --untracked-files=normal)" ]] || die "Working tree is not clean. Commit or stash local work first."
|
||||
git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null || die "Configure this branch's Git upstream before updating."
|
||||
script_before="$(git hash-object scripts/docker-update.sh)"
|
||||
git pull --ff-only >>"$LOG_FILE" 2>&1
|
||||
if [ "$script_before" != "$(git hash-object scripts/docker-update.sh)" ]; then
|
||||
log "Updater changed; restarting the newly pulled script."
|
||||
exec 9>&-
|
||||
exec bash "$DIR/scripts/docker-update.sh"
|
||||
if [[ "$UPDATE_SKIP_PULL" = 0 ]]; then
|
||||
git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null || die "Configure this branch's Git upstream before updating."
|
||||
script_before="$(git hash-object scripts/docker-update.sh)"
|
||||
git pull --ff-only >>"$LOG_FILE" 2>&1
|
||||
if [ "$script_before" != "$(git hash-object scripts/docker-update.sh)" ]; then
|
||||
log "Updater changed; restarting the newly pulled script."
|
||||
exec 9>&-
|
||||
exec bash "$DIR/scripts/docker-update.sh" "$@"
|
||||
fi
|
||||
fi
|
||||
# Load after pulling so image location changes follow the repository.
|
||||
source "$DIR/scripts/docker-config.sh"
|
||||
load_docker_config "$DIR" || die "Invalid .docker-install: expected MODE=prebuilt or source and PUBLIC_URL=http(s)://your-host."
|
||||
[[ -z "$UPDATE_APP_DIGEST" || -n "${CMS_IMAGE_REPOSITORY:-}" ]] || die "Digest selection requires prebuilt mode (registry access)."
|
||||
export CMS_RELEASE="$(git rev-parse HEAD)"
|
||||
[[ "$CMS_RELEASE" =~ ^[0-9a-f]{40}$ ]] || die "Invalid Git commit."
|
||||
[[ -f .env ]] || die "Create .env before installing or updating."
|
||||
@@ -92,11 +97,14 @@ migration_image="epicnext-cms-migrations:$CMS_RELEASE"
|
||||
if [[ -n "${CMS_IMAGE_REPOSITORY:-}" ]]; then
|
||||
[[ "$CMS_IMAGE_REPOSITORY" =~ ^[a-z0-9.-]+(:[0-9]+)?/[a-z0-9._/-]+$ ]] || die "Invalid CMS_IMAGE_REPOSITORY; use registry/owner/image without a tag."
|
||||
log "Pulling prebuilt application and matching migrations for $CMS_RELEASE"
|
||||
docker pull "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" >>"$LOG_FILE" 2>&1 || die "Application image unavailable. Publication for this commit may still be running; retry bash cms update after CI succeeds. For private packages, log in to the registry first."
|
||||
app_reference="$CMS_IMAGE_REPOSITORY:$CMS_RELEASE"
|
||||
[[ -z "$UPDATE_APP_DIGEST" ]] || app_reference="$CMS_IMAGE_REPOSITORY@$UPDATE_APP_DIGEST"
|
||||
docker pull "$app_reference" >>"$LOG_FILE" 2>&1 || die "Application image unavailable. Publication for this commit may still be running; retry bash cms update after CI succeeds. For private packages, log in to the registry first."
|
||||
remote_migration_image="$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations"
|
||||
[[ -z "$UPDATE_MIGRATIONS_DIGEST" ]] || remote_migration_image="$CMS_IMAGE_REPOSITORY@$UPDATE_MIGRATIONS_DIGEST"
|
||||
docker pull "$remote_migration_image" >>"$LOG_FILE" 2>&1 || die "Matching migrations image unavailable. Current CMS is unchanged; retry after publication succeeds."
|
||||
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" "epicnext-cms:$CMS_RELEASE"
|
||||
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations" "$migration_image"
|
||||
docker tag "$app_reference" "epicnext-cms:$CMS_RELEASE"
|
||||
docker tag "$remote_migration_image" "$migration_image"
|
||||
else
|
||||
docker build --network=host --target migrations --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" -t "$migration_image" . >>"$LOG_FILE" 2>&1
|
||||
docker compose build --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" cms >>"$LOG_FILE" 2>&1
|
||||
@@ -104,6 +112,11 @@ fi
|
||||
expected_image="$(docker image inspect --format '{{.Id}}' "epicnext-cms:$CMS_RELEASE")"
|
||||
revision="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$expected_image")"
|
||||
[[ "$revision" = "$CMS_RELEASE" ]] || die "Built image has revision $revision, expected $CMS_RELEASE."
|
||||
migration_revision="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$migration_image")"
|
||||
[[ "$migration_revision" = "$CMS_RELEASE" ]] || die "Migrations image has no matching release label. Rebuild/publish both artifacts from this checkout; the database was not changed."
|
||||
# Compose uses the candidate image's USER and exact mounts/env without starting the server.
|
||||
docker compose run --rm --no-deps --entrypoint node cms --input-type=module -e 'import {access,constants} from "node:fs/promises";import {validateRuntime} from "./docker-start.mjs";validateRuntime(process.env);for(const p of ["/app/storage","/app/public/nitro-assets","/app/public/swf","/var/www/Gamedata"]){try{await access(p,constants.R_OK|constants.W_OK|constants.X_OK)}catch{console.error("Runtime storage access denied: "+p);process.exit(1)}}' >>"$LOG_FILE" 2>&1 || die "Candidate configuration/storage validation failed; database unchanged. Check directory access for UID/GID 33."
|
||||
log "Before migration: ensure a verified database backup is available. Image rollback does not undo database migrations."
|
||||
migration_mounts=(--mount "type=bind,source=$DIR/.env,target=/app/.env,readonly")
|
||||
if [[ -f "$DIR/.env.local" ]]; then migration_mounts+=(--mount "type=bind,source=$DIR/.env.local,target=/app/.env.local,readonly"); fi
|
||||
docker run --rm --network host "${migration_mounts[@]}" --entrypoint pnpm "$migration_image" db:migrate >>"$LOG_FILE" 2>&1
|
||||
|
||||
Reference in new issue
Block a user