Pi stack behind Nginx Proxy Manager: remove Caddy and cert-sync (D-120)
The server publishes 8080 on the Kvitt Pi's LAN interfaces and trusts X-Forwarded-For only from HA_HOST, NPM's LAN address (an IPv4 literal, checked by install.sh). KVITT_PUBLIC_HOST (owner-chosen, not icehack.asuscomm.com, which is NPM's proxy host for Home Assistant) builds KVITT_PUBLIC_URL. Only fcm-service-account.json and rclone.conf are installed; install.sh removes the Caddyfile, the Home Assistant SSH key, known_hosts and the certs and caddy_data volumes left by the earlier stack. Health checks: port 8080, then through NPM. The cert-sync image leaves the workflow and build-images.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# example workflow and the Gitea Actions documentation; the first run is an owner step, infra/README.md).
|
# example workflow and the Gitea Actions documentation; the first run is an owner step, infra/README.md).
|
||||||
#
|
#
|
||||||
# Runner: act_runner with the label `host` on the owner's Raspberry Pi (Alpine-based host, docker builds on the
|
# Runner: act_runner with the label `host` on the owner's Raspberry Pi (Alpine-based host, docker builds on the
|
||||||
# Pi itself). Pushes three linux/arm64 images to the Gitea container registry of the same instance (gitea.lan,
|
# Pi itself). Pushes two linux/arm64 images to the Gitea container registry of the same instance (gitea.lan,
|
||||||
# plain HTTP), each tagged with the version vMAJOR.MINOR.PATCH and `latest`, then pushes the git tag of that version.
|
# plain HTTP), each tagged with the version vMAJOR.MINOR.PATCH and `latest`, then pushes the git tag of that version.
|
||||||
# On the Kvitt Pi: /opt/kvitt/deploy.sh <version>.
|
# On the Kvitt Pi: /opt/kvitt/deploy.sh <version>.
|
||||||
#
|
#
|
||||||
@@ -34,7 +34,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- "server/**"
|
- "server/**"
|
||||||
- "infra/cert-sync/**"
|
|
||||||
- "infra/backup/**"
|
- "infra/backup/**"
|
||||||
- ".gitea/workflows/**"
|
- ".gitea/workflows/**"
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
@@ -82,16 +81,6 @@ jobs:
|
|||||||
# --provenance=false on every build: one plain linux/arm64 manifest per tag, as Podman and infra/build-images.sh
|
# --provenance=false on every build: one plain linux/arm64 manifest per tag, as Podman and infra/build-images.sh
|
||||||
# push. Without it buildx pushes an index with an attestation entry (platform unknown/unknown), which the Gitea
|
# push. Without it buildx pushes an index with an attestation entry (platform unknown/unknown), which the Gitea
|
||||||
# package page lists as an extra platform; nothing here reads attestations.
|
# package page lists as an extra platform; nothing here reads attestations.
|
||||||
- name: Build cert-sync image
|
|
||||||
run: |
|
|
||||||
docker buildx build \
|
|
||||||
--platform linux/arm64 \
|
|
||||||
--provenance=false \
|
|
||||||
-t "$REGISTRY/kvitt-cert-sync:$VERSION" \
|
|
||||||
-t "$REGISTRY/kvitt-cert-sync:latest" \
|
|
||||||
--metadata-file /tmp/kvitt-cert-sync.metadata.json \
|
|
||||||
./infra/cert-sync \
|
|
||||||
--push
|
|
||||||
- name: Build backup image
|
- name: Build backup image
|
||||||
run: |
|
run: |
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
@@ -118,7 +107,7 @@ jobs:
|
|||||||
- name: Print image digests
|
- name: Print image digests
|
||||||
# The digests deploy.sh prints and records on the Kvitt Pi (/opt/kvitt/deployed-digests, D-117) must match.
|
# The digests deploy.sh prints and records on the Kvitt Pi (/opt/kvitt/deployed-digests, D-117) must match.
|
||||||
run: |
|
run: |
|
||||||
for name in cert-sync backup server; do
|
for name in backup server; do
|
||||||
digest=$(sed -n 's/.*"containerimage.digest": *"\([^"]*\)".*/\1/p' "/tmp/kvitt-$name.metadata.json")
|
digest=$(sed -n 's/.*"containerimage.digest": *"\([^"]*\)".*/\1/p' "/tmp/kvitt-$name.metadata.json")
|
||||||
echo "$REGISTRY/kvitt-$name:$VERSION ${digest:-unknown}"
|
echo "$REGISTRY/kvitt-$name:$VERSION ${digest:-unknown}"
|
||||||
rm -f "/tmp/kvitt-$name.metadata.json"
|
rm -f "/tmp/kvitt-$name.metadata.json"
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build the three Kvitt images for linux/arm64 on the Mac and push them to the Gitea registry (SPEC §10.3).
|
# Build the two Kvitt images for linux/arm64 on the Mac and push them to the Gitea registry (SPEC §10.3).
|
||||||
# The Mac fallback for the Gitea Actions workflow .gitea/workflows/build-images.yaml (which is the normal path).
|
# The Mac fallback for the Gitea Actions workflow .gitea/workflows/build-images.yaml (which is the normal path).
|
||||||
# Usage: infra/build-images.sh [tag] tag defaults to the short git SHA of HEAD; `latest` is always tagged too.
|
# Usage: infra/build-images.sh [tag] tag defaults to the short git SHA of HEAD; `latest` is always tagged too.
|
||||||
#
|
#
|
||||||
# Images (D-104): ${KVITT_REGISTRY}/kvitt-server (context server/), kvitt-cert-sync (infra/cert-sync),
|
# Images (D-104, D-120): ${KVITT_REGISTRY}/kvitt-server (context server/) and kvitt-backup (infra/backup).
|
||||||
# kvitt-backup (infra/backup). KVITT_REGISTRY and the optional credentials come from infra/secrets/registry.env
|
# KVITT_REGISTRY and the optional credentials come from infra/secrets/registry.env (git-ignored):
|
||||||
# (git-ignored):
|
|
||||||
# KVITT_REGISTRY=gitea.lan/jacobh2
|
# KVITT_REGISTRY=gitea.lan/jacobh2
|
||||||
# KVITT_REGISTRY_USER=<gitea user> optional
|
# KVITT_REGISTRY_USER=<gitea user> optional
|
||||||
# KVITT_REGISTRY_TOKEN=<token, write:package> optional
|
# KVITT_REGISTRY_TOKEN=<token, write:package> optional
|
||||||
@@ -41,7 +40,6 @@ registry_host="${registry%%/*}"
|
|||||||
# name|context|dockerfile
|
# name|context|dockerfile
|
||||||
images=(
|
images=(
|
||||||
"kvitt-server|server|server/Dockerfile"
|
"kvitt-server|server|server/Dockerfile"
|
||||||
"kvitt-cert-sync|infra/cert-sync|infra/cert-sync/Dockerfile"
|
|
||||||
"kvitt-backup|infra/backup|infra/backup/Dockerfile"
|
"kvitt-backup|infra/backup|infra/backup/Dockerfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# Kvitt cert-sync image (SPEC §10.3, §10.5.1): alpine with openssh-client, curl and sync.sh on a daily schedule.
|
|
||||||
# Versions resolved 2026-10-18: alpine 3.24.2 (Docker Hub official image, multi-arch index digest below); packages
|
|
||||||
# from Alpine v3.24 main/community: openssh-client-default 10.3_p1, curl 8.22.0, openssl 3.5.9 (PEM checks),
|
|
||||||
# supercronic 0.2.49, tzdata 2026e.
|
|
||||||
# The scheduler is supercronic, not busybox crond: the container runs as uid 1000 (SPEC §10.2, D-023) and busybox
|
|
||||||
# crond cannot run jobs as a non-root user (checked 2026-10-18 in alpine 3.24.2: no job ever ran). See D-104.
|
|
||||||
FROM docker.io/library/alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
|
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
"openssh-client-default~10.3" \
|
|
||||||
"curl~8.22" \
|
|
||||||
"openssl~3.5" \
|
|
||||||
"supercronic~0.2" \
|
|
||||||
tzdata \
|
|
||||||
&& adduser -D -u 1000 -g "Kvitt cert-sync" kvitt \
|
|
||||||
&& mkdir -p /certs \
|
|
||||||
&& chown 1000:1000 /certs
|
|
||||||
|
|
||||||
COPY sync.sh entrypoint.sh /usr/local/bin/
|
|
||||||
RUN chmod 0755 /usr/local/bin/sync.sh /usr/local/bin/entrypoint.sh \
|
|
||||||
&& echo '0 4 * * * /usr/local/bin/sync.sh' > /etc/kvitt-cert-sync.crontab
|
|
||||||
|
|
||||||
ENV TZ=Europe/Stockholm
|
|
||||||
USER 1000:1000
|
|
||||||
# sync.sh once at start, then daily at 04:00 Europe/Stockholm.
|
|
||||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# cert-sync container start (SPEC §10.5.1): one sync at start, then the daily schedule. A failed first sync keeps
|
|
||||||
# the container running (the healthcheck reports whether a certificate exists).
|
|
||||||
/usr/local/bin/sync.sh || true
|
|
||||||
exec supercronic -quiet /etc/kvitt-cert-sync.crontab
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Kvitt certificate sync (SPEC §10.5.1). Fetches fullchain.pem and privkey.pem from the Home Assistant Pi over SSH
|
|
||||||
# and installs them into /certs when they are valid, newer and changed; then reloads Caddy through its admin API.
|
|
||||||
# Any failure logs one line and keeps the existing certificate.
|
|
||||||
#
|
|
||||||
# The remote command is always `tar -C <cert dir> -cf - fullchain.pem privkey.pem`, read as a tar stream. That
|
|
||||||
# works with both authorized_keys variants of the README: with `restrict,command="tar ..."` the forced command
|
|
||||||
# runs whatever is requested, and with a plain key the requested command is the same tar line.
|
|
||||||
#
|
|
||||||
# Environment (from /opt/kvitt/.env): HA_HOST, HA_PORT, HA_USER, HA_CERT_DIR.
|
|
||||||
# Files: /secrets/ha_ssh_key, /secrets/known_hosts (read-only), /certs (writable), /etc/caddy/Caddyfile (read-only).
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
CERTS=${CERTS_DIR:-/certs}
|
|
||||||
KEY=${HA_SSH_KEY:-/secrets/ha_ssh_key}
|
|
||||||
KNOWN_HOSTS=${HA_KNOWN_HOSTS:-/secrets/known_hosts}
|
|
||||||
CADDYFILE=${CADDYFILE:-/etc/caddy/Caddyfile}
|
|
||||||
CADDY_ADMIN=${CADDY_ADMIN:-http://caddy:2019}
|
|
||||||
|
|
||||||
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) cert-sync: $*"; }
|
|
||||||
|
|
||||||
tmp=""
|
|
||||||
cleanup() { [ -n "$tmp" ] && rm -rf "$tmp"; return 0; }
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
fail() { log "FAILED: $*; existing certificate kept"; exit 1; }
|
|
||||||
|
|
||||||
for v in HA_HOST HA_PORT HA_USER HA_CERT_DIR; do
|
|
||||||
eval "val=\${$v:-}"
|
|
||||||
# shellcheck disable=SC2154 # val is set by the eval above
|
|
||||||
[ -n "$val" ] || fail "$v is not set"
|
|
||||||
done
|
|
||||||
[ -r "$KEY" ] || fail "no readable key at $KEY"
|
|
||||||
[ -r "$KNOWN_HOSTS" ] || fail "no readable known_hosts at $KNOWN_HOSTS"
|
|
||||||
|
|
||||||
tmp=$(mktemp -d /tmp/cert-sync.XXXXXX)
|
|
||||||
|
|
||||||
# 1. Fetch as a tar stream.
|
|
||||||
if ! ssh -i "$KEY" -p "$HA_PORT" \
|
|
||||||
-o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=20 \
|
|
||||||
-o StrictHostKeyChecking=yes -o UserKnownHostsFile="$KNOWN_HOSTS" -o GlobalKnownHostsFile=/dev/null \
|
|
||||||
-o LogLevel=ERROR \
|
|
||||||
"$HA_USER@$HA_HOST" "tar -C '$HA_CERT_DIR' -cf - fullchain.pem privkey.pem" > "$tmp/certs.tar" 2> "$tmp/ssh.err"; then
|
|
||||||
fail "ssh to $HA_USER@$HA_HOST:$HA_PORT failed: $(head -c 300 "$tmp/ssh.err" | tr '\n' ' ')"
|
|
||||||
fi
|
|
||||||
mkdir "$tmp/new"
|
|
||||||
tar -xf "$tmp/certs.tar" -C "$tmp/new" fullchain.pem privkey.pem 2>/dev/null || fail "the tar stream did not hold fullchain.pem and privkey.pem"
|
|
||||||
new_chain="$tmp/new/fullchain.pem"
|
|
||||||
new_key="$tmp/new/privkey.pem"
|
|
||||||
|
|
||||||
# 2. Both parse as PEM, the certificate has not expired, and the key belongs to the certificate.
|
|
||||||
openssl x509 -in "$new_chain" -noout 2>/dev/null || fail "fullchain.pem does not parse as a PEM certificate"
|
|
||||||
openssl pkey -in "$new_key" -noout 2>/dev/null || fail "privkey.pem does not parse as a PEM private key"
|
|
||||||
openssl x509 -in "$new_chain" -noout -checkend 0 >/dev/null 2>&1 || fail "fullchain.pem has expired"
|
|
||||||
cert_pub=$(openssl x509 -in "$new_chain" -noout -pubkey | openssl pkey -pubin -outform DER 2>/dev/null | sha256sum | cut -d' ' -f1)
|
|
||||||
key_pub=$(openssl pkey -in "$new_key" -pubout -outform DER 2>/dev/null | sha256sum | cut -d' ' -f1)
|
|
||||||
[ "$cert_pub" = "$key_pub" ] || fail "privkey.pem does not belong to fullchain.pem"
|
|
||||||
new_end_iso=$(openssl x509 -in "$new_chain" -noout -enddate -dateopt iso_8601 | cut -d= -f2)
|
|
||||||
|
|
||||||
# 3. Unchanged? Older than the installed one?
|
|
||||||
cur_chain="$CERTS/fullchain.pem"
|
|
||||||
cur_key="$CERTS/privkey.pem"
|
|
||||||
if [ -s "$cur_chain" ] && [ -s "$cur_key" ]; then
|
|
||||||
if [ "$(sha256sum < "$new_chain")" = "$(sha256sum < "$cur_chain")" ] && \
|
|
||||||
[ "$(sha256sum < "$new_key")" = "$(sha256sum < "$cur_key")" ]; then
|
|
||||||
log "unchanged (not after $new_end_iso)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# ISO 8601 not-after dates compare as strings.
|
|
||||||
cur_end_iso=$(openssl x509 -in "$cur_chain" -noout -enddate -dateopt iso_8601 2>/dev/null | cut -d= -f2 || true)
|
|
||||||
if [ -n "$cur_end_iso" ] && openssl x509 -in "$cur_chain" -noout -checkend 0 >/dev/null 2>&1 && \
|
|
||||||
[ "$new_end_iso" != "$cur_end_iso" ] && \
|
|
||||||
[ "$(printf '%s\n%s\n' "$new_end_iso" "$cur_end_iso" | sort | head -n 1)" = "$new_end_iso" ]; then
|
|
||||||
fail "fetched certificate (not after $new_end_iso) expires before the installed one ($cur_end_iso)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Install: mode 0640, rename into place (key first, so a reload never pairs a new chain with an old key for long).
|
|
||||||
install -m 0640 "$new_key" "$CERTS/.privkey.pem.new" || fail "cannot write $CERTS"
|
|
||||||
install -m 0640 "$new_chain" "$CERTS/.fullchain.pem.new" || fail "cannot write $CERTS"
|
|
||||||
mv -f "$CERTS/.privkey.pem.new" "$cur_key"
|
|
||||||
mv -f "$CERTS/.fullchain.pem.new" "$cur_chain"
|
|
||||||
log "installed new certificate (not after $new_end_iso)"
|
|
||||||
|
|
||||||
# 5. Reload Caddy. At first start Caddy is not up yet (it waits for this container to be healthy) and loads the
|
|
||||||
# files when it starts, so an unreachable admin API is not a failure.
|
|
||||||
if curl -fsS -o /dev/null --max-time 20 -X POST "$CADDY_ADMIN/load" \
|
|
||||||
-H 'Content-Type: text/caddyfile' -H 'Cache-Control: must-revalidate' --data-binary @"$CADDYFILE" 2> "$tmp/curl.err"; then
|
|
||||||
log "caddy reloaded"
|
|
||||||
elif curl -fsS -o /dev/null --max-time 5 "$CADDY_ADMIN/config/" 2>/dev/null; then
|
|
||||||
log "FAILED: caddy reload: $(head -c 300 "$tmp/curl.err" | tr '\n' ' '); the new files are in place and load when Caddy restarts"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
log "caddy not reachable; it loads the certificate when it starts"
|
|
||||||
fi
|
|
||||||
+12
-11
@@ -4,20 +4,21 @@
|
|||||||
|
|
||||||
# --- Read by compose.yaml -----------------------------------------------------------------------------------------
|
# --- Read by compose.yaml -----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
# Registry and owner path of the Kvitt images; images are ${KVITT_REGISTRY}/kvitt-server|kvitt-cert-sync|kvitt-backup.
|
# Registry and owner path of the Kvitt images; images are ${KVITT_REGISTRY}/kvitt-server|kvitt-backup.
|
||||||
KVITT_REGISTRY=gitea.lan/jacobh2
|
KVITT_REGISTRY=gitea.lan/jacobh2
|
||||||
# Image tag to run. deploy.sh sets it; first install: the newest version tag the Gitea workflow pushed (vMAJOR.MINOR.PATCH),
|
# Image tag to run. deploy.sh sets it; first install: the newest version tag the Gitea workflow pushed (vMAJOR.MINOR.PATCH),
|
||||||
# or the short git SHA that infra/build-images.sh printed.
|
# or the short git SHA that infra/build-images.sh printed.
|
||||||
KVITT_TAG=CHANGE_ME_TAG
|
KVITT_TAG=CHANGE_ME_TAG
|
||||||
# LAN address (or host name) of the Home Assistant Pi that holds the Let's Encrypt certificate (OQ-3).
|
# Kvitt's own public host name (D-120), owner-chosen: a domain or DuckDNS name pointing at the home's public address.
|
||||||
|
# NPM holds its Let's Encrypt certificate; the router forwards external 8443 to NPM's 443. Invite links and the
|
||||||
|
# landing page use https://<KVITT_PUBLIC_HOST>:8443 (KVITT_PUBLIC_URL). Not icehack.asuscomm.com: that name is
|
||||||
|
# NPM's proxy host for Home Assistant.
|
||||||
|
KVITT_PUBLIC_HOST=CHANGE_ME_KVITT_PUBLIC_HOST
|
||||||
|
# LAN IPv4 address of the Home Assistant Pi, where Nginx Proxy Manager terminates TLS and forwards to this Pi's port
|
||||||
|
# 8080 (D-120). The server trusts X-Forwarded-For only from this address (KVITT_TRUSTED_PROXY), so it must be the
|
||||||
|
# address NPM's connections come from: an IP literal such as 192.168.1.20, not a host name (install.sh checks it).
|
||||||
|
# Kept the name HA_HOST (the proxy host); it no longer has the SSH settings HA_PORT, HA_USER, HA_CERT_DIR of cert-sync.
|
||||||
HA_HOST=CHANGE_ME_HA_HOST
|
HA_HOST=CHANGE_ME_HA_HOST
|
||||||
# SSH port of the Home Assistant SSH add-on (OQ-3; the "Advanced SSH & Web Terminal" and "Terminal & SSH" add-ons default to 22).
|
|
||||||
HA_PORT=22
|
|
||||||
# SSH user on the Home Assistant Pi (OQ-3). root only with the restricted key line of infra/README.md step 3; otherwise a
|
|
||||||
# dedicated non-root user that can only read HA_CERT_DIR (D-117). A plain, unrestricted root key is not acceptable.
|
|
||||||
HA_USER=CHANGE_ME_HA_USER
|
|
||||||
# Directory holding fullchain.pem and privkey.pem on the Home Assistant Pi (OQ-3; /ssl on Home Assistant OS).
|
|
||||||
HA_CERT_DIR=CHANGE_ME_HA_CERT_DIR
|
|
||||||
# rclone remote of the Drive backups, as named in rclone.conf.
|
# rclone remote of the Drive backups, as named in rclone.conf.
|
||||||
RCLONE_REMOTE=gdrive:
|
RCLONE_REMOTE=gdrive:
|
||||||
# Google Drive folder id the backups go to (OQ-4, answered).
|
# Google Drive folder id the backups go to (OQ-4, answered).
|
||||||
@@ -30,9 +31,9 @@ KVITT_REGISTRY_USER=
|
|||||||
# Gitea access token of that user with read:package scope; leave empty for anonymous pull. Never commit a filled .env.
|
# Gitea access token of that user with read:package scope; leave empty for anonymous pull. Never commit a filled .env.
|
||||||
KVITT_REGISTRY_TOKEN=
|
KVITT_REGISTRY_TOKEN=
|
||||||
|
|
||||||
# --- Informational: used in the owner's commands on the Mac (scp), read by nothing on the Pi --------------------
|
# --- Informational: used in the owner's commands on the Mac (scp) and in NPM, read by nothing on the Pi ---------
|
||||||
|
|
||||||
# LAN address of the Kvitt Pi itself (OQ-3).
|
# LAN address of the Kvitt Pi itself (OQ-3); also NPM's Forward Hostname / IP (infra/README.md, NPM section).
|
||||||
KVITT_PI_HOST=CHANGE_ME_KVITT_PI_HOST
|
KVITT_PI_HOST=CHANGE_ME_KVITT_PI_HOST
|
||||||
# SSH user on the Kvitt Pi (OQ-3).
|
# SSH user on the Kvitt Pi (OQ-3).
|
||||||
KVITT_PI_SSH_USER=CHANGE_ME_KVITT_PI_SSH_USER
|
KVITT_PI_SSH_USER=CHANGE_ME_KVITT_PI_SSH_USER
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# Kvitt Caddyfile (SPEC §10.5.2, D-117). Installed as /opt/kvitt/Caddyfile; cert-sync reloads it through the admin API.
|
|
||||||
{
|
|
||||||
admin 0.0.0.0:2019
|
|
||||||
auto_https off
|
|
||||||
# Caddy's own error log (for example a 502 when the server is down) carries the request too: same URI filter.
|
|
||||||
log default {
|
|
||||||
output stderr
|
|
||||||
format filter {
|
|
||||||
request>uri regexp `^(/i/)[^?]+|([?]).+` `${1}${2}REDACTED`
|
|
||||||
wrap json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
icehack.asuscomm.com:8443 {
|
|
||||||
tls /certs/fullchain.pem /certs/privkey.pem
|
|
||||||
request_body {
|
|
||||||
max_size 6MB
|
|
||||||
}
|
|
||||||
# Set after the server's response headers (defer), so a header the server also sends appears once.
|
|
||||||
header {
|
|
||||||
defer
|
|
||||||
Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
|
|
||||||
X-Content-Type-Options nosniff
|
|
||||||
X-Frame-Options DENY
|
|
||||||
Referrer-Policy no-referrer
|
|
||||||
}
|
|
||||||
reverse_proxy server:8080
|
|
||||||
# Access log: the invite code in /i/<code> and every query string (the inviter's name in ?from=) become
|
|
||||||
# REDACTED (SPEC §8.7); the Authorization header is redacted by Caddy's default.
|
|
||||||
log {
|
|
||||||
output stdout
|
|
||||||
format filter {
|
|
||||||
request>uri regexp `^(/i/)[^?]+|([?]).+` `${1}${2}REDACTED`
|
|
||||||
wrap json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-78
@@ -1,7 +1,8 @@
|
|||||||
# Kvitt production stack on the Kvitt Pi (SPEC §10.2). Deployed by install.sh to /opt/kvitt/compose.yaml together
|
# Kvitt production stack on the Kvitt Pi (SPEC §10.2). Deployed by install.sh to /opt/kvitt/compose.yaml together
|
||||||
# with /opt/kvitt/Caddyfile and /opt/kvitt/.env (from .env.example). Run every command from /opt/kvitt.
|
# with /opt/kvitt/.env (from .env.example). Run every command from /opt/kvitt.
|
||||||
# Only caddy publishes a port (8443); the server, Caddy's admin API and the sidecars are reachable only on the
|
# TLS terminates in Nginx Proxy Manager on the Home Assistant Pi (SPEC §10.5, D-120), which forwards plain HTTP over
|
||||||
# internal network `kvitt` (SPEC §8.9).
|
# the LAN to the server's published port 8080. Only the server publishes a port; backup is reachable only on the
|
||||||
|
# internal network `kvitt` (SPEC §8.9). The router forwards the internet only to NPM, never to this Pi.
|
||||||
# Image names: ${KVITT_REGISTRY}/kvitt-<name>, the names the Gitea Actions workflow pushes (D-104).
|
# Image names: ${KVITT_REGISTRY}/kvitt-<name>, the names the Gitea Actions workflow pushes (D-104).
|
||||||
|
|
||||||
name: kvitt
|
name: kvitt
|
||||||
@@ -13,49 +14,6 @@ x-logging: &logging
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
caddy:
|
|
||||||
# Official image, pinned by version and multi-arch index digest (resolved 2026-10-18, infra/README.md).
|
|
||||||
image: docker.io/library/caddy:2.11.7@sha256:685ae3d7c34d9b5f8caeddc5d14051db7bbd88ff1f5a2fa0c16bd8ca328fb7af
|
|
||||||
restart: unless-stopped
|
|
||||||
# Non-root (D-117): uid 10002, plus group 1000 (cert-sync's group) to read the 0640 files in the 0750 /certs.
|
|
||||||
# 8443 needs no capability, but the image's /usr/bin/caddy carries the file capability cap_net_bind_service=ep,
|
|
||||||
# and the kernel refuses to exec it when that capability is not in the bounding set (checked 2026-10-18:
|
|
||||||
# "exec container process /usr/bin/caddy: Operation not permitted" with cap_drop ALL alone). So NET_BIND_SERVICE
|
|
||||||
# is the one capability kept. caddy_data (/data) is owned by 10002 (install.sh); /config (autosave of the loaded
|
|
||||||
# config) and /tmp are tmpfs, the rest of the root filesystem is read-only.
|
|
||||||
user: "10002:10002"
|
|
||||||
group_add: ["1000"]
|
|
||||||
read_only: true
|
|
||||||
cap_drop: [ALL]
|
|
||||||
cap_add: [NET_BIND_SERVICE]
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
tmpfs:
|
|
||||||
- /config
|
|
||||||
- /tmp
|
|
||||||
mem_limit: 64m
|
|
||||||
ports:
|
|
||||||
- "8443:8443"
|
|
||||||
environment:
|
|
||||||
TZ: Europe/Stockholm
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- certs:/certs:ro
|
|
||||||
- caddy_data:/data
|
|
||||||
depends_on:
|
|
||||||
cert-sync:
|
|
||||||
condition: service_healthy
|
|
||||||
server:
|
|
||||||
condition: service_started
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:2019/config/ >/dev/null"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
logging: *logging
|
|
||||||
networks: [kvitt]
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
image: ${KVITT_REGISTRY:?set KVITT_REGISTRY in .env}/kvitt-server:${KVITT_TAG:?set KVITT_TAG in .env}
|
image: ${KVITT_REGISTRY:?set KVITT_REGISTRY in .env}/kvitt-server:${KVITT_TAG:?set KVITT_TAG in .env}
|
||||||
command: ["serve"]
|
command: ["serve"]
|
||||||
@@ -66,11 +24,17 @@ services:
|
|||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
mem_limit: 160m
|
mem_limit: 160m
|
||||||
|
# On all interfaces of the Pi: NPM on the Home Assistant Pi connects to <KVITT_PI_HOST>:8080 over the LAN.
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
TZ: Europe/Stockholm
|
TZ: Europe/Stockholm
|
||||||
# The remaining server settings keep their SPEC §10.4 defaults (KVITT_PUBLIC_URL
|
|
||||||
# https://icehack.asuscomm.com:8443, KVITT_TRUSTED_PROXY caddy, KVITT_FCM_PROJECT_ID kvitt-1337, ...).
|
|
||||||
KVITT_DATA_DIR: /data
|
KVITT_DATA_DIR: /data
|
||||||
|
KVITT_PUBLIC_URL: https://${KVITT_PUBLIC_HOST:?set KVITT_PUBLIC_HOST in .env}:8443
|
||||||
|
# NPM's LAN address: only a request whose TCP peer is this address may set X-Forwarded-For, whose last entry
|
||||||
|
# is then the client IP of the rate limits (SPEC §8.5, §8.6, D-120). An IP literal (install.sh checks it).
|
||||||
|
KVITT_TRUSTED_PROXY: ${HA_HOST:?set HA_HOST in .env}
|
||||||
|
# The remaining server settings keep their SPEC §10.4 defaults (KVITT_FCM_PROJECT_ID kvitt-1337, ...).
|
||||||
volumes:
|
volumes:
|
||||||
- data:/data
|
- data:/data
|
||||||
- /opt/kvitt/secrets/fcm-service-account.json:/secrets/fcm-service-account.json:ro
|
- /opt/kvitt/secrets/fcm-service-account.json:/secrets/fcm-service-account.json:ro
|
||||||
@@ -85,34 +49,6 @@ services:
|
|||||||
logging: *logging
|
logging: *logging
|
||||||
networks: [kvitt]
|
networks: [kvitt]
|
||||||
|
|
||||||
cert-sync:
|
|
||||||
image: ${KVITT_REGISTRY}/kvitt-cert-sync:${KVITT_TAG}
|
|
||||||
restart: unless-stopped
|
|
||||||
user: "1000:1000"
|
|
||||||
cap_drop: [ALL]
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
mem_limit: 32m
|
|
||||||
environment:
|
|
||||||
TZ: Europe/Stockholm
|
|
||||||
HA_HOST: ${HA_HOST:?set HA_HOST in .env}
|
|
||||||
HA_PORT: ${HA_PORT:-22}
|
|
||||||
HA_USER: ${HA_USER:?set HA_USER in .env}
|
|
||||||
HA_CERT_DIR: ${HA_CERT_DIR:?set HA_CERT_DIR in .env}
|
|
||||||
volumes:
|
|
||||||
- certs:/certs
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- /opt/kvitt/secrets/ha_ssh_key:/secrets/ha_ssh_key:ro
|
|
||||||
- /opt/kvitt/secrets/ha_known_hosts:/secrets/known_hosts:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "test -s /certs/fullchain.pem && test -s /certs/privkey.pem"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 60s
|
|
||||||
logging: *logging
|
|
||||||
networks: [kvitt]
|
|
||||||
|
|
||||||
backup:
|
backup:
|
||||||
image: ${KVITT_REGISTRY}/kvitt-backup:${KVITT_TAG}
|
image: ${KVITT_REGISTRY}/kvitt-backup:${KVITT_TAG}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -142,8 +78,6 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
certs:
|
|
||||||
caddy_data:
|
|
||||||
backups:
|
backups:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
+4
-2
@@ -5,7 +5,8 @@
|
|||||||
# to every deploy); skipped when no database exists yet, or with --skip-backup (rollback while Drive is down)
|
# to every deploy); skipped when no database exists yet, or with --skip-backup (rollback while Drive is down)
|
||||||
# 2. sets KVITT_TAG in /opt/kvitt/.env (the previous tag is kept in /opt/kvitt/previous-tag)
|
# 2. sets KVITT_TAG in /opt/kvitt/.env (the previous tag is kept in /opt/kvitt/previous-tag)
|
||||||
# 3. docker compose pull, prints the pulled image digests and appends them to /opt/kvitt/deployed-digests (D-117),
|
# 3. docker compose pull, prints the pulled image digests and appends them to /opt/kvitt/deployed-digests (D-117),
|
||||||
# docker compose up -d, waits for server to be healthy, prints GET /health
|
# docker compose up -d, waits for server to be healthy, prints GET /health on port 8080 and through Nginx
|
||||||
|
# Proxy Manager (D-120; a failure through NPM is a warning, the deploy itself succeeded)
|
||||||
# Rollback after a failed migration: /opt/kvitt/deploy.sh "$(cat /opt/kvitt/previous-tag)"; if a migration had
|
# Rollback after a failed migration: /opt/kvitt/deploy.sh "$(cat /opt/kvitt/previous-tag)"; if a migration had
|
||||||
# partially applied, restore the pre-deploy backup (infra/RESTORE.md).
|
# partially applied, restore the pre-deploy backup (infra/RESTORE.md).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -55,5 +56,6 @@ if ! wait_healthy server 180; then
|
|||||||
die "server is not healthy on $tag. Roll back: $KVITT_DIR/deploy.sh ${current:-<previous tag>} --skip-backup"
|
die "server is not healthy on $tag. Roll back: $KVITT_DIR/deploy.sh ${current:-<previous tag>} --skip-backup"
|
||||||
fi
|
fi
|
||||||
compose ps
|
compose ps
|
||||||
print_health || die "server is healthy but /health through Caddy failed (docker compose logs caddy cert-sync)"
|
print_health || die "server is healthy but GET /health on port 8080 failed (docker compose logs server)"
|
||||||
|
print_proxy_health || say "WARNING: /health through Nginx Proxy Manager failed: Kvitt is not reachable from outside (README, Troubleshooting)"
|
||||||
say "Deployed $tag."
|
say "Deployed $tag."
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ compose run --rm --no-deps -T -v "$src:/incoming:ro" --entrypoint /bin/sh backup
|
|||||||
rm -f "/data/apk/kvitt-$v.apk"; echo "removed kvitt-$v.apk"
|
rm -f "/data/apk/kvitt-$v.apk"; echo "removed kvitt-$v.apk"
|
||||||
done
|
done
|
||||||
echo "latest.json installed"'
|
echo "latest.json installed"'
|
||||||
say "GET https://$PUBLIC_HOST:$PUBLIC_PORT/apk/latest (via this Pi):"
|
say "GET $SERVER_URL/apk/latest:"
|
||||||
curl -fsS --max-time 15 --resolve "$PUBLIC_HOST:$PUBLIC_PORT:127.0.0.1" "https://$PUBLIC_HOST:$PUBLIC_PORT/apk/latest" \
|
curl -fsS --max-time 15 "$SERVER_URL/apk/latest" || die "GET /apk/latest failed"
|
||||||
|| die "GET /apk/latest failed"
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
+42
-20
@@ -1,15 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Kvitt Pi install (infra/README.md "Pi: what the owner does"). Run ON the Kvitt Pi, as the owner's user, from the
|
# Kvitt Pi install (infra/README.md "Pi: what the owner does"). Run ON the Kvitt Pi, as the owner's user, from the
|
||||||
# bundle folder copied from the Mac (infra/pi/): cd ~/kvitt-pi && ./install.sh
|
# bundle folder copied from the Mac (infra/pi/): cd ~/kvitt-pi && ./install.sh
|
||||||
# Idempotent: safe to run again after changing .env, the secrets, compose.yaml or the Caddyfile.
|
# Idempotent: safe to run again after changing .env, the secrets or compose.yaml.
|
||||||
# 1. checks Docker Engine and the compose plugin
|
# 1. checks Docker Engine and the compose plugin
|
||||||
# 2. checks .env (no CHANGE_ME left in the keys the stack reads)
|
# 2. checks .env (no CHANGE_ME left in the keys the stack reads; KVITT_PUBLIC_HOST a host name, HA_HOST an IPv4
|
||||||
# 3. creates /opt/kvitt/{secrets,import,apk} and installs the secret files from ./secrets/ with their modes
|
# address)
|
||||||
|
# 3. creates /opt/kvitt/{secrets,import,apk}, installs the secret files from ./secrets/ with their modes and
|
||||||
|
# removes what the Caddy and cert-sync stack of before D-120 left in /opt/kvitt
|
||||||
# 4. trusts the plain-HTTP registry in /etc/docker/daemon.json (insecure-registries) and logs in when credentials are set
|
# 4. trusts the plain-HTTP registry in /etc/docker/daemon.json (insecure-registries) and logs in when credentials are set
|
||||||
# 5. copies compose.yaml, Caddyfile, .env and the operation scripts to /opt/kvitt; a KVITT_TAG that deploy.sh set
|
# 5. copies compose.yaml, .env and the operation scripts to /opt/kvitt; a KVITT_TAG that deploy.sh set
|
||||||
# in /opt/kvitt/.env is kept, so re-running an older bundle never rolls the server back to the bundle's tag
|
# in /opt/kvitt/.env is kept, so re-running an older bundle never rolls the server back to the bundle's tag
|
||||||
# 6. pulls the images (digests recorded in /opt/kvitt/deployed-digests), gives the named volumes their owners,
|
# 6. pulls the images (digests recorded in /opt/kvitt/deployed-digests), gives the named volumes their owners,
|
||||||
# starts the stack, waits for /health
|
# starts the stack, waits for /health on port 8080, then tries /health through Nginx Proxy Manager
|
||||||
# Uses sudo for /opt/kvitt, the secret files and the Docker daemon configuration.
|
# Uses sudo for /opt/kvitt, the secret files and the Docker daemon configuration.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
bundle="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
bundle="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -26,26 +28,33 @@ command -v python3 >/dev/null || die "python3 is missing (step 4 edits daemon.js
|
|||||||
# 2. .env
|
# 2. .env
|
||||||
env_src="$bundle/.env"
|
env_src="$bundle/.env"
|
||||||
[[ -f "$env_src" ]] || die "no .env next to install.sh: cp .env.example .env and fill it (README step 4)"
|
[[ -f "$env_src" ]] || die "no .env next to install.sh: cp .env.example .env and fill it (README step 4)"
|
||||||
for key in KVITT_REGISTRY KVITT_TAG HA_HOST HA_PORT HA_USER HA_CERT_DIR RCLONE_REMOTE RCLONE_FOLDER_ID; do
|
for key in KVITT_REGISTRY KVITT_TAG KVITT_PUBLIC_HOST HA_HOST RCLONE_REMOTE RCLONE_FOLDER_ID; do
|
||||||
value=$(env_value "$key" "$env_src")
|
value=$(env_value "$key" "$env_src")
|
||||||
[[ -n "$value" ]] || die ".env: $key is empty"
|
[[ -n "$value" ]] || die ".env: $key is empty"
|
||||||
[[ "$value" != CHANGE_ME* ]] || die ".env: $key is still $value"
|
[[ "$value" != CHANGE_ME* ]] || die ".env: $key is still $value"
|
||||||
done
|
done
|
||||||
|
# The server compares the TCP peer with KVITT_TRUSTED_PROXY (= HA_HOST). A host name would have to resolve inside
|
||||||
|
# the container to exactly NPM's LAN address (mDNS names such as homeassistant.local do not), so require the address.
|
||||||
|
ha_host=$(env_value HA_HOST "$env_src")
|
||||||
|
[[ "$ha_host" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] \
|
||||||
|
|| die ".env: HA_HOST must be the Home Assistant Pi's LAN IPv4 address (for example 192.168.1.20), not $ha_host"
|
||||||
|
public_host=$(env_value KVITT_PUBLIC_HOST "$env_src")
|
||||||
|
[[ "$public_host" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}$ ]] \
|
||||||
|
|| die ".env: KVITT_PUBLIC_HOST must be a host name such as kvitt.example.org (no scheme, no port), not $public_host"
|
||||||
|
[[ "$public_host" != icehack.asuscomm.com ]] \
|
||||||
|
|| die ".env: KVITT_PUBLIC_HOST cannot be icehack.asuscomm.com, NPM's proxy host for Home Assistant (D-120)"
|
||||||
registry=$(env_value KVITT_REGISTRY "$env_src")
|
registry=$(env_value KVITT_REGISTRY "$env_src")
|
||||||
registry_host=${registry%%/*}
|
registry_host=${registry%%/*}
|
||||||
registry_user=$(env_value KVITT_REGISTRY_USER "$env_src")
|
registry_user=$(env_value KVITT_REGISTRY_USER "$env_src")
|
||||||
registry_token=$(env_value KVITT_REGISTRY_TOKEN "$env_src")
|
registry_token=$(env_value KVITT_REGISTRY_TOKEN "$env_src")
|
||||||
|
|
||||||
# 3. Directories and secrets. uid 1000 = cert-sync, uid 10001 = server and backup, uid 10002 = caddy (SPEC §10.2,
|
# 3. Directories and secrets. uid 10001 = server and backup (SPEC §10.2, §10.7, D-105).
|
||||||
# §10.7, D-023, D-105, D-117).
|
|
||||||
say "Creating $KVITT_DIR"
|
say "Creating $KVITT_DIR"
|
||||||
sudo install -d -m 0755 -o "$(id -u)" -g "$(id -g)" "$KVITT_DIR" "$KVITT_DIR/import" "$KVITT_DIR/apk"
|
sudo install -d -m 0755 -o "$(id -u)" -g "$(id -g)" "$KVITT_DIR" "$KVITT_DIR/import" "$KVITT_DIR/apk"
|
||||||
sudo install -d -m 0700 -o root -g root "$KVITT_DIR/secrets"
|
sudo install -d -m 0700 -o root -g root "$KVITT_DIR/secrets"
|
||||||
# name : owner uid : group : mode
|
# name : owner uid : group : mode
|
||||||
secret_specs=(
|
secret_specs=(
|
||||||
"fcm-service-account.json:0:10001:0440"
|
"fcm-service-account.json:0:10001:0440"
|
||||||
"ha_ssh_key:1000:1000:0600"
|
|
||||||
"ha_known_hosts:1000:1000:0600"
|
|
||||||
"rclone.conf:10001:10001:0600"
|
"rclone.conf:10001:10001:0600"
|
||||||
)
|
)
|
||||||
missing=()
|
missing=()
|
||||||
@@ -71,6 +80,13 @@ done
|
|||||||
if [[ -d "$bundle/secrets" ]]; then
|
if [[ -d "$bundle/secrets" ]]; then
|
||||||
say " The copies in $bundle/secrets/ are no longer needed: rm -rf $bundle/secrets"
|
say " The copies in $bundle/secrets/ are no longer needed: rm -rf $bundle/secrets"
|
||||||
fi
|
fi
|
||||||
|
# Left by the Caddy and cert-sync stack of before D-120: the Home Assistant SSH key must not stay on this Pi.
|
||||||
|
for old in "$KVITT_DIR/secrets/ha_ssh_key" "$KVITT_DIR/secrets/ha_known_hosts" "$KVITT_DIR/Caddyfile"; do
|
||||||
|
if sudo test -e "$old"; then
|
||||||
|
sudo rm -rf "$old"
|
||||||
|
say " removed $old (not used since D-120)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# 4. Registry trust (plain HTTP on the home Wi-Fi, SPEC §10.3) and optional login.
|
# 4. Registry trust (plain HTTP on the home Wi-Fi, SPEC §10.3) and optional login.
|
||||||
daemon_json=/etc/docker/daemon.json
|
daemon_json=/etc/docker/daemon.json
|
||||||
@@ -113,7 +129,6 @@ installed_tag=""
|
|||||||
[[ -f "$KVITT_DIR/.env" ]] && installed_tag=$(env_value KVITT_TAG)
|
[[ -f "$KVITT_DIR/.env" ]] && installed_tag=$(env_value KVITT_TAG)
|
||||||
bundle_tag=$(env_value KVITT_TAG "$env_src")
|
bundle_tag=$(env_value KVITT_TAG "$env_src")
|
||||||
install -m 0644 "$bundle/compose.yaml" "$KVITT_DIR/compose.yaml"
|
install -m 0644 "$bundle/compose.yaml" "$KVITT_DIR/compose.yaml"
|
||||||
install -m 0644 "$bundle/Caddyfile" "$KVITT_DIR/Caddyfile"
|
|
||||||
install -m 0600 "$env_src" "$KVITT_DIR/.env"
|
install -m 0600 "$env_src" "$KVITT_DIR/.env"
|
||||||
if [[ -n "$installed_tag" && "$installed_tag" != "$bundle_tag" ]]; then
|
if [[ -n "$installed_tag" && "$installed_tag" != "$bundle_tag" ]]; then
|
||||||
env_set KVITT_TAG "$installed_tag"
|
env_set KVITT_TAG "$installed_tag"
|
||||||
@@ -122,7 +137,7 @@ fi
|
|||||||
for script in lib.sh deploy.sh bootstrap.sh backup-now.sh install-apk.sh; do
|
for script in lib.sh deploy.sh bootstrap.sh backup-now.sh install-apk.sh; do
|
||||||
install -m 0755 "$bundle/$script" "$KVITT_DIR/$script"
|
install -m 0755 "$bundle/$script" "$KVITT_DIR/$script"
|
||||||
done
|
done
|
||||||
say "Installed compose.yaml, Caddyfile, .env and the scripts in $KVITT_DIR"
|
say "Installed compose.yaml, .env and the scripts in $KVITT_DIR"
|
||||||
|
|
||||||
# 6. Pull, volume owners, start.
|
# 6. Pull, volume owners, start.
|
||||||
compose config --quiet
|
compose config --quiet
|
||||||
@@ -133,17 +148,24 @@ compose create
|
|||||||
# backup image, which mounts the same data volume, sets /data). Runs as root once per install, then exits.
|
# backup image, which mounts the same data volume, sets /data). Runs as root once per install, then exits.
|
||||||
compose run --rm --no-deps -T --user 0:0 --cap-add CHOWN --entrypoint /bin/sh backup \
|
compose run --rm --no-deps -T --user 0:0 --cap-add CHOWN --entrypoint /bin/sh backup \
|
||||||
-c 'chown 10001:10001 /data /backups && mkdir -p /data/apk && chown 10001:10001 /data/apk'
|
-c 'chown 10001:10001 /data /backups && mkdir -p /data/apk && chown 10001:10001 /data/apk'
|
||||||
# /certs: cert-sync (1000) writes, Caddy (10002, supplementary group 1000) reads; /data of Caddy: 10002 (D-117).
|
# --remove-orphans also removes the caddy and cert-sync containers of a stack installed before D-120.
|
||||||
compose run --rm --no-deps -T --user 0:0 --cap-add CHOWN --cap-add FOWNER --entrypoint /bin/sh cert-sync \
|
|
||||||
-c 'chown 1000:1000 /certs && chmod 0750 /certs'
|
|
||||||
compose run --rm --no-deps -T --user 0:0 --cap-add CHOWN --entrypoint /bin/sh caddy \
|
|
||||||
-c 'chown -R 10002:10002 /data'
|
|
||||||
compose up -d --remove-orphans
|
compose up -d --remove-orphans
|
||||||
|
# Their volumes: certs held a copy of the certificate's private key.
|
||||||
|
for volume in kvitt_certs kvitt_caddy_data; do
|
||||||
|
if docker volume inspect "$volume" >/dev/null 2>&1; then
|
||||||
|
if docker volume rm "$volume" >/dev/null; then
|
||||||
|
say "Removed the volume $volume (not used since D-120)"
|
||||||
|
else
|
||||||
|
say "Could not remove the volume $volume (still in use?): docker volume rm $volume"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
ok=1
|
ok=1
|
||||||
wait_healthy server 180 || ok=0
|
wait_healthy server 180 || ok=0
|
||||||
wait_healthy cert-sync 120 || { ok=0; say "cert-sync has no certificate yet: docker compose logs cert-sync (README, Troubleshooting)"; }
|
|
||||||
wait_healthy caddy 120 || ok=0
|
|
||||||
compose ps
|
compose ps
|
||||||
print_health || ok=0
|
print_health || ok=0
|
||||||
(( ok == 1 )) || die "the stack is not fully up yet; see the lines above"
|
(( ok == 1 )) || die "the stack is not fully up yet; see the lines above"
|
||||||
say "Kvitt is running. Next: $KVITT_DIR/bootstrap.sh (first install only)."
|
if ! print_proxy_health; then
|
||||||
|
say "Not reachable through Nginx Proxy Manager yet: expected until its proxy host is set up (README step 7)."
|
||||||
|
fi
|
||||||
|
say "Kvitt is running. Next: README step 7 (NPM proxy host), then $KVITT_DIR/bootstrap.sh (first install only)."
|
||||||
|
|||||||
+23
-6
@@ -4,8 +4,10 @@
|
|||||||
# shellcheck disable=SC2034 # variables are used by the scripts that source this file
|
# shellcheck disable=SC2034 # variables are used by the scripts that source this file
|
||||||
|
|
||||||
KVITT_DIR=${KVITT_DIR:-/opt/kvitt}
|
KVITT_DIR=${KVITT_DIR:-/opt/kvitt}
|
||||||
PUBLIC_HOST=icehack.asuscomm.com
|
# The server's port, published on the Pi's LAN interfaces; Nginx Proxy Manager forwards to it (D-120).
|
||||||
PUBLIC_PORT=8443
|
SERVER_URL=http://localhost:8080
|
||||||
|
# Nginx Proxy Manager's HTTPS port on the Home Assistant Pi (the router forwards external 8443 to it, D-120).
|
||||||
|
NPM_HTTPS_PORT=443
|
||||||
|
|
||||||
say() { printf '%s\n' "$*"; }
|
say() { printf '%s\n' "$*"; }
|
||||||
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
@@ -56,14 +58,29 @@ wait_healthy() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# GET /health through Caddy on this Pi, with the real certificate check (no -k): proves TLS and the proxy path.
|
# GET /health from the server's published port on this Pi.
|
||||||
print_health() {
|
print_health() {
|
||||||
local out
|
local out
|
||||||
if out=$(curl -fsS --max-time 15 --resolve "$PUBLIC_HOST:$PUBLIC_PORT:127.0.0.1" "https://$PUBLIC_HOST:$PUBLIC_PORT/health" 2>&1); then
|
if out=$(curl -fsS --max-time 15 "$SERVER_URL/health" 2>&1); then
|
||||||
say "GET https://$PUBLIC_HOST:$PUBLIC_PORT/health (via this Pi): $out"
|
say "GET $SERVER_URL/health: $out"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
say "GET https://$PUBLIC_HOST:$PUBLIC_PORT/health (via this Pi) failed: $out"
|
say "GET $SERVER_URL/health failed: $out"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# GET /health through Nginx Proxy Manager on the Home Assistant Pi (HA_HOST in .env) for KVITT_PUBLIC_HOST, with
|
||||||
|
# the real certificate check (no -k): proves NPM's proxy host, its certificate and the LAN path to this Pi's port
|
||||||
|
# 8080. The router forward is not on this path.
|
||||||
|
print_proxy_health() {
|
||||||
|
local out ha_host host
|
||||||
|
ha_host=$(env_value HA_HOST)
|
||||||
|
host=$(env_value KVITT_PUBLIC_HOST)
|
||||||
|
if out=$(curl -fsS --max-time 15 --resolve "$host:$NPM_HTTPS_PORT:$ha_host" "https://$host:$NPM_HTTPS_PORT/health" 2>&1); then
|
||||||
|
say "GET https://$host:$NPM_HTTPS_PORT/health (via NPM at $ha_host): $out"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
say "GET https://$host:$NPM_HTTPS_PORT/health (via NPM at $ha_host) failed: $out"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user