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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ jobs:
- run: pnpm db:verify-clean
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test:homelab-installer
- run: pnpm test:unit
- run: pnpm contracts:generate
- name: Verify committed migrations
Expand Down
358 changes: 254 additions & 104 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions deploy/docker/.env.homelab.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MUSTER_PUBLIC_URL=http://192.168.1.19:3004
MUSTER_PUBLIC_URL=http://muster.example.lan:3004
MUSTER_HTTP_PORT=3004
AUTH_TRUSTED_ORIGINS=http://192.168.1.19:3004,http://homelab:3004
AUTH_TRUSTED_ORIGINS=http://muster.example.lan:3004,http://homelab:3004
MUSTER_ORGANISATION_NAME=Muster Workspace
MUSTER_ORGANISATION_SLUG=muster
MUSTER_DEFAULT_TIMEZONE=Australia/Sydney
Expand Down
14 changes: 9 additions & 5 deletions deploy/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ The homelab profile pulls the public GHCR image and keeps PostgreSQL, Redis,
MinIO, Mailpit, and synthetic product mocks on the internal Compose network:

```bash
./scripts/install-homelab.sh
MUSTER_IMAGE=ghcr.io/jusso-dev/muster@sha256:75ebdad962373ff1fa5dbef8dba8f0a005de6058e21655dad8c72b1129e90861 \
./scripts/install-homelab.sh
```

The installer creates a mode-600 `.env.homelab`, generates independent
database, authentication, storage, and administrator secrets, pulls the public
image, starts the stack, waits for health, and creates the local administrator.
Only Muster's configured HTTP port is published. The default uses plain HTTP
for the trusted local network at `192.168.1.19`; use an HTTPS reverse proxy and set
`AUTH_SECURE_COOKIES=true` when exposing Muster beyond it.
reviewed digest, starts the stack, waits for health, and creates the local
administrator. Replace the digest only after reviewing the corresponding
release evidence; mutable tags such as `latest` are rejected.
Only Muster's configured HTTP port is published. The example uses
`http://muster.example.lan:3004`; replace it with the exact trusted browser
origin. Use an HTTPS reverse proxy and set `AUTH_SECURE_COOKIES=true` when
exposing Muster beyond a trusted local network.
2 changes: 1 addition & 1 deletion deploy/docker/docker-compose.homelab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ x-muster-environment: &muster-environment
CODEX_HOME: /var/lib/muster/codex

x-muster-service: &muster-service
image: ghcr.io/jusso-dev/muster:${MUSTER_VERSION:-latest}
image: ${MUSTER_IMAGE:-ghcr.io/jusso-dev/muster:${MUSTER_VERSION:-latest}}
pull_policy: always
restart: unless-stopped
environment: *muster-environment
Expand Down
3 changes: 2 additions & 1 deletion docs/operations/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ The Codex credential is runtime state, not image content. Authenticate once:
docker compose --profile setup run --rm codex-login
```

Compose stores the resulting `auth.json` in the private `codex-state` volume.
The root Compose topology stores `auth.json` in private `muster-codex` volume;
the homelab topology calls its corresponding private volume `codex-state`.
Restrict Docker access, never publish or log this file, and revoke the Codex
session if the host is compromised. The gateway reports
`authentication_required` until the credential exists.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"test:unit": "vitest run",
"test:e2e": "playwright test",
"test:e2e:homelab": "playwright test --config=playwright.homelab.config.ts",
"test:homelab-installer": "bash scripts/test-install-homelab.sh",
"contracts:generate": "pnpm --filter @muster/contracts generate",
"db:generate": "pnpm --filter @muster/database generate",
"db:migrate": "pnpm --filter @muster/database migrate",
Expand Down
87 changes: 83 additions & 4 deletions scripts/install-homelab.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,31 @@ cd "$(dirname "$0")/.."

env_file=".env.homelab"
compose_file="deploy/docker/docker-compose.homelab.yml"
requested_public_url="${MUSTER_PUBLIC_URL:-http://192.168.1.19:3004}"
requested_public_url="${MUSTER_PUBLIC_URL:-http://muster.example.lan:3004}"
requested_http_port="${MUSTER_HTTP_PORT:-3004}"
requested_version="${MUSTER_VERSION:-}"
requested_image="${MUSTER_IMAGE:-}"

read_env_value() {
sed -n "s|^$1=||p" "$env_file" | tail -n 1
}

if [[ -n "$requested_version" && -n "$requested_image" ]]; then
printf 'Set either MUSTER_VERSION or MUSTER_IMAGE, not both.\n' >&2
exit 2
fi
if [[ ! -f "$env_file" && -z "$requested_version" && -z "$requested_image" ]]; then
printf 'A reviewed immutable MUSTER_IMAGE digest or MUSTER_VERSION sha tag is required for a fresh install.\n' >&2
exit 2
fi
if [[ -n "$requested_version" && ! "$requested_version" =~ ^sha-[0-9a-f]{7,40}$ ]]; then
printf 'MUSTER_VERSION must be an immutable sha-<hex> tag.\n' >&2
exit 2
fi
if [[ -n "$requested_image" && ! "$requested_image" =~ ^ghcr\.io/jusso-dev/muster@sha256:[0-9a-f]{64}$ ]]; then
printf 'MUSTER_IMAGE must be the reviewed ghcr.io/jusso-dev/muster@sha256:<digest> reference.\n' >&2
exit 2
fi

if [[ ! -f "$env_file" ]]; then
cp deploy/docker/.env.homelab.example "$env_file"
Expand Down Expand Up @@ -39,9 +62,65 @@ if ! grep -q '^AUTH_TRUSTED_ORIGINS=' "$env_file"; then
"$requested_public_url" "$requested_http_port" >> "$env_file"
fi

set -a
source "$env_file"
set +a
# Persist an explicitly requested reviewed image reference before sourcing the env
# file. Otherwise the template's `latest` value would silently win.
if [[ -n "$requested_version" ]]; then
sed -i.bak \
-e "s|^MUSTER_VERSION=.*|MUSTER_VERSION=${requested_version}|" \
-e '/^MUSTER_IMAGE=/d' \
"$env_file"
if ! grep -q '^MUSTER_VERSION=' "$env_file"; then
printf 'MUSTER_VERSION=%s\n' "$requested_version" >> "$env_file"
fi
rm "$env_file.bak"
fi

if [[ -n "$requested_image" ]]; then
sed -i.bak \
-e "s|^MUSTER_IMAGE=.*|MUSTER_IMAGE=${requested_image}|" \
-e '/^MUSTER_VERSION=/d' \
"$env_file"
if ! grep -q '^MUSTER_IMAGE=' "$env_file"; then
printf 'MUSTER_IMAGE=%s\n' "$requested_image" >> "$env_file"
fi
rm "$env_file.bak"
fi

effective_version="$(read_env_value MUSTER_VERSION)"
effective_image="$(read_env_value MUSTER_IMAGE)"
if [[ -n "$effective_version" && -n "$effective_image" ]]; then
printf 'The persisted homelab configuration contains conflicting image references.\n' >&2
exit 2
fi
if [[ -n "$effective_version" ]]; then
if [[ ! "$effective_version" =~ ^sha-[0-9a-f]{7,40}$ ]]; then
printf 'The persisted MUSTER_VERSION is not an immutable sha-<hex> tag.\n' >&2
exit 2
fi
elif [[ -n "$effective_image" ]]; then
if [[ ! "$effective_image" =~ ^ghcr\.io/jusso-dev/muster@sha256:[0-9a-f]{64}$ ]]; then
printf 'The persisted MUSTER_IMAGE is not a reviewed GHCR digest.\n' >&2
exit 2
fi
else
printf 'The persisted homelab configuration has no immutable image reference.\n' >&2
exit 2
fi

MUSTER_HTTP_PORT="$(read_env_value MUSTER_HTTP_PORT)"
MUSTER_PUBLIC_URL="$(read_env_value MUSTER_PUBLIC_URL)"
MUSTER_LOCAL_ADMIN_EMAIL="$(read_env_value MUSTER_LOCAL_ADMIN_EMAIL)"
MUSTER_LOCAL_ADMIN_PASSWORD="$(read_env_value MUSTER_LOCAL_ADMIN_PASSWORD)"
if [[ -z "$MUSTER_LOCAL_ADMIN_EMAIL" || -z "$MUSTER_LOCAL_ADMIN_PASSWORD" ]]; then
printf 'Homelab administrator credentials are missing from %s.\n' "$env_file" >&2
exit 2
fi

for external_network in tawny_default kelpie_default; do
if ! docker network inspect "$external_network" >/dev/null 2>&1; then
docker network create "$external_network" >/dev/null
fi
done

docker compose --env-file "$env_file" -f "$compose_file" pull
docker compose --env-file "$env_file" -f "$compose_file" up -d
Expand Down
105 changes: 105 additions & 0 deletions scripts/test-install-homelab.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "$0")/.." && pwd)"
fixture_root="$(mktemp -d)"
trap 'rm -rf "$fixture_root"' EXIT

mkdir -p "$fixture_root/scripts" "$fixture_root/deploy/docker" "$fixture_root/bin"
cp "$repo_root/scripts/install-homelab.sh" "$fixture_root/scripts/"
cp "$repo_root/deploy/docker/.env.homelab.example" "$fixture_root/deploy/docker/"
cp "$repo_root/deploy/docker/docker-compose.homelab.yml" "$fixture_root/deploy/docker/"

cat >"$fixture_root/bin/openssl" <<'EOF'
#!/usr/bin/env bash
length="${*: -1}"
printf '%*s\n' "$((length * 2))" '' | tr ' ' a
EOF

cat >"$fixture_root/bin/docker" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"${DOCKER_CALLS:?}"
if [[ "${1:-}" == "network" && "${2:-}" == "inspect" ]]; then
grep -Fxq "${3:-}" "${DOCKER_NETWORKS:?}" 2>/dev/null
exit
fi
if [[ "${1:-}" == "network" && "${2:-}" == "create" ]]; then
printf '%s\n' "${3:-}" >>"${DOCKER_NETWORKS:?}"
fi
EOF

cat >"$fixture_root/bin/curl" <<'EOF'
#!/usr/bin/env bash
if [[ " $* " == *" --write-out "* ]]; then
printf '200'
fi
EOF

cat >"$fixture_root/bin/sleep" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF

chmod +x "$fixture_root/bin/"*
export PATH="$fixture_root/bin:$PATH"
export DOCKER_CALLS="$fixture_root/docker-calls"
export DOCKER_NETWORKS="$fixture_root/docker-networks"
touch "$DOCKER_CALLS" "$DOCKER_NETWORKS"

if (
cd "$fixture_root"
./scripts/install-homelab.sh >/dev/null 2>&1
); then
printf 'Fresh install accepted a missing immutable image reference.\n' >&2
exit 1
fi

digest="ghcr.io/jusso-dev/muster@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
(
cd "$fixture_root"
MUSTER_PUBLIC_URL=http://muster.test:3004 \
AUTH_TRUSTED_ORIGINS=http://muster.test:3004 \
MUSTER_IMAGE="$digest" \
./scripts/install-homelab.sh >/dev/null
)
grep -Fxq "MUSTER_IMAGE=$digest" "$fixture_root/.env.homelab"
if grep -q '^MUSTER_VERSION=' "$fixture_root/.env.homelab"; then
printf 'Digest install retained conflicting MUSTER_VERSION.\n' >&2
exit 1
fi
grep -Fxq "tawny_default" "$DOCKER_NETWORKS"
grep -Fxq "kelpie_default" "$DOCKER_NETWORKS"

(
cd "$fixture_root"
MUSTER_VERSION=sha-bbbbbbb ./scripts/install-homelab.sh >/dev/null
)
grep -Fxq "MUSTER_VERSION=sha-bbbbbbb" "$fixture_root/.env.homelab"
if grep -q '^MUSTER_IMAGE=' "$fixture_root/.env.homelab"; then
printf 'Tag transition retained conflicting MUSTER_IMAGE.\n' >&2
exit 1
fi

second_digest="ghcr.io/jusso-dev/muster@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
(
cd "$fixture_root"
MUSTER_IMAGE="$second_digest" ./scripts/install-homelab.sh >/dev/null
)
grep -Fxq "MUSTER_IMAGE=$second_digest" "$fixture_root/.env.homelab"
if grep -q '^MUSTER_VERSION=' "$fixture_root/.env.homelab"; then
printf 'Digest transition retained conflicting MUSTER_VERSION.\n' >&2
exit 1
fi

sed -i.bak -e '/^MUSTER_IMAGE=/d' "$fixture_root/.env.homelab"
rm "$fixture_root/.env.homelab.bak"
printf 'MUSTER_VERSION=latest\n' >>"$fixture_root/.env.homelab"
if (
cd "$fixture_root"
./scripts/install-homelab.sh >/dev/null 2>&1
); then
printf 'Installer accepted a persisted mutable image tag.\n' >&2
exit 1
fi

printf 'Homelab installer transitions passed.\n'
Loading