Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b357ef95d8 | ||
|
|
67422663b7 | ||
|
|
88705fec88 | ||
|
|
c098807aa4 | ||
|
|
e1eee2d3c7 | ||
|
|
ff83daed1e | ||
|
|
080ae343e6 | ||
|
|
8d3185f8ab | ||
|
|
ff40a71145 |
No files matched your search
@@ -1,88 +0,0 @@
|
|||||||
# Build and deployment workflows
|
|
||||||
|
|
||||||
Gitea Actions checks this repository, builds its custom images, and deploys
|
|
||||||
selected services to the workstation. Workflows use the self-hosted runner labels
|
|
||||||
`linux`, `arch`, and `homelab`; deployment jobs also require `prod`.
|
|
||||||
|
|
||||||
## Checks
|
|
||||||
|
|
||||||
`ci.yaml` runs Compose validation, actionlint, ShellCheck, Prettier, Ruff,
|
|
||||||
yamllint, hadolint, and kubeconform. Tool versions are pinned in
|
|
||||||
`workflows/tool-versions.env` and installed by `install-ci-tools.sh`.
|
|
||||||
|
|
||||||
Compose CI checks structure without resolving local environment files or paths.
|
|
||||||
On the reviewed main commit it only discovers standard filenames; the
|
|
||||||
`fix/deploy-validation` branch adds the manual Compose entry points too.
|
|
||||||
|
|
||||||
Kubeconform validates known resource schemas. Unknown CRDs are skipped. On main,
|
|
||||||
CI also attempts server-side dry-runs for marked services; these require an
|
|
||||||
existing namespace and contact the cluster's admission webhooks. A cluster that
|
|
||||||
is unreachable produces a warning and skips that CI pass. Deploy validation has
|
|
||||||
its own dry-run stage.
|
|
||||||
|
|
||||||
`renovate-ci.yaml` validates Renovate settings and checks that its generated
|
|
||||||
ConfigMap matches `renovate/renovate.json`.
|
|
||||||
|
|
||||||
## Image builds
|
|
||||||
|
|
||||||
CI builds changed custom images for `errorpages`, both `homepages` variants, and
|
|
||||||
the two `edu_master` Python services. Main builds publish `main`, `prod`, and a
|
|
||||||
commit tag. Dev builds publish `dev`. Build jobs wait for the lint and manifest
|
|
||||||
checks.
|
|
||||||
|
|
||||||
Kubernetes deployment resolves the lab's own registry images to digests, preferring
|
|
||||||
commit-specific tags. Third-party image versions remain declared in the manifests.
|
|
||||||
|
|
||||||
## Deploy selection
|
|
||||||
|
|
||||||
`workflows/deploy-lib.sh` owns the stage logic; `ssh-run.sh` invokes it on the
|
|
||||||
workstation through SSH. Kubernetes selection uses `k8s/active`; Compose selection
|
|
||||||
uses an `active` file beside a standard `compose.yaml` or `compose.yml`.
|
|
||||||
Kustomize overlays are supported, although the current tree primarily contains
|
|
||||||
plain manifests.
|
|
||||||
|
|
||||||
Secret files, examples, Helm values, and patch files are excluded from plain
|
|
||||||
manifest selection. Create local Kubernetes Secrets separately in their target
|
|
||||||
namespaces. The Helm table lists Prometheus, Loki, Alloy, and Reloader, with each
|
|
||||||
release controlled by its configured marker. Other charts need separate setup.
|
|
||||||
|
|
||||||
## Trigger and required settings
|
|
||||||
|
|
||||||
Automatic deployment follows a successful main CI run when the repository Actions
|
|
||||||
variable `AUTODEPLOY` is `true`. The manual deploy workflow bypasses that switch
|
|
||||||
and targets the fetched main branch when no validated commit SHA is provided.
|
|
||||||
A manual dispatch does not prove that this commit passed CI.
|
|
||||||
|
|
||||||
Configure the Actions secrets `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_SSH_KEY`, and,
|
|
||||||
where needed, `DEPLOY_PORT` and `DEPLOY_PATH`. Registry publishing uses
|
|
||||||
`REGISTRY_USERNAME` and `REGISTRY_PASSWORD`. The remote user needs access to Git,
|
|
||||||
Docker, kubectl, Helm, jq, and the state directory used for snapshots.
|
|
||||||
|
|
||||||
Keep `APPLY_PRUNE` false on the reviewed implementation: its per-file prune loop
|
|
||||||
is unsafe. `fix/deploy-prune-guard` rejects that option before changes are applied.
|
|
||||||
|
|
||||||
Preflight fetches and resets the remote checkout. It refuses when tracked files
|
|
||||||
have local changes; ignored local env and Secret files stay in place. Do not use
|
|
||||||
a development checkout with uncommitted tracked changes as the deployment target.
|
|
||||||
|
|
||||||
## Stages and recovery
|
|
||||||
|
|
||||||
1. Preflight fetches the target commit and checks the remote working tree.
|
|
||||||
2. Validate selects services, parses Compose, performs Kubernetes dry-runs, and
|
|
||||||
checks referenced Secrets.
|
|
||||||
3. Apply Kubernetes records a workload snapshot, upgrades selected Helm releases,
|
|
||||||
applies resources, and refreshes owned custom images.
|
|
||||||
4. Apply Compose recreates marked stacks and checks container state.
|
|
||||||
5. Verify Kubernetes checks changed workloads and attempts rollback for failures.
|
|
||||||
6. Smoke probes public routes after verification.
|
|
||||||
|
|
||||||
The two apply jobs share a remote lock. Workflow concurrency queues deployments
|
|
||||||
rather than interrupting an older apply. Snapshots live under
|
|
||||||
`$XDG_STATE_HOME/homelab-deploy`, or `~/.local/state/homelab-deploy` by default.
|
|
||||||
They contain the pre-apply workload data and commit identifier.
|
|
||||||
|
|
||||||
Rollback uses workload revisions. It does not restore ConfigMaps, Secrets,
|
|
||||||
database schemas, or data. Helm-owned workloads are handled through the Helm
|
|
||||||
upgrade's rollback path; the generic rollback skips them. Compose has no automatic
|
|
||||||
rollback. See the [review](../docs/repository-review.md) for remaining recovery
|
|
||||||
limitations, including SSH retries and serial rollback timing.
|
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Local regressions only: kubectl is mocked and Docker is used for config parsing.
|
||||||
|
set -euo pipefail
|
||||||
|
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
scratch="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$scratch"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "$scratch/repo/app" "$scratch/repo/postgres" "$scratch/repo/netbird" "$scratch/repo/renovate"
|
||||||
|
git -C "$scratch/repo" init -q
|
||||||
|
for file in app/compose.yaml postgres/shared-compose.yaml netbird/client.compose.yaml renovate/renovate-compose.yaml; do
|
||||||
|
touch "$scratch/repo/$file"
|
||||||
|
done
|
||||||
|
git -C "$scratch/repo" add .
|
||||||
|
# shellcheck source=../workflows/compose-lint.sh
|
||||||
|
source "$repo/.gitea/workflows/compose-lint.sh"
|
||||||
|
actual="$(cd "$scratch/repo" && compose_files)"
|
||||||
|
expected=$'app/compose.yaml\nnetbird/client.compose.yaml\npostgres/shared-compose.yaml\nrenovate/renovate-compose.yaml'
|
||||||
|
[ "$actual" = "$expected" ] || { echo 'Compose discovery missed a file' >&2; exit 1; }
|
||||||
|
|
||||||
|
cat >"$scratch/compose.yaml" <<'YAML'
|
||||||
|
services:
|
||||||
|
example:
|
||||||
|
image: busybox:1.37.0
|
||||||
|
environment:
|
||||||
|
REQUIRED: ${HOMELAB_TEST_REQUIRED:?required for this regression}
|
||||||
|
YAML
|
||||||
|
unset HOMELAB_TEST_REQUIRED
|
||||||
|
if validate_compose_file "$scratch/compose.yaml" >"$scratch/config.log" 2>&1; then
|
||||||
|
echo 'Full Compose validation accepted a missing variable' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'required for this regression' "$scratch/config.log"
|
||||||
|
HOMELAB_TEST_REQUIRED=present validate_compose_file "$scratch/compose.yaml"
|
||||||
|
|
||||||
|
cat >"$scratch/resources.json" <<'JSON'
|
||||||
|
{"kind":"List","items":[
|
||||||
|
{"kind":"Deployment","metadata":{"namespace":"app"},"spec":{"template":{"spec":{
|
||||||
|
"containers":[{"envFrom":[{"secretRef":{"name":"credentials"}},{"secretRef":{"name":"optional","optional":true}}],"env":[{"valueFrom":{"secretKeyRef":{"name":"credentials","key":"password"}}}]}],
|
||||||
|
"initContainers":[{"envFrom":[{"secretRef":{"name":"init"}}]}],
|
||||||
|
"imagePullSecrets":[{"name":"registry"}],
|
||||||
|
"volumes":[{"secret":{"secretName":"mounted"}},{"projected":{"sources":[{"secret":{"name":"projected"}},{"secret":{"name":"optional-projected","optional":true}}]}}]
|
||||||
|
}}}},
|
||||||
|
{"kind":"CronJob","metadata":{},"spec":{"jobTemplate":{"spec":{"template":{"spec":{"containers":[{"envFrom":[{"secretRef":{"name":"cron"}}]}]}}}}}},
|
||||||
|
{"kind":"IngressRoute","metadata":{"namespace":"app"},"spec":{"tls":{"secretName":"controller-issued-tls"}}}
|
||||||
|
]}
|
||||||
|
JSON
|
||||||
|
actual="$(jq -r -f "$repo/.gitea/workflows/secret-references.jq" "$scratch/resources.json" | sort)"
|
||||||
|
expected=$'app credentials\napp init\napp mounted\napp projected\napp registry\ndefault cron'
|
||||||
|
[ "$actual" = "$expected" ] || { echo "Unexpected Secret references: $actual" >&2; exit 1; }
|
||||||
|
|
||||||
|
REPO="$repo"
|
||||||
|
# shellcheck source=../workflows/deploy-lib.sh
|
||||||
|
source "$repo/.gitea/workflows/deploy-lib.sh"
|
||||||
|
K8S_MANIFESTS=("$scratch/resources.json")
|
||||||
|
KUSTOMIZE_APPS=()
|
||||||
|
# No live cluster access. Reject credentials in app even if they exist elsewhere.
|
||||||
|
kubectl() {
|
||||||
|
case "$1" in
|
||||||
|
create) cat "$scratch/resources.json" ;;
|
||||||
|
get)
|
||||||
|
if [ "$3" = credentials ] && [ "$5" = app ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*) echo "Unexpected kubectl invocation: $*" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
if check_referenced_secrets >"$scratch/secrets.log"; then
|
||||||
|
echo 'Namespace-scoped Secret check accepted a missing Secret' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'MISSING OR UNREADABLE: app/credentials' "$scratch/secrets.log"
|
||||||
|
# API/rendering errors must not produce an empty reference list and pass.
|
||||||
|
kubectl() { return 1; }
|
||||||
|
if check_referenced_secrets >"$scratch/secrets.log"; then
|
||||||
|
echo 'Secret check accepted a failed manifest render' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s\n' 'Deploy validation regressions passed.'
|
||||||
@@ -3,7 +3,7 @@ name: ci
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
tools_dir="$(bash .gitea/workflows/install-ci-tools.sh shellcheck)"
|
tools_dir="$(bash .gitea/workflows/install-ci-tools.sh shellcheck jq)"
|
||||||
export PATH="$tools_dir:$PATH"
|
export PATH="$tools_dir:$PATH"
|
||||||
mapfile -t scripts < <(
|
mapfile -t scripts < <(
|
||||||
git ls-files '*.sh' ':(glob)**/*.bash'
|
git ls-files '*.sh' ':(glob)**/*.bash'
|
||||||
@@ -98,6 +98,7 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
shellcheck --external-sources --source-path=SCRIPTDIR --severity=style "${scripts[@]}"
|
shellcheck --external-sources --source-path=SCRIPTDIR --severity=style "${scripts[@]}"
|
||||||
|
bash .gitea/tests/deploy-validation.sh
|
||||||
|
|
||||||
lint-prettier:
|
lint-prettier:
|
||||||
runs-on: [self-hosted, linux, arch, homelab]
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
|||||||
@@ -21,8 +21,7 @@
|
|||||||
# All committed Compose files, including the ones deploy never starts.
|
# All committed Compose files, including the ones deploy never starts.
|
||||||
compose_files() {
|
compose_files() {
|
||||||
git ls-files \
|
git ls-files \
|
||||||
'*/compose.yaml' '*/compose.yml' 'compose.yaml' 'compose.yml' \
|
'*compose.yaml' '*compose.yml'
|
||||||
'*/docker-compose.yaml' '*/docker-compose.yml'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prints the flags that turn `docker compose config` into the general check.
|
# Prints the flags that turn `docker compose config` into the general check.
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ warn() {
|
|||||||
echo "WARNING: $*" >&2
|
echo "WARNING: $*" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Prune needs the complete desired set in one invocation. Per-file pruning
|
||||||
|
# treats resources from the other files as absent and can delete them.
|
||||||
|
check_prune_mode() {
|
||||||
|
if [ "$APPLY_PRUNE" = "true" ]; then
|
||||||
|
echo "ERROR: APPLY_PRUNE=true is unsupported by the per-file deploy loop." >&2
|
||||||
|
echo "Disable it; remove obsolete resources explicitly after review." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
collect_k8s() {
|
collect_k8s() {
|
||||||
git -C "$REPO" ls-files -- "$1" \
|
git -C "$REPO" ls-files -- "$1" \
|
||||||
| grep -E '\.ya?ml$' \
|
| grep -E '\.ya?ml$' \
|
||||||
@@ -686,27 +696,53 @@ stage_preflight() {
|
|||||||
git -C "$REPO" reset --hard "$target"
|
git -C "$REPO" reset --hard "$target"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Required pod Secrets, scoped to the resource namespace. TLS route Secrets are
|
||||||
|
# created by cert-manager and are not prerequisites for applying a Certificate.
|
||||||
|
check_referenced_secrets() {
|
||||||
|
local m k objects refs extracted ns name
|
||||||
|
local missing=()
|
||||||
|
refs=""
|
||||||
|
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
|
||||||
|
objects="$(kubectl create --dry-run=client --validate=false -f "$m" -o json)" || return 1
|
||||||
|
extracted="$(printf '%s' "$objects" | jq -r -f "$REPO/.gitea/workflows/secret-references.jq")" || return 1
|
||||||
|
refs+="$extracted"$'\n'
|
||||||
|
done
|
||||||
|
for k in ${KUSTOMIZE_APPS[@]+"${KUSTOMIZE_APPS[@]}"}; do
|
||||||
|
objects="$(kubectl kustomize "$k" | kubectl create --dry-run=client --validate=false -f - -o json)" || return 1
|
||||||
|
extracted="$(printf '%s' "$objects" | jq -r -f "$REPO/.gitea/workflows/secret-references.jq")" || return 1
|
||||||
|
refs+="$extracted"$'\n'
|
||||||
|
done
|
||||||
|
while read -r ns name; do
|
||||||
|
[ -n "${name:-}" ] || continue
|
||||||
|
if kubectl get secret "$name" -n "$ns" -o name >/dev/null 2>&1; then
|
||||||
|
echo " ok: $ns/$name"
|
||||||
|
else
|
||||||
|
echo " MISSING OR UNREADABLE: $ns/$name"
|
||||||
|
missing+=("$ns/$name")
|
||||||
|
fi
|
||||||
|
done < <(printf '%s' "$refs" | sort -u)
|
||||||
|
if [ "${#missing[@]}" -gt 0 ]; then
|
||||||
|
echo "ERROR: required pod Secrets are missing or unreadable:"
|
||||||
|
printf ' - %s\n' "${missing[@]}"
|
||||||
|
echo "Create them in the listed namespaces from the service's secret example."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
stage_validate() {
|
stage_validate() {
|
||||||
|
check_prune_mode || return 1
|
||||||
cd "$REPO"
|
cd "$REPO"
|
||||||
select_manifests
|
select_manifests
|
||||||
local m k cf
|
local m k cf
|
||||||
# Compose .env files and secret files are gitignored by design, so the
|
# The deploy host has the local .env and secret files. Resolve them here so
|
||||||
# workstation never has real values for the inactive stacks. This stage only
|
# missing configuration fails before either apply job changes workloads.
|
||||||
# runs the full check on active stacks; the general structure check for every
|
# CI keeps the structure-only check for inactive stacks.
|
||||||
# committed Compose file (active or not) lives in the ci workflow, which has no
|
|
||||||
# .env at all.
|
|
||||||
#
|
|
||||||
# Active stacks are still validated with interpolation and env-file resolution
|
|
||||||
# off, so required-variable guards (:?) and missing local files do not fail the
|
|
||||||
# deploy. Normalization and consistency checks stay enabled.
|
|
||||||
# shellcheck source=compose-lint.sh
|
# shellcheck source=compose-lint.sh
|
||||||
source "$REPO/.gitea/workflows/compose-lint.sh"
|
source "$REPO/.gitea/workflows/compose-lint.sh"
|
||||||
local compose_validate_flags=()
|
|
||||||
mapfile -t compose_validate_flags < <(compose_safe_flags)
|
|
||||||
log "Validate compose stacks"
|
log "Validate compose stacks"
|
||||||
for cf in ${COMPOSE_STACKS[@]+"${COMPOSE_STACKS[@]}"}; do
|
for cf in ${COMPOSE_STACKS[@]+"${COMPOSE_STACKS[@]}"}; do
|
||||||
echo " config: $cf"
|
echo " config: $cf"
|
||||||
validate_compose_file "$cf" ${compose_validate_flags[@]+"${compose_validate_flags[@]}"}
|
validate_compose_file "$cf"
|
||||||
done
|
done
|
||||||
log "Validate k8s manifests (kubectl dry-run=client)"
|
log "Validate k8s manifests (kubectl dry-run=client)"
|
||||||
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
|
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
|
||||||
@@ -724,48 +760,20 @@ stage_validate() {
|
|||||||
done
|
done
|
||||||
log "Checking referenced Secrets exist"
|
log "Checking referenced Secrets exist"
|
||||||
echo " (deploy never applies *secret*.yaml; create missing ones manually)"
|
echo " (deploy never applies *secret*.yaml; create missing ones manually)"
|
||||||
local ref_secrets=() missing_secrets=() all_secrets s
|
check_referenced_secrets
|
||||||
if [ "${#K8S_MANIFESTS[@]}" -gt 0 ]; then
|
|
||||||
while IFS= read -r s; do
|
|
||||||
[ -n "$s" ] && ref_secrets+=("$s")
|
|
||||||
done < <(
|
|
||||||
{
|
|
||||||
grep -h -A1 -E 'secretRef:|secretKeyRef:' "${K8S_MANIFESTS[@]}" 2>/dev/null || true
|
|
||||||
grep -h -E 'secretName:' "${K8S_MANIFESTS[@]}" 2>/dev/null || true
|
|
||||||
} | grep -E 'name:' | sed -E 's/.*name:[[:space:]]*//' | tr -d '"'"'"' "'"'" | sed -E 's/[[:space:]]*#.*//' | awk 'NF' | sort -u || true
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
all_secrets="$(kubectl get secrets -A --no-headers -o custom-columns=:metadata.name 2>/dev/null || true)"
|
|
||||||
for s in ${ref_secrets[@]+"${ref_secrets[@]}"}; do
|
|
||||||
if printf '%s\n' "$all_secrets" | grep -qx "$s"; then
|
|
||||||
echo " ok: $s"
|
|
||||||
else
|
|
||||||
echo " MISSING: $s"
|
|
||||||
missing_secrets+=("$s")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "${#missing_secrets[@]}" -gt 0 ]; then
|
|
||||||
echo "ERROR: ${#missing_secrets[@]} referenced Secret(s) not found in the cluster:"
|
|
||||||
printf ' - %s\n' "${missing_secrets[@]}"
|
|
||||||
echo "Create them manually from the laptop, e.g.:"
|
|
||||||
echo " kubectl apply -f SERVICE/k8s/secrets.yaml # see SERVICE/k8s/secrets.yaml.example"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stage_apply_k8s() {
|
stage_apply_k8s() {
|
||||||
|
check_prune_mode || return 1
|
||||||
cd "$REPO"
|
cd "$REPO"
|
||||||
select_manifests >/dev/null
|
select_manifests >/dev/null
|
||||||
local ns_files=() other_files=() m k prune_opts=()
|
local ns_files=() other_files=() m k
|
||||||
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
|
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
|
||||||
case "$m" in
|
case "$m" in
|
||||||
*/namespace.y?ml) ns_files+=("$m") ;;
|
*/namespace.y?ml) ns_files+=("$m") ;;
|
||||||
*) other_files+=("$m") ;;
|
*) other_files+=("$m") ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
if [ "$APPLY_PRUNE" = "true" ]; then
|
|
||||||
prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Record what is about to change, and publish it for the verify job, before
|
# Record what is about to change, and publish it for the verify job, before
|
||||||
# the first apply. Both are fatal on failure: see snapshot_dir.
|
# the first apply. Both are fatal on failure: see snapshot_dir.
|
||||||
@@ -790,7 +798,7 @@ stage_apply_k8s() {
|
|||||||
if [ "${#other_files[@]}" -gt 0 ]; then
|
if [ "${#other_files[@]}" -gt 0 ]; then
|
||||||
log "Applying resources (${#other_files[@]} files, our images pinned to digests)"
|
log "Applying resources (${#other_files[@]} files, our images pinned to digests)"
|
||||||
for m in "${other_files[@]}"; do
|
for m in "${other_files[@]}"; do
|
||||||
if ! render_pinned <"$m" | kubectl apply "${prune_opts[@]}" -f -; then
|
if ! render_pinned <"$m" | kubectl apply -f -; then
|
||||||
echo "ERROR: apply failed for ${m#"$REPO"/}" >&2
|
echo "ERROR: apply failed for ${m#"$REPO"/}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ on:
|
|||||||
# workflow_dispatch so a red lint/validate run can never reach the cluster.
|
# workflow_dispatch so a red lint/validate run can never reach the cluster.
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: [ci]
|
workflows: [ci]
|
||||||
|
branches: [main]
|
||||||
types: [completed]
|
types: [completed]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ install_shellcheck() {
|
|||||||
rm -rf "$tmp"
|
rm -rf "$tmp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_jq() {
|
||||||
|
if at_version jq "${JQ_VERSION}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fetch "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-${goarch}" \
|
||||||
|
"$BIN_DIR/jq"
|
||||||
|
chmod 0755 "$BIN_DIR/jq"
|
||||||
|
}
|
||||||
|
|
||||||
install_uv() {
|
install_uv() {
|
||||||
if at_version uv "${UV_VERSION}"; then
|
if at_version uv "${UV_VERSION}"; then
|
||||||
return 0
|
return 0
|
||||||
@@ -236,6 +245,7 @@ for tool in "${wanted[@]}"; do
|
|||||||
case "$tool" in
|
case "$tool" in
|
||||||
kubeconform) install_kubeconform ;;
|
kubeconform) install_kubeconform ;;
|
||||||
shellcheck) install_shellcheck ;;
|
shellcheck) install_shellcheck ;;
|
||||||
|
jq) install_jq ;;
|
||||||
actionlint) install_actionlint ;;
|
actionlint) install_actionlint ;;
|
||||||
prettier) install_prettier ;;
|
prettier) install_prettier ;;
|
||||||
ruff) install_ruff ;;
|
ruff) install_ruff ;;
|
||||||
|
|||||||
@@ -2,9 +2,23 @@ name: renovate-ci
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "renovate/**"
|
||||||
|
- ".gitea/workflows/renovate-ci.yaml"
|
||||||
|
- ".gitea/workflows/sync-renovate-configmap.sh"
|
||||||
|
- ".gitea/workflows/compose-lint.sh"
|
||||||
|
- ".gitea/workflows/install-ci-tools.sh"
|
||||||
|
- ".gitea/workflows/tool-versions.env"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- "renovate/**"
|
||||||
|
- ".gitea/workflows/renovate-ci.yaml"
|
||||||
|
- ".gitea/workflows/sync-renovate-configmap.sh"
|
||||||
|
- ".gitea/workflows/compose-lint.sh"
|
||||||
|
- ".gitea/workflows/install-ci-tools.sh"
|
||||||
|
- ".gitea/workflows/tool-versions.env"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# kubectl emits a List for files containing multiple resources.
|
||||||
|
(if .kind == "List" then .items[] else . end)
|
||||||
|
| (.metadata.namespace // "default") as $ns
|
||||||
|
| [
|
||||||
|
(.. | objects
|
||||||
|
| (.secretRef? // empty), (.secretKeyRef? // empty), (.secret? // empty)
|
||||||
|
| select(.optional != true)
|
||||||
|
| .name // .secretName // empty),
|
||||||
|
(.. | objects | .imagePullSecrets[]?.name)
|
||||||
|
]
|
||||||
|
| unique[]
|
||||||
|
| select(. != null and . != "")
|
||||||
|
| "\($ns) \(.)"
|
||||||
@@ -31,3 +31,6 @@ UV_VERSION="0.12.17"
|
|||||||
# so the tree that gets tested is the tree that gets built. Renovate keeps this
|
# so the tree that gets tested is the tree that gets built. Renovate keeps this
|
||||||
# in step with the Dockerfile's node: tag via the "node runtime" group.
|
# in step with the Dockerfile's node: tag via the "node runtime" group.
|
||||||
NODE_VERSION="22.23.3"
|
NODE_VERSION="22.23.3"
|
||||||
|
|
||||||
|
# Secret-reference regression tests parse rendered Kubernetes objects.
|
||||||
|
JQ_VERSION="1.8.1"
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# Homelab
|
|
||||||
|
|
||||||
Configuration for my homelab: Kubernetes manifests, Docker Compose stacks, and the
|
|
||||||
Gitea Actions that build and deploy them. Most applications have both deployment
|
|
||||||
formats. Headscale, Nextcloud AIO, and the media stack run on Docker; Kubernetes
|
|
||||||
provides their ingress through Services and EndpointSlices.
|
|
||||||
|
|
||||||
These files contain this lab's domains, IP addresses, storage paths, and private
|
|
||||||
registry names. Running them on another machine takes some editing.
|
|
||||||
|
|
||||||
## Start here
|
|
||||||
|
|
||||||
- [Service list](#services) — what each directory contains.
|
|
||||||
- [Deployment workflow](.gitea/README.md) — selection, validation, and recovery.
|
|
||||||
- [Repository review](docs/repository-review.md) — confirmed problems and fix branches.
|
|
||||||
- [Shared PostgreSQL](postgres/README.md), [Traefik](traefik/README.md), and
|
|
||||||
[cert-manager](cert-manager/README.md) — common dependencies.
|
|
||||||
|
|
||||||
## What gets deployed
|
|
||||||
|
|
||||||
The `active` files are switches for the deploy workflow, not health indicators.
|
|
||||||
|
|
||||||
| File | Effect |
|
|
||||||
| ---------------------- | ----------------------------------------------------------- |
|
|
||||||
| `<service>/active` | Include that directory's `compose.yaml` or `compose.yml`. |
|
|
||||||
| `<service>/k8s/active` | Include its Kubernetes manifests or Kustomize overlay. |
|
|
||||||
| Both | Run the Compose stack and apply the Kubernetes resources. |
|
|
||||||
| Neither | Keep the configuration in Git without automatic deployment. |
|
|
||||||
|
|
||||||
`shared-compose.yaml`, `client.compose.yaml`, and `renovate-compose.yaml` are
|
|
||||||
manual entry points. The deploy script does not discover them.
|
|
||||||
|
|
||||||
Kubernetes selection excludes secret files, examples, Helm values, and patches.
|
|
||||||
Helm releases listed in `deploy-lib.sh` are upgraded separately. Traefik,
|
|
||||||
cert-manager, and CrowdSec have additional bootstrap steps; an `active` marker
|
|
||||||
does not install their charts.
|
|
||||||
|
|
||||||
The table below describes committed configuration. It does not claim that a
|
|
||||||
service is currently healthy or running.
|
|
||||||
|
|
||||||
## Services
|
|
||||||
|
|
||||||
| Service | Configuration | Selected by markers |
|
|
||||||
| ----------------------------------------------------------- | ---------------------------- | ------------------- |
|
|
||||||
| [AdGuard Home](adguardhome/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Authentik](authentik/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [cert-manager](cert-manager/README.md) | Kubernetes / Helm | Manual |
|
|
||||||
| [Cloudflare DDNS](cfddns/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Checkmk](checkmk/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Cloudflare Tunnel](cloudflared/README.md) | Kubernetes / Helm | Manual |
|
|
||||||
| [File converters](converters/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [CrowdSec](crowdsec/README.md) | Kubernetes / Helm | Manual |
|
|
||||||
| [Dockmon](dockmon/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Downtify](downtify/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [EDU session keeper and Telegram bot](edu_master/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Error pages](errorpages/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Gitea](gitea/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Glance](glance/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Headscale](headscale/README.md) | Compose + Kubernetes routing | Compose, Kubernetes |
|
|
||||||
| [Homarr](homarr/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Homepages](homepages/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Immich](immich/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Kener](kener/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Loki and Alloy](loki/README.md) | Kubernetes / Helm | Kubernetes |
|
|
||||||
| [MeTube](metube/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [n8n](n8n/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [NetBird](netbird/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [NetBox](netbox/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Netronome](netronome/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Nextcloud AIO](nextcloud/README.md) | Compose + Kubernetes routing | Compose, Kubernetes |
|
|
||||||
| [Penpot](penpot/README.md) | Compose | Manual |
|
|
||||||
| [Portainer](portainer/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Shared PostgreSQL](postgres/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Monitoring stack](prometheus-stack/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [RackPeek](rackpeek/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Reloader](reloader/README.md) | Kubernetes / Helm | Kubernetes |
|
|
||||||
| [Renovate](renovate/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [SearXNG](searxng/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Media stack](streaming/README.md) | Compose + Kubernetes routing | Compose, Kubernetes |
|
|
||||||
| [Termix](termix/README.md) | Kubernetes + Compose | Manual |
|
|
||||||
| [Traefik](traefik/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Uptime Kuma](uptime-kuma/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [Vaultwarden](vaultwarden/README.md) | Kubernetes + Compose | Kubernetes |
|
|
||||||
| [3x-ui](vpn/xui/README.md) | Kubernetes | Kubernetes |
|
|
||||||
|
|
||||||
## Running a Compose stack
|
|
||||||
|
|
||||||
Use the service README first. Where a service has an env example, copy it inside
|
|
||||||
that service's directory and replace the placeholders. The root `.env.example`
|
|
||||||
is an older collection of variables, not a complete configuration for every stack.
|
|
||||||
|
|
||||||
For example, from the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd netbox
|
|
||||||
cp .env.example .env
|
|
||||||
$EDITOR .env
|
|
||||||
docker compose config --quiet
|
|
||||||
docker compose up -d
|
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
Stacks that attach to `proxy` require an existing Docker network of that name and
|
|
||||||
an appropriate reverse proxy. Published host ports still work independently of
|
|
||||||
Traefik. Check port conflicts before starting an alternative to a Kubernetes
|
|
||||||
service: DNS, STUN, and HTTP listeners can share the same host.
|
|
||||||
|
|
||||||
`docker compose down` keeps named volumes. Adding `-v` removes them.
|
|
||||||
|
|
||||||
## Preparing Kubernetes
|
|
||||||
|
|
||||||
The manifests assume Traefik CRDs, cert-manager, and a working storage provisioner.
|
|
||||||
PrometheusRule and ServiceMonitor resources also need the Prometheus Operator.
|
|
||||||
Replace the lab's hosts and addresses before using the configuration elsewhere.
|
|
||||||
|
|
||||||
Create a service's namespace, then prepare its ignored Secret from the example.
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl apply -f netbox/k8s/namespace.yaml
|
|
||||||
cp netbox/k8s/secrets.yaml.example netbox/k8s/secrets.yaml
|
|
||||||
$EDITOR netbox/k8s/secrets.yaml
|
|
||||||
kubectl apply -f netbox/k8s/secrets.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
The deploy workflow applies the tracked resources for marked services. Avoid
|
|
||||||
applying an entire `k8s/` directory blindly: some directories contain Helm values,
|
|
||||||
examples, and alternative routes. For a manual change, apply the selected manifest
|
|
||||||
explicitly and check the resulting rollout.
|
|
||||||
|
|
||||||
Shared database passwords must agree between the `database` namespace and each
|
|
||||||
application's Secret. Updating the PostgreSQL Secret does not change an existing
|
|
||||||
role's password; see the database README.
|
|
||||||
|
|
||||||
## Local checks
|
|
||||||
|
|
||||||
CI pins its tools in `.gitea/workflows/tool-versions.env`. Use the same versions:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tools_dir="$(bash .gitea/workflows/install-ci-tools.sh)"
|
|
||||||
export PATH="$tools_dir:$PATH"
|
|
||||||
ruff check .
|
|
||||||
ruff format --check .
|
|
||||||
actionlint -config-file .gitea/actionlint.yaml .gitea/workflows/*.yaml
|
|
||||||
.gitea/workflows/sync-renovate-configmap.sh --check
|
|
||||||
```
|
|
||||||
|
|
||||||
The [workflow README](.gitea/README.md#checks) lists the rest of the checks.
|
|
||||||
Structure checks do not establish that local Secrets, mounted files, storage,
|
|
||||||
or external services are ready.
|
|
||||||
|
|
||||||
## Data and recovery
|
|
||||||
|
|
||||||
State lives outside Git: PVCs, Docker volumes, bind mounts, databases, and ignored
|
|
||||||
configuration. Keep backups of application data and the keys needed to read it.
|
|
||||||
An image rollback does not roll back database migrations or ConfigMap contents.
|
|
||||||
|
|
||||||
Many PVCs use the cluster's default StorageClass; monitoring explicitly uses
|
|
||||||
`local-path`. Check the PV reclaim policy before deleting a PVC or namespace.
|
|
||||||
The manifests do not provide a repository-wide backup schedule.
|
|
||||||
|
|
||||||
`incident-archive/` contains past incident notes. `.docs/storage-audit-instruction.md`
|
|
||||||
is a planning document, not evidence that NFS has been installed.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# AdGuard Home
|
|
||||||
|
|
||||||
DNS filtering with a web UI, DNS-over-TLS, and certificates from cert-manager.
|
|
||||||
|
|
||||||
The Kubernetes namespace is `adguard`. The workload uses `adguard-pvc` for
|
|
||||||
configuration and working data, and mounts the `adguard-certs` TLS Secret.
|
|
||||||
The LoadBalancer Service exposes DNS separately from the web ingress.
|
|
||||||
|
|
||||||
The Compose stack publishes TCP/UDP 53 and TCP 853 on the host. Prepare `conf/`
|
|
||||||
and `certs/` before starting it. Starting both DNS deployments on the same address
|
|
||||||
can cause a port conflict.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n adguard
|
|
||||||
kubectl get events -n adguard --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -51,6 +51,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: adguard-deployment
|
name: adguard-deployment
|
||||||
namespace: adguard
|
namespace: adguard
|
||||||
spec:
|
spec:
|
||||||
@@ -64,8 +66,6 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: adguard
|
app: adguard
|
||||||
annotations:
|
|
||||||
reloader.stakater.com/auto: "true"
|
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: adguard
|
- name: adguard
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Authentik
|
|
||||||
|
|
||||||
Identity provider with separate server and worker deployments.
|
|
||||||
|
|
||||||
Kubernetes connects to the shared PostgreSQL service in `database`. Set
|
|
||||||
`AUTHENTIK_DB_PASSWORD` to the same value in both database and application Secrets.
|
|
||||||
Keep `AUTHENTIK_SECRET_KEY` with the backups.
|
|
||||||
|
|
||||||
Compose uses its own PostgreSQL 15 container and bind-mounted media and templates.
|
|
||||||
Its image defaults differ from Kubernetes; check both before an upgrade.
|
|
||||||
The worker mounts the Docker socket for Docker outpost management.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n authentik
|
|
||||||
kubectl get events -n authentik --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -27,6 +27,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: authentik-server-deployment
|
name: authentik-server-deployment
|
||||||
namespace: authentik
|
namespace: authentik
|
||||||
spec:
|
spec:
|
||||||
@@ -63,6 +65,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: authentik-worker-deployment
|
name: authentik-worker-deployment
|
||||||
namespace: authentik
|
namespace: authentik
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# cert-manager
|
|
||||||
|
|
||||||
Public ACME issuers and an internal certificate authority.
|
|
||||||
|
|
||||||
This directory contains chart values and issuer resources, not the controller
|
|
||||||
installation. Install the cert-manager chart with CRDs and the settings in
|
|
||||||
`k8s/cert-manager-values.yaml` before applying the issuers.
|
|
||||||
|
|
||||||
`clusterissuer.yaml` defines staging and production Let's Encrypt issuers.
|
|
||||||
They use HTTP-01 through the Traefik ingress class. Public DNS and inbound HTTP
|
|
||||||
reachability must work for the requested names before issuance.
|
|
||||||
`internal-ca.yaml` bootstraps the internal CA. Keep its private-key Secret backed
|
|
||||||
up; the tracked `.crt` is only a public certificate.
|
|
||||||
|
|
||||||
This directory has no `k8s/active` marker. Apply the issuer files deliberately;
|
|
||||||
`kubectl apply` does not interpret the Helm values file.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Cloudflare DDNS
|
|
||||||
|
|
||||||
Updates the lab DNS records when the public address changes.
|
|
||||||
|
|
||||||
Kubernetes runs in `default` with host networking and reads `cfddns-secrets`.
|
|
||||||
The Compose stack also uses host networking. Configure the API token and domain
|
|
||||||
list from the relevant example; keep DNS names consistent with the ingress rules.
|
|
||||||
|
|
||||||
`config.json.example` is a separate configuration example. The current Compose
|
|
||||||
file does not mount a config.json file. Check configuration against the pinned
|
|
||||||
DDNS image when changing between environment and file-based settings.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n default
|
|
||||||
kubectl get events -n default --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: cfddns
|
name: cfddns
|
||||||
labels:
|
labels:
|
||||||
app: cfddns
|
app: cfddns
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# Checkmk
|
|
||||||
|
|
||||||
Checkmk Raw monitoring site with web and agent-receiver ingress.
|
|
||||||
|
|
||||||
The site data lives in `checkmk-sites-pvc` on Kubernetes and the `sites` named
|
|
||||||
volume on Compose. The agent receiver has a separate TCP route; enabling the
|
|
||||||
web route alone does not expose it.
|
|
||||||
|
|
||||||
Prepare the password in the service env or Secret example. Inspect the Checkmk
|
|
||||||
container logs during the first site creation.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n checkmk
|
|
||||||
kubectl get events -n checkmk --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -17,6 +17,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: checkmk-deployment
|
name: checkmk-deployment
|
||||||
namespace: checkmk
|
namespace: checkmk
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# Cloudflare Tunnel
|
|
||||||
|
|
||||||
A Kubernetes connector for an existing Cloudflare tunnel.
|
|
||||||
|
|
||||||
The Deployment runs in `default` and reads its token from the ignored Secret
|
|
||||||
created from `k8s/secret.yaml.example`. Create the tunnel and its hostname rules
|
|
||||||
in Cloudflare before starting the connector.
|
|
||||||
|
|
||||||
There is no Compose file or `k8s/active` marker. Apply the Secret first, then
|
|
||||||
`k8s/deployment.yaml` when this tunnel is needed.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n default
|
|
||||||
kubectl get events -n default --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: cloudflared
|
name: cloudflared
|
||||||
labels:
|
labels:
|
||||||
app: cloudflared
|
app: cloudflared
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# File converters
|
|
||||||
|
|
||||||
ConvertX for server-side conversion and BentoPDF for PDF tools.
|
|
||||||
|
|
||||||
ConvertX persists files in `convertx-pvc`; BentoPDF has no persistent volume.
|
|
||||||
Kubernetes configuration includes a local `config.yaml.example`, excluded from
|
|
||||||
normal deployment. Copy and apply the real ConfigMap separately where required.
|
|
||||||
|
|
||||||
Compose publishes ConvertX on host port 9992 as well as attaching it to the
|
|
||||||
proxy network. Replace the authentication settings from `.env.example` before
|
|
||||||
exposing it outside the lab.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n converters
|
|
||||||
kubectl get events -n converters --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: convertx-deployment
|
name: convertx-deployment
|
||||||
namespace: converters
|
namespace: converters
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# CrowdSec
|
|
||||||
|
|
||||||
Helm values, dashboards, network policy, and a maintenance CronJob.
|
|
||||||
|
|
||||||
Install CrowdSec separately using `k8s/crowdsec-values.yaml`; the deploy
|
|
||||||
workflow does not have a CrowdSec Helm release entry. There is no `k8s/active`
|
|
||||||
marker in this directory.
|
|
||||||
|
|
||||||
The LAPI policy and janitor run in `crowdsec`. The dashboard ConfigMaps are in
|
|
||||||
`prometheus` for Grafana's sidecar. The janitor has its own ServiceAccount and
|
|
||||||
namespace Role. Review its script and schedule before enabling cleanup.
|
|
||||||
|
|
||||||
Traefik's values state that enforcement moved to a host firewall bouncer. This
|
|
||||||
repository does not install that host component.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n crowdsec
|
|
||||||
kubectl get events -n crowdsec --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Dockmon
|
|
||||||
|
|
||||||
Docker management UI that talks to the host Docker daemon.
|
|
||||||
|
|
||||||
Both runtimes mount `/var/run/docker.sock`. On Kubernetes the socket belongs
|
|
||||||
to the node hosting the pod, so this is not a cluster-wide container manager.
|
|
||||||
|
|
||||||
Compose stores application data in a named volume. Kubernetes uses a StatefulSet
|
|
||||||
with a volume claim template. Its ServersTransport is specific to the upstream
|
|
||||||
connection; keep it with the ingress resources.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n dockmon
|
|
||||||
kubectl get events -n dockmon --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# Repository review
|
|
||||||
|
|
||||||
Reviewed the tracked tree at `cc9c3de` and read the live workstation state on
|
|
||||||
6 October 2026. Changes are split into documentation and individual fix branches,
|
|
||||||
all based on that main commit. The original local checkout and its uncommitted
|
|
||||||
monitoring changes were preserved. No deployment was performed.
|
|
||||||
|
|
||||||
## Confirmed problems with prepared fixes
|
|
||||||
|
|
||||||
| Priority | Problem and consequence | Fix branch |
|
|
||||||
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
||||||
| High | `APPLY_PRUNE=true` is passed to each individual manifest apply. Each invocation sees only that file's desired objects and can delete other resources selected by the shared label. | `fix/deploy-prune-guard` |
|
|
||||||
| High | Deploy validates Compose with interpolation and env/path resolution disabled. Required settings can pass validation and then fail during apply after other workloads have changed. | `fix/deploy-validation` |
|
|
||||||
| Medium | Secret validation is text-based and compares names across all namespaces. A Secret elsewhere can hide a missing local Secret; mounted Secrets are also missed. | `fix/deploy-validation` |
|
|
||||||
| Medium | Compose CI misses `postgres/shared-compose.yaml`, `netbird/client.compose.yaml`, and `renovate/renovate-compose.yaml`. | `fix/deploy-validation` |
|
|
||||||
| Medium | NetBird Compose mounts `entrypoint.sh`, but it is absent. Its README also calls a missing `setup.sh`; a fresh checkout cannot start this stack as documented. | `fix/netbird-compose-runtime` |
|
|
||||||
| Medium | Glance's CSS mount uses `glance-config`, whose keys do not include `user.css`. That key is in `glance-assets`; the pod's subPath mount cannot be prepared correctly. | `fix/glance-assets` |
|
|
||||||
| Medium | The shared PostgreSQL initializer requires `NETBOX_DB_PASSWORD`, but the Compose env example omits it. Following the example leaves first initialization incomplete. | `fix/postgres-env-example` |
|
|
||||||
| Medium | EDU's Compose env example uses old credential names and full URL variables, while the code reads `KEEPER_*` and paths under `EDU_URL_BASE`. | `fix/session-keeper-reliability` |
|
|
||||||
| Medium | Session keeper HTTP calls have no timeouts. Its Redis cookie never expires, probes only check existence, and its logs include cookies. A hung or failed refresh can leave a stale session appearing ready. | `fix/session-keeper-reliability` |
|
|
||||||
| Medium | AdGuard's DoH and SearXNG's Compose rules put Boolean expressions inside `Host(...)`. They are invalid router expressions despite valid YAML. | `fix/compose-router-rules` |
|
|
||||||
|
|
||||||
Traefik matchers should be combined as `Host(a) || Host(b)`; the rule syntax is
|
|
||||||
described in the [Traefik rules documentation](https://doc.traefik.io/traefik/reference/routing-configuration/http/routing/rules-and-priority/).
|
|
||||||
The fix retains the DoH path constraint for both hostnames.
|
|
||||||
|
|
||||||
The prune fix deliberately rejects the unsafe option. It does not introduce
|
|
||||||
automatic deletion under a different implementation. Prune defaults to false,
|
|
||||||
and no tracked resource currently carries the selector label, so this is a
|
|
||||||
latent defect rather than evidence of a live deletion incident.
|
|
||||||
|
|
||||||
The session fix bounds HTTP and Redis calls, validates required credentials,
|
|
||||||
sets a cookie lifetime of two refresh intervals, and marks success only after
|
|
||||||
publishing the verified cookie. With the default ten-minute interval, an outage
|
|
||||||
longer than twenty minutes will make the existing Redis-key readiness checks fail.
|
|
||||||
That is an intentional change from indefinite apparent readiness.
|
|
||||||
|
|
||||||
The deployment fix extracts required pod Secret references from rendered JSON,
|
|
||||||
checks their namespaces, includes init containers, image-pull credentials, and
|
|
||||||
mounted/projected Secrets, and honors optional references. Ingress TLS Secrets
|
|
||||||
issued by cert-manager are not treated as pre-existing pod prerequisites.
|
|
||||||
It checks existence/access, not every key's contents or application validity.
|
|
||||||
|
|
||||||
## Live workstation observations
|
|
||||||
|
|
||||||
The SSH alias `workstation` is reachable. It has one Ready control-plane node,
|
|
||||||
Kubernetes `v1.35.4+k0s`, and a Docker daemon alongside containerd. At inspection,
|
|
||||||
no pods were Pending or in another non-running, non-completed phase. This is a
|
|
||||||
point-in-time observation, not a complete application health test.
|
|
||||||
|
|
||||||
The deployment checkout at `/srv/homelab` is on main commit `2adf17c`, behind the
|
|
||||||
reviewed local commit. It has untracked host configuration and a separate
|
|
||||||
`userbot/` directory. It was not reset or cleaned.
|
|
||||||
|
|
||||||
| Observed difference | Implication |
|
|
||||||
| ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| VictoriaMetrics and vmalert are running; the Prometheus StatefulSet has zero replicas. | A monitoring migration is already in progress outside committed main. Deploying the old Helm values can overwrite those settings. |
|
|
||||||
| Homarr, Cloudflared, and Reloader are installed without their current Git active markers. | Installed services and marker-selected services are different inventories. Missing markers do not establish that a service is stopped. |
|
|
||||||
| Cloudflare DDNS is running in both Docker and Kubernetes. | Confirm which instance should own DNS updates and whether their domain lists overlap before retiring either one. Secret values were not inspected. |
|
|
||||||
| Traefik's LoadBalancer exposes port 8080 at `192.168.80.2`. | The direct API listener is deployed; its external reachability was not tested. |
|
|
||||||
| Default `local-path` has reclaim policy Delete, while many existing PVs have been changed to Retain. | Current retention is partly live state. Recreating a claim can get a different policy from the old PV. |
|
|
||||||
| NetBird, NetBox media/reports/scripts, EDU Redis, Homarr, and VictoriaMetrics have Delete-policy PVs. | Deleting their claims can delete important state. Plan backup and retention changes before namespace cleanup. |
|
|
||||||
|
|
||||||
The monitoring files already modified in the user's local tree correspond to the
|
|
||||||
live migration. They are excluded from these branches. Reconcile that work before
|
|
||||||
using this review's baseline to deploy monitoring.
|
|
||||||
|
|
||||||
## Remaining work
|
|
||||||
|
|
||||||
These need recovery design or infrastructure decisions rather than a small
|
|
||||||
configuration correction:
|
|
||||||
|
|
||||||
- **SSH apply retries can replace the rollback baseline.** `ssh-run.sh` retries
|
|
||||||
exit 255, including `apply-k8s`; every new invocation publishes a fresh snapshot.
|
|
||||||
If the first attempt already changed workloads, the retry snapshots that partial
|
|
||||||
state. Preserve a run-specific original baseline and verify it across retries.
|
|
||||||
- **Rollback can exceed the job budget.** Verification is parallel, but
|
|
||||||
`rollback_workloads` is serial with a five-minute limit per workload. The
|
|
||||||
thirty-minute job budget can expire before recovery finishes. Bound recovery
|
|
||||||
concurrency and account for both phases before choosing a new timeout.
|
|
||||||
- **Snapshot collection is allowed to fail.** Generation and workload snapshot
|
|
||||||
errors are warnings; verify can fall back to all workloads. A snapshot failure
|
|
||||||
must not permit unrelated workloads to be selected for automatic undo.
|
|
||||||
- **Rollback uses the previous revision, not the captured revision.** `rollout undo`
|
|
||||||
without an explicit revision cannot guarantee restoration to the snapshot after
|
|
||||||
retries or intervening rollouts. First deployments also have no previous revision.
|
|
||||||
- **Manual deploy dispatch bypasses the CI-success trigger.** Either validate the
|
|
||||||
target commit's successful CI run or document manual dispatch as an operator
|
|
||||||
override with its own required checks.
|
|
||||||
- **Direct Traefik API exposure is unauthenticated.** The latest local commit
|
|
||||||
explicitly added it for Homarr. Preserve that integration while choosing a
|
|
||||||
cluster-internal authenticated path or a verified network restriction; do not
|
|
||||||
simply disable an integration that is already in use.
|
|
||||||
- **Storage retention and backup are not reproducible as a whole.** Defaults and
|
|
||||||
several important PV policies are Delete. There is no repository-wide backup
|
|
||||||
schedule. Existing PVC StorageClass changes require migration rather than an
|
|
||||||
in-place YAML edit.
|
|
||||||
- **MeTube downloads are temporary on Kubernetes.** `/downloads` is a 20 GiB
|
|
||||||
emptyDir. Decide whether pod replacement should discard files or whether it
|
|
||||||
should use persistent storage. Compose uses a host directory instead.
|
|
||||||
- **First-time activation needs a bootstrap path.** Deploy validation dry-runs
|
|
||||||
namespaced resources before the apply stage creates namespaces and installs
|
|
||||||
selected charts. On a fresh cluster, missing namespaces and CRDs need separate
|
|
||||||
preparation; activation is not a complete installer.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Baseline lint checks passed for Python, shell, workflows, YAML, standard Compose
|
|
||||||
files, and Kubernetes resources with available schemas. Kubeconform found 347
|
|
||||||
resources in 174 files: 201 valid, 146 skipped CRDs, zero invalid resources.
|
|
||||||
That skip count matters: passing schema validation does not validate Traefik rule
|
|
||||||
strings or other controller-specific behavior.
|
|
||||||
|
|
||||||
Fix validation covers:
|
|
||||||
|
|
||||||
- Compose discovery of manual entry points, rejection of required-variable gaps,
|
|
||||||
namespace-scoped and optional Secret references, and API/render failures.
|
|
||||||
- NetBird setup idempotence, preservation of existing keys, file permissions,
|
|
||||||
runtime rendering, and rejection of invalid trusted proxy CIDRs.
|
|
||||||
- Session refresh success and failure paths, timeouts, cookie expiry, log redaction,
|
|
||||||
missing credentials, and nonpositive refresh intervals.
|
|
||||||
- Correct Glance ConfigMap key selection and PostgreSQL initializer/env alignment.
|
|
||||||
- YAML and Compose structure for the corrected router rules, compared with the
|
|
||||||
documented Traefik grammar. They were not exercised on the live proxy.
|
|
||||||
- Prune rejection before any cluster invocation.
|
|
||||||
|
|
||||||
All seven fix branches and the documentation branch merged together without
|
|
||||||
conflicts in a disposable validation worktree. The combined tree passed the
|
|
||||||
CI-equivalent local checks, Markdown formatting/lint and link checks, all 35
|
|
||||||
Compose structure checks, and 11 Python regression tests plus the shell
|
|
||||||
validation regressions. CRD server-side validation and live rollout tests were
|
|
||||||
not run.
|
|
||||||
|
|
||||||
Runtime tests use fixtures and mocks, not production credentials. Live checks read
|
|
||||||
workload metadata, storage policies, chart versions, and container state only.
|
|
||||||
They did not read Secret contents or change services.
|
|
||||||
|
|
||||||
## Reloader follow-up
|
|
||||||
|
|
||||||
`fix/reloader-integration` adds the active marker and opt-in annotations to 28
|
|
||||||
application Deployments/StatefulSets that consume runtime ConfigMaps or Secrets.
|
|
||||||
It corrects AdGuard's misplaced pod-template annotation. The Helm settings use
|
|
||||||
annotation-based reloads, keep global auto-reload disabled, and ignore Jobs and
|
|
||||||
CronJobs. PostgreSQL workloads are excluded because their credential variables
|
|
||||||
and init scripts are only effective on an empty data directory.
|
|
||||||
|
|
||||||
The controller was already running on workstation when inspected. Its live
|
|
||||||
configuration is unchanged by the branch: merge and deploy the integration to
|
|
||||||
apply the new policy and application annotations. Configuration reload behavior
|
|
||||||
was checked against the pinned chart, with Helm rendering and manifest validation;
|
|
||||||
no production configuration was changed to provoke a test restart.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Downtify
|
|
||||||
|
|
||||||
Download UI with a persistent downloads directory.
|
|
||||||
|
|
||||||
Compose stores downloads under `Downtify_downloads/`; Kubernetes uses
|
|
||||||
`downtify-downloads-pvc`. The ingress manifests reference shared infrastructure,
|
|
||||||
so check certificate and middleware availability before enabling them.
|
|
||||||
|
|
||||||
Back up downloads separately if they need to survive storage replacement.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n downtify
|
|
||||||
kubectl get events -n downtify --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# EDU session keeper and Telegram bot
|
|
||||||
|
|
||||||
Keeps an EDU login session in Redis and sends Telegram notifications for new webinars. The bot also serves diary and schedule commands.
|
|
||||||
|
|
||||||
`phpsessid-bot/` logs into EDU and publishes `EDU_PHPSESSID` in Redis.
|
|
||||||
`webinar-checker/` uses that cookie through a remote Playwright browser and stores
|
|
||||||
subscribers, language preferences, and webinar history in Redis.
|
|
||||||
|
|
||||||
Kubernetes runs in `edu-master`, with Redis data in `redis-data-pvc`.
|
|
||||||
`service.yaml`, `servicemonitor.yaml`, and `alerts.yaml` expose and monitor the
|
|
||||||
checker's metrics on port 8000. Its `/health` endpoint reflects recent checks.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Use the keys in `k8s/secrets.yaml.example` as the reference. The committed Compose
|
|
||||||
`.env.example` has stale names until `fix/session-keeper-reliability` is merged.
|
|
||||||
The code reads:
|
|
||||||
|
|
||||||
| Variable | Purpose |
|
|
||||||
| ----------------------------------------------------- | ------------------------------------------------ |
|
|
||||||
| `KEEPER_LOGIN`, `KEEPER_PASSWORD` | EDU login credentials. |
|
|
||||||
| `KEEPER_INTERVAL` | Session refresh interval in minutes; default 10. |
|
|
||||||
| `EDU_URL_BASE` | EDU site origin. |
|
|
||||||
| `EDU_URL_LOGIN`, `EDU_URL_COURSES`, `EDU_URL_WEBINAR` | Paths under that origin. |
|
|
||||||
| `WEBINAR_TELEGRAM_TOKEN`, `WEBINAR_ADMIN_ID` | Telegram bot and administrator. |
|
|
||||||
| `WEBINAR_CHECK_INTERVAL` | Checker interval in seconds; default 60. |
|
|
||||||
| `REDIS_HOST`, `REDIS_PORT` | Redis connection. |
|
|
||||||
| `PLAYWRIGHT_WS` | Remote browser WebSocket endpoint. |
|
|
||||||
|
|
||||||
Set the keeper keys explicitly in the Compose `.env`. Keep the Playwright Python
|
|
||||||
package, browser image, server command, and `PLAYWRIGHT_VERSION` file on matching
|
|
||||||
versions. The two Python images are built and published by CI.
|
|
||||||
|
|
||||||
## Bot use
|
|
||||||
|
|
||||||
Start a private chat with `/start` to subscribe. `/stop`, `/language`, `/diary`,
|
|
||||||
`/schedule`, and `/setclass` manage subscriptions and school views. The
|
|
||||||
administrator can manage the whitelist with `/adduser` and `/removeuser`.
|
|
||||||
|
|
||||||
Back up Redis if subscriber settings and notification history matter. Session
|
|
||||||
cookies and Telegram tokens are credentials; keep them out of shared logs.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n edu-master
|
|
||||||
kubectl get events -n edu-master --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: session-keeper
|
name: session-keeper
|
||||||
namespace: edu-master
|
namespace: edu-master
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: webinar-checker
|
name: webinar-checker
|
||||||
namespace: edu-master
|
namespace: edu-master
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# Error pages
|
|
||||||
|
|
||||||
Static HTTP error pages served by an Nginx image built in CI.
|
|
||||||
|
|
||||||
Edit the HTML in `html/`; the Dockerfile copies it into the image.
|
|
||||||
Kubernetes exposes `error-pages-service` in `error-pages` for Traefik's error
|
|
||||||
middleware. Keep the middleware's namespace and port aligned with that Service.
|
|
||||||
|
|
||||||
For a local build, run `docker build -t homelab-error-pages .` from this directory.
|
|
||||||
Compose references the private registry image rather than a build context.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n error-pages
|
|
||||||
kubectl get events -n error-pages --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Gitea
|
|
||||||
|
|
||||||
Git hosting with HTTP and a separate SSH route.
|
|
||||||
|
|
||||||
Kubernetes uses the shared PostgreSQL service and `gitea-pvc` for repositories
|
|
||||||
and application data. Match the Gitea database password with the shared database
|
|
||||||
Secret. SSH is routed through Traefik's TCP entrypoint on 2221.
|
|
||||||
|
|
||||||
Compose uses a separate PostgreSQL 14 database, bind mounts `gitea-data/` and
|
|
||||||
`gitea-db/`, and publishes host port 2221. It is an alternative deployment with
|
|
||||||
its own database, not a second frontend for the Kubernetes instance.
|
|
||||||
|
|
||||||
Back up repositories, application configuration, and a consistent database dump
|
|
||||||
together. Gitea Actions definitions for this repository live in `../.gitea/`.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n gitea
|
|
||||||
kubectl get events -n gitea --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -17,6 +17,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: gitea-deployment
|
name: gitea-deployment
|
||||||
namespace: gitea
|
namespace: gitea
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# Glance
|
|
||||||
|
|
||||||
Dashboard pages for links, service checks, and Docker containers.
|
|
||||||
|
|
||||||
Compose mounts `config/` and `assets/`. The Kubernetes equivalents are embedded
|
|
||||||
in `k8s/glance-config.yaml`: `glance-config` holds pages and `glance-assets` holds
|
|
||||||
`user.css`. Update both copies when changing shared content.
|
|
||||||
|
|
||||||
Kubernetes serves the dashboard under `/glance`. Its pod also mounts the node's
|
|
||||||
Docker socket. It references `glance-secrets` for `ADGUARD_PASSWORD`, but there is
|
|
||||||
no tracked Secret example; create that Secret in `glance` before starting it.
|
|
||||||
Compose expects a local `.env` with the same password.
|
|
||||||
|
|
||||||
The CSS mount points at the wrong ConfigMap on the reviewed main commit;
|
|
||||||
`fix/glance-assets` corrects it.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n glance
|
|
||||||
kubectl get events -n glance --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: glance-deployment
|
name: glance-deployment
|
||||||
namespace: glance
|
namespace: glance
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Headscale
|
|
||||||
|
|
||||||
Headscale, Headplane, and a separate web administration UI on Docker.
|
|
||||||
|
|
||||||
Kubernetes only provides routes to the Docker host. Update the addresses in
|
|
||||||
`k8s/routing/external-service.yaml` if the host moves.
|
|
||||||
|
|
||||||
Copy `config/headscale.yaml.example`, `config/headplane.yaml.example`, and
|
|
||||||
`config/policy.json.example` to their names without `.example`. Set the public
|
|
||||||
server URL, DNS settings, Headplane cookie secret, and Headscale public URL.
|
|
||||||
The example URLs are placeholders.
|
|
||||||
|
|
||||||
Compose publishes Headscale on 18080, its metrics port on 19090, Headplane on
|
|
||||||
13000, and the other UI on 10080. The data volumes store the Headscale database,
|
|
||||||
keys, and Headplane state. The embedded DERP configuration needs reachable
|
|
||||||
addresses; Compose does not publish its UDP 3478 listener.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Homarr
|
|
||||||
|
|
||||||
Dashboard with Kubernetes integration and persistent application state.
|
|
||||||
|
|
||||||
Kubernetes uses the `homarr` ServiceAccount and the read-only ClusterRole in
|
|
||||||
`k8s/rbac.yaml`. Application data lives in `homarr-pvc`; supply the encryption key
|
|
||||||
from `k8s/secrets.yaml.example` before the first start and retain it with backups.
|
|
||||||
|
|
||||||
The committed ingress is internal. There is no `k8s/active` marker even though
|
|
||||||
manifests exist, so the workflow does not select Homarr automatically.
|
|
||||||
|
|
||||||
Compose publishes ports 80 and 81, mounts appdata and the Docker socket, and
|
|
||||||
expects a local kubeconfig. Check these host ports against Traefik before use.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n homarr
|
|
||||||
kubectl get events -n homarr --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: homarr-deployment
|
name: homarr-deployment
|
||||||
namespace: homarr
|
namespace: homarr
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# Homepages
|
|
||||||
|
|
||||||
Two static sites: Forust and xdfnx.
|
|
||||||
|
|
||||||
The site sources are in `forust_files/` and `xdfnx_files/`. CI builds each with
|
|
||||||
its own Dockerfile and publishes it to the private registry. Kubernetes serves
|
|
||||||
the image contents; Compose overlays the source directories as bind mounts.
|
|
||||||
|
|
||||||
Both Traefik IngressRoute and Gateway API route manifests are committed.
|
|
||||||
Keep their hostnames and backend Services aligned when changing routes.
|
|
||||||
Certificate resources cover public and internal hostnames.
|
|
||||||
|
|
||||||
Build either site locally with `docker build -f Dockerfile.forust .` or
|
|
||||||
`docker build -f Dockerfile.xdfnx .` from this directory.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n homepages
|
|
||||||
kubectl get events -n homepages --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Immich
|
|
||||||
|
|
||||||
Photo library with its own vector-enabled PostgreSQL and machine-learning service.
|
|
||||||
|
|
||||||
This database is separate from the shared PostgreSQL instance. Keep the server
|
|
||||||
and machine-learning versions aligned when upgrading.
|
|
||||||
|
|
||||||
Kubernetes bind-mounts `/mnt/immich/library` from the node. That directory must
|
|
||||||
already exist and contain the intended library; moving the pod to a different
|
|
||||||
node does not move the files. PostgreSQL and Valkey use StatefulSet storage, and
|
|
||||||
the model cache has its own PVC.
|
|
||||||
|
|
||||||
Compose reads `UPLOAD_LOCATION` and `DB_DATA_LOCATION` from `.env`. The example
|
|
||||||
uses the same library path as Kubernetes. Run one writer against that library;
|
|
||||||
do not start both deployments as independent instances over the same files.
|
|
||||||
|
|
||||||
Back up the library and a consistent database dump together. The model cache
|
|
||||||
can be rebuilt; the photo database cannot.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n immich
|
|
||||||
kubectl get events -n immich --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -14,6 +14,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: immich-deployment
|
name: immich-deployment
|
||||||
namespace: immich
|
namespace: immich
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: immich-machine-learning-deployment
|
name: immich-machine-learning-deployment
|
||||||
namespace: immich
|
namespace: immich
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: StatefulSet
|
kind: StatefulSet
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: immich-valkey
|
name: immich-valkey
|
||||||
namespace: immich
|
namespace: immich
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# Kener
|
|
||||||
|
|
||||||
Status page with Redis and persistent database and upload directories.
|
|
||||||
|
|
||||||
Kubernetes uses `kener-db-pvc`, `kener-uploads-pvc`, and a Redis StatefulSet.
|
|
||||||
Compose keeps the corresponding directories in named volumes. Set the signing
|
|
||||||
and other credentials from the env or Secret example.
|
|
||||||
|
|
||||||
The monitors and route settings live in `k8s/config.yaml` and `k8s/ingress.yaml`.
|
|
||||||
There is no active marker for either runtime.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n kener
|
|
||||||
kubectl get events -n kener --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: kener-deployment
|
name: kener-deployment
|
||||||
namespace: kener
|
namespace: kener
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# Loki and Alloy
|
|
||||||
|
|
||||||
Loki log storage and Alloy collection, both deployed through Helm.
|
|
||||||
|
|
||||||
The deploy library lists separate `loki` and `alloy` releases in `prometheus`,
|
|
||||||
controlled by this directory's `k8s/active` marker. Chart versions are pinned in
|
|
||||||
`deploy-lib.sh`; settings live in `loki-values.yaml` and `alloy-values.yaml`.
|
|
||||||
|
|
||||||
Alloy collects Kubernetes logs. Grafana's Loki datasource is configured in the
|
|
||||||
monitoring stack. Review Loki retention and storage settings before enabling
|
|
||||||
collection on a new cluster.
|
|
||||||
|
|
||||||
Check releases with `helm list -n prometheus` and inspect collector logs before
|
|
||||||
assuming that an empty Grafana query means there were no events.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# MeTube
|
|
||||||
|
|
||||||
Web downloader behind Traefik.
|
|
||||||
|
|
||||||
Compose bind-mounts `MeTube_downloads/` on the host. Kubernetes uses a 20 GiB
|
|
||||||
`emptyDir` for `/downloads`: completed downloads disappear when the pod is
|
|
||||||
replaced. Download files from the UI promptly if this temporary storage is intended.
|
|
||||||
|
|
||||||
Application settings are in `k8s/config.yaml`. Persisting downloads in Kubernetes
|
|
||||||
would require changing the volume to a PVC and choosing a storage policy.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n metube
|
|
||||||
kubectl get events -n metube --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: metube-deployment
|
name: metube-deployment
|
||||||
namespace: metube
|
namespace: metube
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# n8n
|
|
||||||
|
|
||||||
Workflow automation with persistent application and file storage.
|
|
||||||
|
|
||||||
Kubernetes keeps application state in `n8n-node-pvc` and files in
|
|
||||||
`n8n-files-pvc`; Compose uses `node-data` and `files` named volumes.
|
|
||||||
Webhook URLs and proxy settings are committed in the application config.
|
|
||||||
|
|
||||||
There is no active marker. Review the URLs before enabling the stack, and retain
|
|
||||||
the credential encryption key with the database or application-data backup.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n n8n
|
|
||||||
kubectl get events -n n8n --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: n8n-deployment
|
name: n8n-deployment
|
||||||
namespace: n8n
|
namespace: n8n
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
+102
-43
@@ -1,64 +1,123 @@
|
|||||||
# NetBird
|
# NetBird
|
||||||
|
|
||||||
Self-hosted NetBird with the combined management, signal, relay, and STUN server.
|
Self-hosted NetBird with the combined management, signal, relay, and STUN server. The dashboard and server run behind the repository's existing external Traefik instance on the Docker `proxy` network. Only STUN UDP `3478` is published directly.
|
||||||
|
|
||||||
Kubernetes runs the server and dashboard in `netbird`. The server uses SQLite
|
The deployment uses SQLite for a single-instance homelab server. The persistent `netbird_data` volume and the datastore encryption key are both required to recover the installation.
|
||||||
in `netbird-pvc`; `k8s/config.yaml` contains the template and runtime renderer.
|
|
||||||
Prepare `netbird-secrets` from `k8s/secrets.yaml.example` before the first start.
|
|
||||||
|
|
||||||
## Routing and keys
|
## Files
|
||||||
|
|
||||||
The public hostname is set in the ConfigMap and ingress rules. Keep the issuer,
|
- `compose.yaml`: dashboard and combined server; selected by the marker-driven deploy workflow through `active`.
|
||||||
dashboard endpoints, and public routes consistent. HTTP, WebSocket, and gRPC
|
- `config.template.yaml`: non-secret server configuration rendered at startup.
|
||||||
traffic go through Traefik; the STUN route uses UDP 3478. A CDN's HTTP proxy does
|
- `entrypoint.sh`: injects Docker secrets into an in-memory runtime configuration.
|
||||||
not provide that UDP listener.
|
- `client.compose.yaml`: optional host-network peer using a dashboard-generated setup key.
|
||||||
|
- `.env`: ignored local hostnames, the detected Traefik Docker-network subnet, and optional client setup key.
|
||||||
|
- `secrets/`: ignored relay secret and datastore encryption key.
|
||||||
|
|
||||||
`NETBIRD_PROXY_SUBNET` controls which forwarded client addresses are trusted.
|
## First deployment
|
||||||
Use the actual proxy network CIDR rather than assuming another lab's subnet.
|
|
||||||
Keep the datastore encryption key with every datastore backup. Regenerating it
|
|
||||||
can make stored credentials unreadable.
|
|
||||||
|
|
||||||
## Compose alternative
|
Run these commands on the Docker host before merging the activating branch. The deploy preflight resets tracked files but preserves ignored local state.
|
||||||
|
|
||||||
`compose.yaml` expects `entrypoint.sh`, a local `.env`, and two local files:
|
```bash
|
||||||
`secrets/relay-auth-secret` and `secrets/datastore-encryption-key`.
|
cd /srv/homelab/netbird
|
||||||
The reviewed main commit is missing the renderer and setup script.
|
|
||||||
`fix/netbird-compose-runtime` restores them. Merge that fix before following
|
|
||||||
these setup commands:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd netbird
|
|
||||||
./setup.sh
|
./setup.sh
|
||||||
$EDITOR .env
|
$EDITOR .env
|
||||||
docker compose config --quiet
|
docker compose config --quiet
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The setup script detects the IPv4 subnet of the external `proxy` network and
|
Review the values in `.env` before starting. The example public hostname is `netbird.forust.xyz`; change it if a different public domain was selected. `setup.sh` replaces `NETBIRD_PROXY_SUBNET=auto` with the first IPv4 subnet of the external Docker `proxy` network. Keep that value synchronized with the network; set an explicit CIDR instead if the network is managed elsewhere.
|
||||||
preserves existing secrets. Complete the initial owner setup through the public
|
|
||||||
TLS endpoint after starting the server.
|
`setup.sh` is idempotent and never replaces existing secrets. Do not delete or regenerate `secrets/datastore-encryption-key` after the first successful start unless all encrypted setup keys and API tokens are intentionally being invalidated.
|
||||||
|
|
||||||
|
## Network prerequisites
|
||||||
|
|
||||||
|
- Point the public hostname directly to the Docker host. Do not proxy UDP `3478` through Cloudflare or another CDN.
|
||||||
|
- Allow inbound TCP `80`, TCP `443`, and UDP `3478` through the host firewall and upstream router.
|
||||||
|
- Ensure the external `proxy` Docker network exists and Traefik uses its `websecure` entrypoint and `letsencrypt` resolver. `NETBIRD_PROXY_SUBNET` must describe that network; it is used to trust only forwarded client addresses from Traefik.
|
||||||
|
- Ensure the internal names in `.env` resolve where the local and development aliases are needed.
|
||||||
|
- Keep Traefik's `websecure` read timeout disabled for long-lived gRPC and WebSocket sessions. This repository configures `--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0` in `traefik/compose.yaml`.
|
||||||
|
|
||||||
|
After startup, verify OIDC discovery through the public TLS endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration"
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `https://${NETBIRD_DOMAIN}` immediately and complete the initial owner setup. Treat the initial setup flow as public until the owner exists.
|
||||||
|
|
||||||
## Optional host client
|
## Optional host client
|
||||||
|
|
||||||
`client.compose.yaml` runs a host-network peer in a separate Compose project.
|
The client intentionally lives in a separate Compose project. Normal server deploys use `--remove-orphans`, so keeping the client in the server project would cause it to be removed.
|
||||||
Set `NB_SETUP_KEY` and `NETBIRD_CLIENT_HOSTNAME` in the local `.env`, then run
|
|
||||||
`docker compose -f client.compose.yaml up -d`. It needs `/dev/net/tun` and elevated
|
|
||||||
network capabilities. The normal server deployment does not start this client.
|
|
||||||
|
|
||||||
## Backup
|
1. Create a reusable or ephemeral setup key in the NetBird dashboard.
|
||||||
|
2. Put `NB_SETUP_KEY=<key>` in the ignored `netbird/.env` file.
|
||||||
|
3. Set `NETBIRD_CLIENT_HOSTNAME` to this machine's desired peer name.
|
||||||
|
4. Start and inspect the client:
|
||||||
|
|
||||||
Back up the SQLite data while the server is stopped, together with the encryption
|
```bash
|
||||||
key, relay secret, and local configuration. Test a restore on an isolated host.
|
cd /srv/homelab/netbird
|
||||||
For Compose, the datastore volume has the explicit name `netbird_data`.
|
docker compose -f client.compose.yaml config --quiet
|
||||||
Do not use `docker compose down -v` when keeping the installation.
|
docker compose -f client.compose.yaml up -d
|
||||||
|
docker compose -f client.compose.yaml exec netbird-client netbird status
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n netbird
|
|
||||||
kubectl get events -n netbird --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
The client uses host networking and requires `NET_ADMIN`, `SYS_ADMIN`, `SYS_RESOURCE`, and `/dev/net/tun`. Remove it without affecting the server stack:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f client.compose.yaml down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
Inspect status and logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs --tail=200 netbird-server dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop or remove containers without deleting data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not add `-v` to `docker compose down`; it would delete the NetBird datastore.
|
||||||
|
|
||||||
|
## Backup and restore
|
||||||
|
|
||||||
|
Back up both the persistent volume and the ignored secret files. For a consistent SQLite backup, briefly stop the server first and store the resulting archive and `datastore-encryption-key` in an encrypted backup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/homelab/netbird
|
||||||
|
mkdir -p backups
|
||||||
|
docker compose stop netbird-server
|
||||||
|
docker run --rm \
|
||||||
|
-v netbird_data:/data:ro \
|
||||||
|
-v "$PWD/backups:/backup" \
|
||||||
|
busybox:1.37.0 \
|
||||||
|
tar -C /data -czf "/backup/netbird-data-$(date -u +%Y%m%dT%H%M%SZ).tar.gz" .
|
||||||
|
docker compose start netbird-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Also securely back up:
|
||||||
|
|
||||||
|
- `secrets/datastore-encryption-key` — required to decrypt stored secrets.
|
||||||
|
- `secrets/relay-auth-secret` — keeps issued relay credentials valid across restoration.
|
||||||
|
- `netbird/.env` — optional, but it records the public and internal hostnames.
|
||||||
|
|
||||||
|
Test a restore in an isolated Docker host before relying on a backup.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
1. Take and verify a backup.
|
||||||
|
2. Review NetBird release notes for server, client, and dashboard compatibility.
|
||||||
|
3. Update the pinned tags in `compose.yaml`; update `client.compose.yaml` separately when deploying the client.
|
||||||
|
4. Pull and recreate the selected services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The image tags are intentionally pinned instead of using `latest`, matching this repository's pull-on-deploy policy.
|
||||||
@@ -32,6 +32,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: netbird-server-deployment
|
name: netbird-server-deployment
|
||||||
namespace: netbird
|
namespace: netbird
|
||||||
spec:
|
spec:
|
||||||
@@ -126,6 +128,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: netbird-dashboard-deployment
|
name: netbird-dashboard-deployment
|
||||||
namespace: netbird
|
namespace: netbird
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
+88
-35
@@ -1,43 +1,96 @@
|
|||||||
# NetBox
|
# NetBox
|
||||||
|
|
||||||
Inventory and network documentation with a web process, worker, and Valkey.
|
NetBox for homelab documentation and visualization. Two runtimes are available:
|
||||||
|
|
||||||
Kubernetes uses the shared PostgreSQL service at
|
| Runtime | Manifest | Purpose |
|
||||||
`postgres.database.svc.cluster.local:5432`, database and role `netbox`.
|
| ------- | -------------- | -------------------------------------------------------------- |
|
||||||
The database and application Secrets must contain the same password.
|
| Docker | `compose.yaml` | Local stand on `127.0.0.1:8000` (no public exposure) |
|
||||||
Media, reports, scripts, and Valkey have persistent storage.
|
| k8s | `k8s/` | Homelab service on `netbox.forust.xyz` (and the internal name) |
|
||||||
|
|
||||||
Compose has its own PostgreSQL container and Valkey instances. It publishes the
|
Both use the same image (`netboxcommunity/netbox:v4.7-5.1.1`) and Valkey for tasks
|
||||||
web UI on `127.0.0.1:8000`; its Traefik labels can also expose it while a Docker
|
plus a second logical database for caching. The Docker stand keeps its own
|
||||||
proxy is running. Copy `.env.example` to `.env`, replace the credentials, and run
|
PostgreSQL container, while the k8s deployment uses the shared `database` cluster
|
||||||
`docker compose config --quiet` before starting it.
|
(`postgres.database.svc.cluster.local:5432`, role/database `netbox`); only Valkey
|
||||||
|
stays a per-service StatefulSet.
|
||||||
|
|
||||||
## First Kubernetes start
|
## Docker Compose
|
||||||
|
|
||||||
Create the namespace and application Secret. Provision the database through the
|
```bash
|
||||||
shared database initializer on a fresh instance, or create the role and database
|
cp .env.example .env
|
||||||
manually on an existing instance; see [PostgreSQL](../postgres/README.md).
|
# replace CHANGE_ME
|
||||||
The database NetworkPolicy already includes `netbox`.
|
docker compose up -d
|
||||||
|
|
||||||
Apply the selected application manifests after the database is ready. Startup
|
|
||||||
runs schema migrations, so the probes allow a longer first boot. Inspect web and
|
|
||||||
worker logs before retrying a slow migration.
|
|
||||||
|
|
||||||
## Settings and backup
|
|
||||||
|
|
||||||
`configuration/configuration.py` is the Compose settings file. Its Kubernetes
|
|
||||||
copy is embedded in `k8s/settings.yaml`; keep them aligned.
|
|
||||||
Back up the database and media together. Keep `SECRET_KEY` and
|
|
||||||
`API_TOKEN_PEPPER_1`: changing them invalidates sessions or API tokens.
|
|
||||||
A container rollback cannot undo a database migration.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n netbox
|
|
||||||
kubectl get events -n netbox --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
The UI is available at <http://localhost:8000>. The port is bound to `127.0.0.1`
|
||||||
|
intentionally, so this stand is not exposed on the LAN or public interfaces.
|
||||||
|
|
||||||
|
The `netbox` service is also attached to the external `proxy` network and carries
|
||||||
|
Traefik labels for `netbox.forust.xyz` and `netbox.workstation.internal`. Those
|
||||||
|
labels only take effect while the Docker Traefik stack is running; it is currently
|
||||||
|
stopped, and the live ingress path in this homelab is the k8s Traefik.
|
||||||
|
|
||||||
|
Inspect startup and health with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs -f netbox
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop it with `docker compose down`; data is kept in the named volumes
|
||||||
|
`netbox-postgres`, `netbox-media-files`, `netbox-reports-files`,
|
||||||
|
`netbox-scripts-files` and `netbox-redis-data`.
|
||||||
|
|
||||||
|
## Kubernetes
|
||||||
|
|
||||||
|
`k8s/` is deployed in the homelab cluster and serves `netbox.forust.xyz` publicly
|
||||||
|
plus `netbox.workstation.internal` / `netbox.gigaforust.internal` internally. To
|
||||||
|
rebuild it from scratch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. shared PostgreSQL: the password lives in the shared secret, NetBox keeps a copy
|
||||||
|
kubectl -n database patch secret postgres-shared-secrets \
|
||||||
|
--type merge -p '{"stringData":{"NETBOX_DB_PASSWORD":"<same value>"}}'
|
||||||
|
kubectl -n database exec postgres17-0 -- psql -U postgres -d postgres \
|
||||||
|
-c 'CREATE ROLE netbox LOGIN PASSWORD ...' -c 'CREATE DATABASE netbox OWNER netbox'
|
||||||
|
|
||||||
|
# 2. secrets first: the deploy workflow never applies *secret*.yaml
|
||||||
|
cp k8s/secrets.yaml.example k8s/secrets.yaml # replace CHANGE_ME
|
||||||
|
kubectl apply -f k8s/secrets.yaml
|
||||||
|
|
||||||
|
# 3. manifests
|
||||||
|
kubectl apply -f k8s/
|
||||||
|
```
|
||||||
|
|
||||||
|
The shared cluster is reached at `postgres.database.svc.cluster.local:5432`. Its
|
||||||
|
NetworkPolicy (`postgres/k8s/network-policy.yaml`) must list the `netbox` namespace
|
||||||
|
or connections are dropped, and `postgres/initdb/01-create-databases.sh` already
|
||||||
|
creates the role and database on a fresh data directory. NetBox has no PostgreSQL
|
||||||
|
StatefulSet of its own — only `netbox-valkey`.
|
||||||
|
|
||||||
|
`netbox.forust.xyz` resolves to this host (`78.98.72.122`) through the `DOMAINS`
|
||||||
|
list in the `default/cfddns` secret. cert-manager issues `netbox-prod-tls` with the
|
||||||
|
`letsencrypt-prod` issuer, the internal route uses `internal-wildcard-tls`.
|
||||||
|
|
||||||
|
Resources are permanent again now that the first-boot migrations are complete:
|
||||||
|
the web container reserves `100m`/`512Mi` and is capped at `2` CPU/`2Gi`, the
|
||||||
|
worker reserves `50m`/`256Mi` and is capped at `1` CPU/`1Gi`, and Valkey reserves
|
||||||
|
`25m`/`64Mi` and is capped at `250m`/`256Mi`. The deliberately generous CPU caps
|
||||||
|
leave enough headroom for future schema migrations without letting one process
|
||||||
|
consume the whole node.
|
||||||
|
|
||||||
|
The first start applies ~810 migrations, each in its own transaction with DDL and
|
||||||
|
a commit; every later start is a no-op. The startup probe allows 15 minutes and
|
||||||
|
`progressDeadlineSeconds` is 1800 for the same reason. Probes run inside the pod
|
||||||
|
and explicitly set `Host: netbox.forust.xyz`; a kubelet `httpGet.host` field would
|
||||||
|
replace the probe destination with that public hostname and bypass the pod.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
- `netbox/.env` (compose) and `netbox/k8s/secrets.yaml` (k8s) are gitignored. Only
|
||||||
|
`.env.example` and `k8s/secrets.yaml.example` are committed.
|
||||||
|
- `netbox/configuration/configuration.py` is env-driven: hosts, database, Redis and
|
||||||
|
the Django keys all come from the environment, so the same settings file works in
|
||||||
|
both runtimes. The k8s copy lives in the `netbox-settings` ConfigMap
|
||||||
|
(`k8s/settings.yaml`) and must be kept in sync with the file.
|
||||||
|
- Rotating `SECRET_KEY` invalidates all sessions; rotating `API_TOKEN_PEPPER_1`
|
||||||
|
invalidates every API token.
|
||||||
@@ -14,6 +14,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: netbox-deployment
|
name: netbox-deployment
|
||||||
namespace: netbox
|
namespace: netbox
|
||||||
labels:
|
labels:
|
||||||
@@ -118,6 +120,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: netbox-worker-deployment
|
name: netbox-worker-deployment
|
||||||
namespace: netbox
|
namespace: netbox
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: StatefulSet
|
kind: StatefulSet
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: netbox-valkey
|
name: netbox-valkey
|
||||||
namespace: netbox
|
namespace: netbox
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Netronome
|
|
||||||
|
|
||||||
Network monitoring application using the shared PostgreSQL instance on Kubernetes.
|
|
||||||
|
|
||||||
Kubernetes reads application settings from its ConfigMap and Secret. Match the
|
|
||||||
Netronome role password with `NETRONOME_DB_PASSWORD` in the shared database Secret.
|
|
||||||
Its namespace is included in the PostgreSQL NetworkPolicy.
|
|
||||||
|
|
||||||
The Compose configuration is a separate deployment; review its local database
|
|
||||||
settings and env example before starting it. Keep monitoring history in the
|
|
||||||
database backup.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n netronome
|
|
||||||
kubectl get events -n netronome --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -14,6 +14,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: netronome-deployment
|
name: netronome-deployment
|
||||||
namespace: netronome
|
namespace: netronome
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# Nextcloud AIO
|
|
||||||
|
|
||||||
Nextcloud All-in-One on Docker, with Kubernetes routes to the Docker host.
|
|
||||||
|
|
||||||
The master container manages its own child containers through the Docker
|
|
||||||
socket. Kubernetes does not run the Nextcloud application; the EndpointSlices
|
|
||||||
under `k8s/routing/` point to host services.
|
|
||||||
|
|
||||||
Compose publishes the AIO administration interface on 8888. The Apache frontend
|
|
||||||
uses host port 11000. `NEXTCLOUD_DATADIR` is `/mnt/nextcloud/ncdata`; prepare that
|
|
||||||
storage before first setup and do not change the path casually afterwards.
|
|
||||||
|
|
||||||
Use AIO's backup and restore tools for the managed application. Keep the master
|
|
||||||
configuration volume and the data directory with the recovery plan. Do not
|
|
||||||
remove child containers just because they do not appear as Compose services.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Penpot
|
|
||||||
|
|
||||||
A Compose-only design application with frontend, backend, exporter, database, and cache.
|
|
||||||
|
|
||||||
There is no active marker or Kubernetes deployment here. Configure the public
|
|
||||||
URL and credentials from `.env.example` before starting `compose.yaml`.
|
|
||||||
|
|
||||||
Penpot has its own PostgreSQL container. The shared database initializer still
|
|
||||||
contains a Penpot role, but this Compose stack does not use it.
|
|
||||||
Back up the application assets and database together.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Portainer
|
|
||||||
|
|
||||||
Container management UI backed by the host Docker socket.
|
|
||||||
|
|
||||||
Kubernetes mounts the node's Docker socket and persists application data in
|
|
||||||
`portainer-data-pvc`. This targets Docker on that node, not Kubernetes workloads.
|
|
||||||
Compose uses the `portainer_data` volume for its state.
|
|
||||||
|
|
||||||
Review initial administrator setup and route access before exposing the UI.
|
|
||||||
Neither deployment has an active marker.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n portainer
|
|
||||||
kubectl get events -n portainer --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
+28
-56
@@ -1,63 +1,35 @@
|
|||||||
# Shared PostgreSQL
|
# Shared PostgreSQL
|
||||||
|
|
||||||
PostgreSQL 17 for the Kubernetes deployments of Authentik, Gitea, NetBox, and Netronome.
|
This directory contains the shared PostgreSQL 17 deployment for Authentik,
|
||||||
|
Gitea, NetBox, Netronome, and Statuspage. It creates one database and one login role
|
||||||
|
per service. Per-service standalone databases were removed after the
|
||||||
|
migration (Sep 2026); Penpot stays on its own compose PostgreSQL (archived,
|
||||||
|
not part of the shared instance).
|
||||||
|
|
||||||
The server runs in `database` as StatefulSet `postgres17`, with data in
|
## Compatibility baseline
|
||||||
`postgres17-data`. Applications connect to
|
|
||||||
`postgres.database.svc.cluster.local:5432`. The NetworkPolicy allows only the
|
|
||||||
listed application namespaces; add a new consumer there as well as provisioning
|
|
||||||
its database.
|
|
||||||
|
|
||||||
## Initialization
|
| Service | Current application | Shared PostgreSQL 17 |
|
||||||
|
| ---------- | ------------------- | -------------------------------------- |
|
||||||
|
| Authentik | 2025.10.x | Supported (Authentik requires 14+) |
|
||||||
|
| Gitea | 1.27.3 | Supported (Gitea requires 12+) |
|
||||||
|
| NetBox | 4.7.x | Supported (NetBox 4.x requires 13+) |
|
||||||
|
| Netronome | 0.14.0 | Supported (upstream's example uses 17) |
|
||||||
|
| Statuspage | custom | Supported |
|
||||||
|
|
||||||
`initdb/01-create-databases.sh` creates roles and databases on an empty data
|
A major-version change must use a logical dump/restore; changing only the
|
||||||
directory. The Kubernetes copy is embedded in `k8s/postgres.yaml`.
|
image tag while keeping a data directory is not supported.
|
||||||
It also provisions Penpot and Statuspage roles, even though those are not active
|
|
||||||
consumers in the current Kubernetes manifests.
|
|
||||||
|
|
||||||
The initializer requires every listed password. Prepare `k8s/secrets.yaml` from
|
For Compose, copy `.env.example` to `.env`, set all passwords, and start it with
|
||||||
the example before applying the StatefulSet. Existing application Secrets keep
|
`docker compose -f shared-compose.yaml up -d`. This file is intentionally not
|
||||||
copies of their own database passwords; they must match the corresponding role.
|
named `compose.yaml`, so the repository deploy workflow does not start a second
|
||||||
|
database accidentally.
|
||||||
|
Applications that use this database must also join that external network and use
|
||||||
|
`homelab-postgres:5432`.
|
||||||
|
|
||||||
The init scripts do not run again when an existing data directory is mounted.
|
For Kubernetes, create `k8s/secrets.yaml` from the example before applying the
|
||||||
Changing a Secret does not rotate the PostgreSQL role password. Rotate the role
|
manifests. The `k8s/active` marker makes the normal deploy workflow include the
|
||||||
with SQL and update the application Secret together.
|
namespace, StatefulSet, ConfigMap, and NetworkPolicy. Applications use
|
||||||
|
`postgres.database.svc.cluster.local:5432`.
|
||||||
## Compose alternative
|
Migrate each existing database with a tested logical dump/restore before
|
||||||
|
switching an application. Do not reuse a PostgreSQL 14 or 17 data directory
|
||||||
From this directory:
|
with PostgreSQL 15.
|
||||||
|
|
||||||
```sh
|
|
||||||
cp .env.example .env
|
|
||||||
$EDITOR .env
|
|
||||||
docker compose -f shared-compose.yaml config --quiet
|
|
||||||
docker compose -f shared-compose.yaml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Add `NETBOX_DB_PASSWORD` to `.env` as well: the reviewed env example omits it;
|
|
||||||
`fix/postgres-env-example` restores the key. Fill every required password.
|
|
||||||
This stack creates the `homelab-database` Docker network and the
|
|
||||||
`homelab-postgres` container. Compose applications need to join that network
|
|
||||||
explicitly to use it; several committed Compose stacks use their own databases.
|
|
||||||
|
|
||||||
The filename is intentional: the automatic deploy discovery does not start this
|
|
||||||
stack just because the Kubernetes database is active.
|
|
||||||
|
|
||||||
## Backup and upgrades
|
|
||||||
|
|
||||||
Keep database dumps and role definitions, including ownership and grants.
|
|
||||||
Take a logical backup before changing a major PostgreSQL version. A new image
|
|
||||||
tag over the existing data directory is not a major-version migration.
|
|
||||||
Test restores separately before changing application connection settings.
|
|
||||||
Immich uses its own vector-enabled database and is outside this shared instance.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n database
|
|
||||||
kubectl get events -n database --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Monitoring stack
|
|
||||||
|
|
||||||
Prometheus, Grafana, Alertmanager, and application alert rules.
|
|
||||||
|
|
||||||
Kubernetes installs `kube-prometheus-stack` in `prometheus` through the deploy
|
|
||||||
library. Its chart version is pinned there; `k8s/grafana-values.yaml` contains the
|
|
||||||
values for the whole stack, despite the filename.
|
|
||||||
|
|
||||||
The values file is tracked. `.gitignore` also lists it, but that does not stop Git
|
|
||||||
tracking later edits. Keep local credentials in Secrets rather than treating
|
|
||||||
changes to this file as ignored.
|
|
||||||
|
|
||||||
Prepare Grafana admin and Alertmanager Secrets separately. The Alertmanager
|
|
||||||
configuration example and Telegram template are in `k8s/`; the main deploy
|
|
||||||
selection excludes the example config. Certificates and ingress expose Grafana,
|
|
||||||
and the rule files add service-specific alerts.
|
|
||||||
|
|
||||||
The values use `local-path` PVCs for Grafana, Prometheus, and Alertmanager.
|
|
||||||
Retention is limited by both time and size. Back up Grafana state and any history
|
|
||||||
that must survive a storage failure.
|
|
||||||
|
|
||||||
The Compose stack has separate Prometheus and Alertmanager configuration files.
|
|
||||||
There is no root active marker. This README describes committed main files;
|
|
||||||
local VictoriaMetrics experiments are not part of that configuration.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# RackPeek
|
|
||||||
|
|
||||||
Rack inventory UI behind Traefik.
|
|
||||||
|
|
||||||
Kubernetes stores configuration in `rackpeek-pvc`. The Compose alternative uses
|
|
||||||
its own data mount. Keep rack descriptions and inventory data in the backup.
|
|
||||||
|
|
||||||
Public and internal certificates and routes are in `k8s/`. There are no tracked
|
|
||||||
Secret examples for this service.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n rackpeek
|
|
||||||
kubectl get events -n rackpeek --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Reloader
|
|
||||||
|
|
||||||
Restarts opted-in workloads when the ConfigMaps or Secrets they consume change.
|
|
||||||
The deploy workflow upgrades the `reloader` Helm release in namespace `reloader`;
|
|
||||||
`k8s/active` enables it. The chart version is pinned in `deploy-lib.sh`.
|
|
||||||
|
|
||||||
## Workload integration
|
|
||||||
|
|
||||||
Put this annotation on the Deployment or StatefulSet metadata:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
reloader.stakater.com/auto: "true"
|
|
||||||
```
|
|
||||||
|
|
||||||
The annotation belongs to the workload, not `spec.template.metadata`.
|
|
||||||
Reloader discovers references in environment variables and mounted volumes.
|
|
||||||
This covers startup-only settings and ConfigMaps or Secrets mounted with `subPath`.
|
|
||||||
See the [upstream usage guide](https://github.com/stakater/Reloader/blob/v1.4.22/README.md#usage).
|
|
||||||
|
|
||||||
The application manifests opt in 28 workloads, including AdGuard's TLS files,
|
|
||||||
NetBird, both NetBox processes, EDU bots, and the password-protected Valkey servers.
|
|
||||||
Inactive services have the same annotations ready for later activation.
|
|
||||||
|
|
||||||
## Controller policy
|
|
||||||
|
|
||||||
The controller watches all namespaces but only restarts annotated workloads.
|
|
||||||
It uses the `annotations` reload strategy, so changes trigger a pod-template
|
|
||||||
annotation rather than injecting extra environment variables.
|
|
||||||
|
|
||||||
Jobs and CronJobs are excluded: their next execution reads current configuration.
|
|
||||||
PostgreSQL is intentionally not opted in. Its password variables and init scripts
|
|
||||||
apply to first initialization; restarting an existing database does not rotate
|
|
||||||
roles or rerun those scripts. Rotate database credentials with SQL and update the
|
|
||||||
clients' Secrets together.
|
|
||||||
|
|
||||||
Helm-managed monitoring components already have their own configuration reload
|
|
||||||
paths; Traefik watches its file-provider configuration. They are not globally
|
|
||||||
opted in. The controller does not react to files in PVCs or changes to external
|
|
||||||
services unless a watched ConfigMap or Secret changes.
|
|
||||||
|
|
||||||
## Verify
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl -n reloader rollout status deployment/reloader-reloader
|
|
||||||
kubectl -n reloader logs deployment/reloader-reloader --since=10m
|
|
||||||
kubectl -n netbird get deployment netbird-server-deployment \
|
|
||||||
-o jsonpath='{.metadata.annotations.reloader\.stakater\.com/auto}'
|
|
||||||
```
|
|
||||||
|
|
||||||
A changed configuration can briefly interrupt a single-replica service, especially
|
|
||||||
one using `Recreate`. Installing annotations does not validate the configuration
|
|
||||||
or migrate database data. Keep changes to shared Secrets coordinated across consumers.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
Whitespace-only changes.
@@ -1,11 +1,17 @@
|
|||||||
# Pinned chart: stakater/reloader 2.2.17 (app v1.4.22).
|
# Pinned chart: stakater/reloader 2.2.17 (app v1.4.22).
|
||||||
# Deployed by the deploy workflow, namespace reloader.
|
# Deployed by the deploy workflow, namespace reloader.
|
||||||
# Restarts pods when a ConfigMap or Secret they consume changes. Opt-in per workload
|
# Restarts pods when a ConfigMap or Secret they consume changes. Opt-in per workload
|
||||||
# via the reloader.stakater.com/auto: "true" pod annotation; watchGlobally because
|
# via the reloader.stakater.com/auto: "true" workload annotation; watchGlobally because
|
||||||
# the workloads that need it are spread across a few dozen namespaces.
|
# the workloads that need it are spread across a few dozen namespaces.
|
||||||
|
|
||||||
reloader:
|
reloader:
|
||||||
watchGlobally: true
|
watchGlobally: true
|
||||||
|
# Only opted-in workloads are restarted. Keep scheduled jobs on their schedule.
|
||||||
|
autoReloadAll: false
|
||||||
|
ignoreJobs: true
|
||||||
|
ignoreCronJobs: true
|
||||||
|
# Change pod-template annotations rather than injecting STAKATER_* env vars.
|
||||||
|
reloadStrategy: annotations
|
||||||
|
|
||||||
deployment:
|
deployment:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
|
|||||||
+80
-30
@@ -1,51 +1,101 @@
|
|||||||
# Renovate
|
# Renovate for Gitea
|
||||||
|
|
||||||
Container and chart dependency updates for the Gitea repository.
|
Renovate runs as a Kubernetes CronJob and creates container image update pull
|
||||||
|
requests in Gitea. It does not deploy changes itself.
|
||||||
|
|
||||||
The Kubernetes CronJob runs in `renovate` every six hours with overlapping
|
## Kubernetes
|
||||||
CronJob executions forbidden. Prepare the bot PAT from the Secret example.
|
|
||||||
Give the dedicated Gitea user access to the repositories it should update.
|
|
||||||
|
|
||||||
`renovate.json` is the source configuration. The ConfigMap is a generated copy:
|
Create a dedicated Gitea user named `renovate-bot`, create a repository access
|
||||||
|
token, and grant it repository read/write plus issue read/write permissions.
|
||||||
|
Add `read:packages` if Renovate must inspect private Gitea registry images.
|
||||||
|
|
||||||
|
Create the ignored Secret locally; never commit the PAT:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
.gitea/workflows/sync-renovate-configmap.sh
|
cp renovate/k8s/secrets.yaml.example renovate/k8s/secrets.yaml
|
||||||
.gitea/workflows/sync-renovate-configmap.sh --check
|
$EDITOR renovate/k8s/secrets.yaml
|
||||||
|
kubectl apply -f renovate/k8s/namespace.yaml
|
||||||
|
kubectl apply -f renovate/k8s/secrets.yaml
|
||||||
|
kubectl apply -f renovate/k8s/configmap.yaml
|
||||||
|
kubectl apply -f renovate/k8s/cronjob.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Run those commands from the repository root. The `renovate-ci` workflow checks
|
The `renovate/k8s/active` marker makes the normal deployment workflow include
|
||||||
that the generated configuration agrees with the source.
|
the namespace, ConfigMap, and CronJob. The Secret is intentionally excluded
|
||||||
|
from Git and must be applied separately after every new cluster.
|
||||||
|
|
||||||
## Run manually
|
Run it immediately instead of waiting for the six-hour schedule.
|
||||||
|
|
||||||
From the repository root:
|
Two options, both use the same `renovate/renovate.json`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
kubectl create job --from=cronjob/renovate renovate-manual-$(date +%s) -n renovate
|
kubectl create job --from=cronjob/renovate renovate-manual-$(date +%s) -n renovate
|
||||||
kubectl get jobs,pods -n renovate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively use the `renovate-run` Actions workflow. It reads the image tag
|
or the `renovate-run` Actions workflow (Actions tab → `renovate-run` →
|
||||||
from the CronJob and accepts repository, log-level, and dry-run inputs. Actions
|
Run workflow). It runs the same image as the CronJob on the self-hosted runner
|
||||||
requires `RENOVATE_TOKEN`; `RENOVATE_GITHUB_COM_TOKEN` is optional.
|
via Docker — the tag is read out of `renovate/k8s/cronjob.yaml` at run time
|
||||||
The Actions concurrency group and the CronJob policy are separate, so avoid
|
rather than hardcoded, so the two cannot drift apart. Required Actions secrets
|
||||||
starting both against the same repository at once.
|
(repo or org settings):
|
||||||
|
|
||||||
For Compose, copy `.env.example` to `.env` in this directory and run
|
- `RENOVATE_TOKEN` — renovate-bot PAT (repository + issue read/write).
|
||||||
`docker compose -f renovate-compose.yaml run --rm renovate`. That file is a
|
- `RENOVATE_GITHUB_COM_TOKEN` — optional, for changelogs and GitHub rate limits.
|
||||||
manual entry point and is not selected by the deploy workflow.
|
|
||||||
|
|
||||||
The config also tracks chart versions in `deploy-lib.sh` and tool versions in
|
Inputs: `repositories` (default `forust/homelab`), `log_level`
|
||||||
`tool-versions.env`. Renovate opens pull requests; the normal CI and deploy
|
(`info`/`debug`). Only one run at a time (concurrency group
|
||||||
workflows handle changes after merge.
|
`renovate-run`), same as the CronJob `Forbid` policy.
|
||||||
|
|
||||||
## Inspect
|
Inspect runs with:
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
kubectl get pods,svc,pvc -n renovate
|
kubectl get cronjob,jobs,pods -n renovate
|
||||||
kubectl get events -n renovate --sort-by=.metadata.creationTimestamp
|
kubectl logs -n renovate job/<job-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
`RENOVATE_GITHUB_COM_TOKEN` is optional but recommended for changelogs and
|
||||||
|
GitHub API rate limits. Set it in the Kubernetes Secret if available.
|
||||||
|
|
||||||
|
## Compose
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env`, set the PAT, and run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f renovate-compose.yaml run --rm renovate
|
||||||
|
```
|
||||||
|
|
||||||
|
The Compose file is intentionally named `renovate-compose.yaml`, so the
|
||||||
|
repository's automatic deployment discovery does not start it accidentally.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
`renovate/renovate.json` is the single source of truth. The Compose file and the
|
||||||
|
`renovate-run` workflow mount that file directly.
|
||||||
|
|
||||||
|
A ConfigMap cannot read from the repository, so the CronJob needs the config
|
||||||
|
inlined. `renovate/k8s/configmap.yaml` is therefore a **generated** copy:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.gitea/workflows/sync-renovate-configmap.sh # regenerate after editing
|
||||||
|
.gitea/workflows/sync-renovate-configmap.sh --check # fail if out of date
|
||||||
|
```
|
||||||
|
|
||||||
|
The `renovate-ci` workflow runs the `--check` form on every PR and push, so a
|
||||||
|
config edit that forgets to regenerate the ConfigMap cannot be merged.
|
||||||
|
|
||||||
|
Beyond images, `customManagers` in the config track:
|
||||||
|
|
||||||
|
- Helm chart versions pinned in `.gitea/workflows/deploy-lib.sh`. The built-in
|
||||||
|
`helmv3` manager only reads `Chart.yaml` and `helm-values` only reads values
|
||||||
|
files, so neither sees a version written into a `helm upgrade` command —
|
||||||
|
these are declared as `custom.regex` managers against the `helm` datasource.
|
||||||
|
- CI linter versions in `.gitea/workflows/tool-versions.env`.
|
||||||
|
|
||||||
|
The Renovate image tag is deliberately _not_ in `tool-versions.env`:
|
||||||
|
`renovate/k8s/cronjob.yaml` owns it, and the workflows read it from there.
|
||||||
|
|
||||||
|
## How updates flow
|
||||||
|
|
||||||
|
Renovate scans both `compose.yaml` files and Kubernetes manifests, opens a
|
||||||
|
branch and PR with image tag changes, and waits for CI. After merge, the
|
||||||
|
existing deployment workflow applies Kubernetes changes or redeploys Compose
|
||||||
|
stacks. Renovate never updates running workloads directly.
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# SearXNG
|
|
||||||
|
|
||||||
Search frontend with a separate Valkey cache.
|
|
||||||
|
|
||||||
Kubernetes keeps the application settings in a ConfigMap and starts Valkey as a
|
|
||||||
StatefulSet. Set the secret from the example before exposing the search endpoint.
|
|
||||||
There is no active marker.
|
|
||||||
|
|
||||||
Compose expects local configuration under `core-config/`, which is ignored.
|
|
||||||
Prepare it before starting the stack; a container image alone does not supply
|
|
||||||
this lab's settings.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n searxng
|
|
||||||
kubectl get events -n searxng --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: searxng-deployment
|
name: searxng-deployment
|
||||||
namespace: searxng
|
namespace: searxng
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# Media stack
|
|
||||||
|
|
||||||
Docker services for playback, requests, library management, and downloads.
|
|
||||||
|
|
||||||
Compose runs Jellyfin, Jellyseerr, Sonarr, Radarr, Prowlarr, qBittorrent, and the
|
|
||||||
other services declared in the file. Kubernetes only routes to host endpoints;
|
|
||||||
update `k8s/routing/external-service.yaml` when the Docker host or ports change.
|
|
||||||
|
|
||||||
Prepare the paths, user/group IDs, and credentials from `.env.example`. Service
|
|
||||||
configuration and media/download directories are bind mounts. Preserve their
|
|
||||||
permissions when moving data, and keep the application databases with backups.
|
|
||||||
|
|
||||||
Review device mounts for hardware acceleration before starting on another host.
|
|
||||||
Both active markers are present, so normal deploys include Compose and routing.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Termix
|
|
||||||
|
|
||||||
Terminal and SSH connection manager with persistent application data.
|
|
||||||
|
|
||||||
Kubernetes stores state in `termix-pvc`; Compose mounts `termix-data/`.
|
|
||||||
The application config and routes are committed separately under `k8s/`.
|
|
||||||
|
|
||||||
There is no active marker. Review access control and retain the application data
|
|
||||||
needed to recover saved connections before enabling it.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n termix
|
|
||||||
kubectl get events -n termix --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: termix-deployment
|
name: termix-deployment
|
||||||
namespace: termix
|
namespace: termix
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# Traefik
|
|
||||||
|
|
||||||
Ingress for HTTP, gRPC, TCP, and UDP services, with public and internal TLS.
|
|
||||||
|
|
||||||
Kubernetes uses the Helm settings in `k8s/traefik-values.yaml`. The deploy
|
|
||||||
library applies supporting resources in this directory but does not install or
|
|
||||||
upgrade the Traefik chart. Bootstrap the chart and CRDs separately.
|
|
||||||
|
|
||||||
The LoadBalancer address is set to `192.168.80.2`. Change it for another network.
|
|
||||||
Entrypoints include web traffic, Gitea SSH, NetBird STUN, and other lab protocols.
|
|
||||||
Public certificates come from cert-manager; internal certificates use the lab CA.
|
|
||||||
The file provider reads `traefik-dynamic` through an additional volume and flags.
|
|
||||||
|
|
||||||
## API access
|
|
||||||
|
|
||||||
The committed chart values enable `api.insecure` and expose TCP 8080 through the
|
|
||||||
LoadBalancer for Homarr integration. That listener has no Traefik authentication.
|
|
||||||
Its reachability depends on external network controls. Review those controls
|
|
||||||
before deploying these values outside the trusted network.
|
|
||||||
|
|
||||||
The normal dashboard IngressRoute is a separate path; protecting that route does
|
|
||||||
not protect the direct port 8080 listener.
|
|
||||||
|
|
||||||
## Compose alternative
|
|
||||||
|
|
||||||
Compose mounts static and dynamic config, certificates, ACME state, and the
|
|
||||||
Docker socket. It needs the external `proxy` network. Local file-server routing
|
|
||||||
and TLS files have `.example` templates; copy only the ones needed for the host.
|
|
||||||
|
|
||||||
Keep ACME state and private keys with backups. Changing ingress values can affect
|
|
||||||
every service at once, so inspect routes and entrypoints after an upgrade.
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Uptime Kuma
|
|
||||||
|
|
||||||
Service checks, status pages, and Prometheus metrics.
|
|
||||||
|
|
||||||
Kubernetes keeps state in `uptime-kuma-pvc` and exposes metrics through a
|
|
||||||
ServiceMonitor. The metrics credentials come from the local Secret example.
|
|
||||||
`alerts.yaml` adds Prometheus rules; a running Kuma UI alone does not establish
|
|
||||||
that Prometheus is scraping it.
|
|
||||||
|
|
||||||
Compose stores state in `data/`. Back up that application database and verify
|
|
||||||
notification delivery after restoring it.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n uptime-kuma
|
|
||||||
kubectl get events -n uptime-kuma --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Vaultwarden
|
|
||||||
|
|
||||||
Password vault server with persistent data and public/internal ingress.
|
|
||||||
|
|
||||||
Kubernetes uses `vaultwarden-pvc` and the public URL from a ConfigMap.
|
|
||||||
Compose has a separate data volume. Preserve the database, attachments, and keys
|
|
||||||
as part of the same backup.
|
|
||||||
|
|
||||||
The env example only sets `DOMAIN`; there is no tracked administrator Secret
|
|
||||||
example. Configure any administrator token separately and keep it out of Git. Verify
|
|
||||||
sign-in and client synchronization after any update. Do not use a successful
|
|
||||||
container restart as the only restore check.
|
|
||||||
|
|
||||||
## Inspect
|
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n vaultwarden
|
|
||||||
kubectl get events -n vaultwarden --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../README.md) for deployment selection.
|
|
||||||
@@ -13,6 +13,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: vaultwarden-deployment
|
name: vaultwarden-deployment
|
||||||
namespace: vaultwarden
|
namespace: vaultwarden
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# VPN services
|
|
||||||
|
|
||||||
[x-ui](xui/README.md) contains the Kubernetes deployment for 3x-ui. This directory
|
|
||||||
has no shared Compose stack. Active markers are checked at each service's `k8s/`
|
|
||||||
level, including nested paths.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 3x-ui
|
|
||||||
|
|
||||||
Kubernetes deployment for the 3x-ui management panel in namespace `xui`.
|
|
||||||
The `k8s/active` marker includes it in normal deploy selection.
|
|
||||||
|
|
||||||
The workload, data mounts, and ports are in `k8s/xui.yaml`; panel routing and TLS
|
|
||||||
are in the ingress and certificate files. Keep panel access and proxy protocol
|
|
||||||
ports separate when changing the configuration.
|
|
||||||
|
|
||||||
Back up the application's database and keys before upgrades. Check the actual
|
|
||||||
host and volume paths before moving the workload to another node.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get pods,svc,pvc -n xui
|
|
||||||
kubectl get events -n xui --sort-by=.metadata.creationTimestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [repository README](../../README.md) for deployment selection.
|
|
||||||
@@ -20,6 +20,8 @@ spec:
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/auto: "true"
|
||||||
name: xui-deployment
|
name: xui-deployment
|
||||||
namespace: xui
|
namespace: xui
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
Reference in new issue
Block a user