Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcd16f6128 | ||
|
|
2ac2a94bb4 | ||
|
|
3c7e358dd5 | ||
|
|
9be8fb6c69 | ||
|
|
7fdeacffb8 | ||
|
|
cef499de73 | ||
|
|
1b70a55300 | ||
|
|
3f2b4e9acf | ||
|
|
6057734a4f | ||
|
|
8eedf8b74b | ||
|
|
8c0e36a5c0 | ||
|
|
78cd15f12c | ||
|
|
18c633c242 | ||
|
|
4510394531 | ||
|
|
2545312db1 | ||
|
|
8ce0b809c0 | ||
|
|
506c04e15c | ||
|
|
bdcbb3d5af | ||
|
|
faac6febb8 | ||
|
|
695da1308c | ||
|
|
552b22cb66 | ||
|
|
3756e60c95 | ||
|
|
d0d217ddf4 | ||
|
|
24f84bab2f | ||
|
|
0b8a3c16a7 | ||
|
|
e9a68aae77 | ||
|
|
5359df5ed6 | ||
|
|
8aea0f0d13 | ||
|
|
65d4f2d482 | ||
|
|
2fe9b7632f | ||
|
|
e436d89eef | ||
|
|
8729cb5062 | ||
|
|
f1e4a1088d | ||
|
|
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
+94
@@ -0,0 +1,94 @@
|
||||
#!/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 ! skip_uninstalled_vmagent_crd "$REPO/prometheus-stack/k8s/vmagent.yaml"; then
|
||||
echo 'VMAgent preflight did not skip an uninstalled CRD' >&2
|
||||
exit 1
|
||||
fi
|
||||
kubectl() { return 0; }
|
||||
if skip_uninstalled_vmagent_crd "$REPO/prometheus-stack/k8s/vmagent.yaml"; then
|
||||
echo 'VMAgent preflight skipped an installed CRD' >&2
|
||||
exit 1
|
||||
fi
|
||||
if skip_uninstalled_vmagent_crd "$REPO/prometheus-stack/k8s/victoria.yaml"; then
|
||||
echo 'VMAgent preflight skipped an unrelated manifest' >&2
|
||||
exit 1
|
||||
fi
|
||||
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:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
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"
|
||||
mapfile -t scripts < <(
|
||||
git ls-files '*.sh' ':(glob)**/*.bash'
|
||||
@@ -98,6 +98,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
shellcheck --external-sources --source-path=SCRIPTDIR --severity=style "${scripts[@]}"
|
||||
bash .gitea/tests/deploy-validation.sh
|
||||
|
||||
lint-prettier:
|
||||
runs-on: [self-hosted, linux, arch, homelab]
|
||||
@@ -141,6 +142,7 @@ jobs:
|
||||
export PATH="$tools_dir:$PATH"
|
||||
ruff check .
|
||||
ruff format --check .
|
||||
python3 -m unittest discover -s tests -v
|
||||
|
||||
lint-yaml:
|
||||
runs-on: [self-hosted, linux, arch, homelab]
|
||||
@@ -292,6 +294,11 @@ jobs:
|
||||
echo "server-side dry-run: ${#manifests[@]} manifests, ${#kustomize_apps[@]} kustomize apps"
|
||||
failed=0
|
||||
for m in ${manifests[@]+"${manifests[@]}"}; do
|
||||
if [[ "$m" == "prometheus-stack/k8s/vmagent.yaml" ]] \
|
||||
&& ! kubectl get crd vmagents.operator.victoriametrics.com >/dev/null 2>&1; then
|
||||
echo "skip server-side dry-run until the VictoriaMetrics Operator CRD is installed: $m"
|
||||
continue
|
||||
fi
|
||||
if ! out="$(kubectl apply --dry-run=server -f "$m" 2>&1)"; then
|
||||
failed=1
|
||||
echo "::error file=${m}::$(printf '%s' "$out" | head -1)"
|
||||
@@ -331,11 +338,20 @@ jobs:
|
||||
- name: Detect changed docker-built services
|
||||
id: services
|
||||
shell: bash
|
||||
env:
|
||||
PUSH_BEFORE: ${{ github.event.before }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base="${{ github.event.before }}"
|
||||
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
|
||||
base="$(git rev-list --max-parents=0 HEAD)"
|
||||
base="${PUSH_BEFORE:-}"
|
||||
empty_tree="$(git hash-object -t tree /dev/null)"
|
||||
if [[ "$base" =~ ^0{40}$ ]]; then
|
||||
base="$empty_tree"
|
||||
elif [[ ! "$base" =~ ^[0-9a-fA-F]{40}$ ]] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
|
||||
# Some Gitea push payloads expose `before` as multiple root commits
|
||||
# joined by newlines. It is not a usable diff base; use this push's
|
||||
# first parent so image changes in the current commit are still built.
|
||||
base="$(git rev-parse "${GITHUB_SHA}^" 2>/dev/null || printf '%s' "$empty_tree")"
|
||||
echo "::warning::invalid push-before value; comparing against ${base}"
|
||||
fi
|
||||
|
||||
# A failed diff used to leave changed_files empty, which reads exactly
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
# All committed Compose files, including the ones deploy never starts.
|
||||
compose_files() {
|
||||
git ls-files \
|
||||
'*/compose.yaml' '*/compose.yml' 'compose.yaml' 'compose.yml' \
|
||||
'*/docker-compose.yaml' '*/docker-compose.yml'
|
||||
'*compose.yaml' '*compose.yml'
|
||||
}
|
||||
|
||||
# Prints the flags that turn `docker compose config` into the general check.
|
||||
|
||||
@@ -31,6 +31,16 @@ warn() {
|
||||
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() {
|
||||
git -C "$REPO" ls-files -- "$1" \
|
||||
| grep -E '\.ya?ml$' \
|
||||
@@ -536,6 +546,7 @@ rollback_workloads() {
|
||||
# have to be declared as custom.regex managers in renovate/renovate.json.
|
||||
HELM_RELEASES=(
|
||||
"prometheus-stack|prometheus-community/kube-prometheus-stack|prometheus|86.2.3|prometheus-stack/k8s/grafana-values.yaml|prometheus-stack/k8s/active"
|
||||
"victoria-operator|victoriametrics/victoria-metrics-operator|prometheus|0.68.1|prometheus-stack/k8s/victoria-operator-values.yaml|prometheus-stack/k8s/active"
|
||||
"loki|grafana/loki|prometheus|7.3.0|loki/k8s/loki-values.yaml|loki/k8s/active"
|
||||
"alloy|grafana/alloy|prometheus|1.12.1|loki/k8s/alloy-values.yaml|loki/k8s/active"
|
||||
"reloader|stakater/reloader|reloader|2.2.17|reloader/k8s/reloader-values.yaml|reloader/k8s/active"
|
||||
@@ -547,6 +558,7 @@ helm_repo_for() {
|
||||
prometheus-community/*) echo "prometheus-community https://prometheus-community.github.io/helm-charts" ;;
|
||||
grafana/*) echo "grafana https://grafana.github.io/helm-charts" ;;
|
||||
stakater/*) echo "stakater https://stakater.github.io/stakater-charts" ;;
|
||||
victoriametrics/*) echo "victoriametrics https://victoriametrics.github.io/helm-charts" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -686,30 +698,74 @@ stage_preflight() {
|
||||
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
|
||||
if skip_uninstalled_vmagent_crd "$m"; then
|
||||
continue
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
# The VMAgent CRD is installed by the VictoriaMetrics Operator Helm release in
|
||||
# stage_apply_k8s, after this preflight stage. Skip only its dry-run until then.
|
||||
skip_uninstalled_vmagent_crd() {
|
||||
local manifest="$1"
|
||||
if [[ "$manifest" == "$REPO/prometheus-stack/k8s/vmagent.yaml" ]] \
|
||||
&& ! kubectl get crd vmagents.operator.victoriametrics.com >/dev/null 2>&1; then
|
||||
echo " skip: VMAgent CRD is installed by Helm during apply: ${manifest#"$REPO"/}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
stage_validate() {
|
||||
check_prune_mode || return 1
|
||||
cd "$REPO"
|
||||
select_manifests
|
||||
local m k cf
|
||||
# Compose .env files and secret files are gitignored by design, so the
|
||||
# workstation never has real values for the inactive stacks. This stage only
|
||||
# runs the full check on active stacks; the general structure check for every
|
||||
# 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.
|
||||
# The deploy host has the local .env and secret files. Resolve them here so
|
||||
# missing configuration fails before either apply job changes workloads.
|
||||
# CI keeps the structure-only check for inactive stacks.
|
||||
# shellcheck source=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"
|
||||
for cf in ${COMPOSE_STACKS[@]+"${COMPOSE_STACKS[@]}"}; do
|
||||
echo " config: $cf"
|
||||
validate_compose_file "$cf" ${compose_validate_flags[@]+"${compose_validate_flags[@]}"}
|
||||
validate_compose_file "$cf"
|
||||
done
|
||||
log "Validate k8s manifests (kubectl dry-run=client)"
|
||||
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
|
||||
if skip_uninstalled_vmagent_crd "$m"; then
|
||||
continue
|
||||
fi
|
||||
kubectl apply --dry-run=client -f "$m" >/dev/null
|
||||
done
|
||||
for k in ${KUSTOMIZE_APPS[@]+"${KUSTOMIZE_APPS[@]}"}; do
|
||||
@@ -717,6 +773,9 @@ stage_validate() {
|
||||
done
|
||||
log "Validate k8s manifests (kubectl dry-run=server)"
|
||||
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
|
||||
if skip_uninstalled_vmagent_crd "$m"; then
|
||||
continue
|
||||
fi
|
||||
kubectl apply --dry-run=server -f "$m" >/dev/null
|
||||
done
|
||||
for k in ${KUSTOMIZE_APPS[@]+"${KUSTOMIZE_APPS[@]}"}; do
|
||||
@@ -724,48 +783,20 @@ stage_validate() {
|
||||
done
|
||||
log "Checking referenced Secrets exist"
|
||||
echo " (deploy never applies *secret*.yaml; create missing ones manually)"
|
||||
local ref_secrets=() missing_secrets=() all_secrets s
|
||||
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
|
||||
check_referenced_secrets
|
||||
}
|
||||
|
||||
stage_apply_k8s() {
|
||||
check_prune_mode || return 1
|
||||
cd "$REPO"
|
||||
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
|
||||
case "$m" in
|
||||
*/namespace.y?ml) ns_files+=("$m") ;;
|
||||
*) other_files+=("$m") ;;
|
||||
esac
|
||||
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
|
||||
# the first apply. Both are fatal on failure: see snapshot_dir.
|
||||
@@ -790,7 +821,7 @@ stage_apply_k8s() {
|
||||
if [ "${#other_files[@]}" -gt 0 ]; then
|
||||
log "Applying resources (${#other_files[@]} files, our images pinned to digests)"
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
# workflow_dispatch so a red lint/validate run can never reach the cluster.
|
||||
workflow_run:
|
||||
workflows: [ci]
|
||||
branches: [main]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -137,9 +138,10 @@ jobs:
|
||||
# lets it start after a failed dependency; the needs on apply-compose are a
|
||||
# barrier, so verification begins only once both applies are done.
|
||||
verify-k8s:
|
||||
needs: [apply-k8s, apply-compose]
|
||||
needs: [preflight, apply-k8s, apply-compose]
|
||||
if: >-
|
||||
always() &&
|
||||
needs.preflight.result == 'success' &&
|
||||
needs.apply-k8s.result != 'skipped' &&
|
||||
needs.apply-compose.result != 'skipped'
|
||||
runs-on: [self-hosted, linux, arch, homelab, prod]
|
||||
@@ -182,8 +184,11 @@ jobs:
|
||||
# suppressing them on a rollback would hide the one run where the answer
|
||||
# matters most.
|
||||
smoke:
|
||||
needs: [verify-k8s]
|
||||
if: always() && needs.verify-k8s.result != 'skipped'
|
||||
needs: [preflight, verify-k8s]
|
||||
if: >-
|
||||
always() &&
|
||||
needs.preflight.result == 'success' &&
|
||||
needs.verify-k8s.result != 'skipped'
|
||||
runs-on: [self-hosted, linux, arch, homelab, prod]
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
|
||||
@@ -120,6 +120,15 @@ install_shellcheck() {
|
||||
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() {
|
||||
if at_version uv "${UV_VERSION}"; then
|
||||
return 0
|
||||
@@ -236,6 +245,7 @@ for tool in "${wanted[@]}"; do
|
||||
case "$tool" in
|
||||
kubeconform) install_kubeconform ;;
|
||||
shellcheck) install_shellcheck ;;
|
||||
jq) install_jq ;;
|
||||
actionlint) install_actionlint ;;
|
||||
prettier) install_prettier ;;
|
||||
ruff) install_ruff ;;
|
||||
|
||||
@@ -2,9 +2,23 @@ name: renovate-ci
|
||||
|
||||
on:
|
||||
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:
|
||||
branches:
|
||||
- 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:
|
||||
|
||||
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
|
||||
# in step with the Dockerfile's node: tag via the "node runtime" group.
|
||||
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.
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
- "traefik.http.routers.adguard-dev.entrypoints=websecure"
|
||||
- "traefik.http.routers.adguard-dev.tls=true"
|
||||
# DoH Router
|
||||
- "traefik.http.routers.dns-over-https.rule=(Host(`dns.forust.xyz` || Host(`adguard.forust.xyz`)) && PathPrefix(`/dns-query`))"
|
||||
- "traefik.http.routers.dns-over-https.rule=(Host(`dns.forust.xyz`) || Host(`adguard.forust.xyz`)) && PathPrefix(`/dns-query`)"
|
||||
- "traefik.http.routers.dns-over-https.entrypoints=websecure"
|
||||
- "traefik.http.routers.dns-over-https.tls.certresolver=letsencrypt"
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: adguard-deployment
|
||||
namespace: adguard
|
||||
spec:
|
||||
@@ -64,8 +66,6 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app: adguard
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
spec:
|
||||
containers:
|
||||
- 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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: authentik-server-deployment
|
||||
namespace: authentik
|
||||
spec:
|
||||
@@ -63,6 +65,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: authentik-worker-deployment
|
||||
namespace: authentik
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: cfddns
|
||||
labels:
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: checkmk-deployment
|
||||
namespace: checkmk
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: cloudflared
|
||||
labels:
|
||||
app: cloudflared
|
||||
@@ -18,7 +20,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: cloudflared
|
||||
image: cloudflare/cloudflared:2026.9.3
|
||||
image: cloudflare/cloudflared:2026.10.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- tunnel
|
||||
|
||||
@@ -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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: convertx-deployment
|
||||
namespace: converters
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: session-keeper
|
||||
namespace: edu-master
|
||||
labels:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: webinar-checker
|
||||
namespace: edu-master
|
||||
labels:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.14-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.14-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: gitea-deployment
|
||||
namespace: gitea
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: glance-deployment
|
||||
namespace: glance
|
||||
spec:
|
||||
@@ -69,7 +71,7 @@ spec:
|
||||
name: glance-config
|
||||
- name: glance-assets
|
||||
configMap:
|
||||
name: glance-config
|
||||
name: glance-assets
|
||||
- name: docker-socket
|
||||
hostPath:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
@@ -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.
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
homarr:
|
||||
container_name: homarr
|
||||
image: ghcr.io/homarr-labs/homarr:v2.1.2
|
||||
image: ghcr.io/homarr-labs/homarr:v2.2.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./appdata:/appdata
|
||||
|
||||
@@ -13,6 +13,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: homarr-deployment
|
||||
namespace: homarr
|
||||
spec:
|
||||
@@ -30,7 +32,7 @@ spec:
|
||||
serviceAccountName: homarr
|
||||
containers:
|
||||
- name: homarr
|
||||
image: ghcr.io/homarr-labs/homarr:v2.1.2
|
||||
image: ghcr.io/homarr-labs/homarr:v2.2.0
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: homarr-config
|
||||
|
||||
@@ -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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: immich-deployment
|
||||
namespace: immich
|
||||
labels:
|
||||
|
||||
@@ -14,6 +14,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: immich-machine-learning-deployment
|
||||
namespace: immich
|
||||
labels:
|
||||
|
||||
@@ -17,6 +17,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: immich-valkey
|
||||
namespace: immich
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: kener-deployment
|
||||
namespace: kener
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: metube-deployment
|
||||
namespace: metube
|
||||
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.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
n8n:
|
||||
image: docker.n8n.io/n8nio/n8n:2.42.3
|
||||
image: docker.n8n.io/n8nio/n8n:2.43.0
|
||||
container_name: n8n
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
|
||||
+3
-1
@@ -13,6 +13,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: n8n-deployment
|
||||
namespace: n8n
|
||||
spec:
|
||||
@@ -29,7 +31,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: n8n
|
||||
image: docker.n8n.io/n8nio/n8n:2.42.3
|
||||
image: docker.n8n.io/n8nio/n8n:2.43.0
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: n8n-config
|
||||
|
||||
+102
-43
@@ -1,64 +1,123 @@
|
||||
# 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
|
||||
in `netbird-pvc`; `k8s/config.yaml` contains the template and runtime renderer.
|
||||
Prepare `netbird-secrets` from `k8s/secrets.yaml.example` before the first start.
|
||||
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.
|
||||
|
||||
## Routing and keys
|
||||
## Files
|
||||
|
||||
The public hostname is set in the ConfigMap and ingress rules. Keep the issuer,
|
||||
dashboard endpoints, and public routes consistent. HTTP, WebSocket, and gRPC
|
||||
traffic go through Traefik; the STUN route uses UDP 3478. A CDN's HTTP proxy does
|
||||
not provide that UDP listener.
|
||||
- `compose.yaml`: dashboard and combined server; selected by the marker-driven deploy workflow through `active`.
|
||||
- `config.template.yaml`: non-secret server configuration rendered at startup.
|
||||
- `entrypoint.sh`: injects Docker secrets into an in-memory runtime configuration.
|
||||
- `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.
|
||||
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.
|
||||
## First deployment
|
||||
|
||||
## 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:
|
||||
`secrets/relay-auth-secret` and `secrets/datastore-encryption-key`.
|
||||
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
|
||||
```bash
|
||||
cd /srv/homelab/netbird
|
||||
./setup.sh
|
||||
$EDITOR .env
|
||||
docker compose config --quiet
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The setup script detects the IPv4 subnet of the external `proxy` network and
|
||||
preserves existing secrets. Complete the initial owner setup through the public
|
||||
TLS endpoint after starting the server.
|
||||
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.
|
||||
|
||||
`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
|
||||
|
||||
`client.compose.yaml` runs a host-network peer in a separate Compose project.
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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
|
||||
key, relay secret, and local configuration. Test a restore on an isolated host.
|
||||
For Compose, the datastore volume has the explicit name `netbird_data`.
|
||||
Do not use `docker compose down -v` when keeping the installation.
|
||||
|
||||
## Inspect
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
kubectl get pods,svc,pvc -n netbird
|
||||
kubectl get events -n netbird --sort-by=.metadata.creationTimestamp
|
||||
```bash
|
||||
cd /srv/homelab/netbird
|
||||
docker compose -f client.compose.yaml config --quiet
|
||||
docker compose -f client.compose.yaml up -d
|
||||
docker compose -f client.compose.yaml exec netbird-client netbird status
|
||||
```
|
||||
|
||||
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.
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
umask 077
|
||||
|
||||
TEMPLATE_PATH=/opt/netbird/config.template.yaml
|
||||
RENDERED_PATH=/run/netbird/config.yaml
|
||||
RELAY_SECRET_PATH=/run/secrets/relay_auth_secret
|
||||
ENCRYPTION_KEY_PATH=/run/secrets/datastore_encryption_key
|
||||
|
||||
is_valid_proxy_subnet() {
|
||||
candidate="$1"
|
||||
case "$candidate" in
|
||||
0.0.0.0/0)
|
||||
return 1
|
||||
;;
|
||||
*/*)
|
||||
address="${candidate%%/*}"
|
||||
prefix="${candidate#*/}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$prefix" in
|
||||
0|[1-9]|[1-2][0-9]|3[0-2]) ;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
old_ifs="$IFS"
|
||||
IFS=.
|
||||
# shellcheck disable=SC2086
|
||||
set -- $address
|
||||
IFS="$old_ifs"
|
||||
[ "$#" -eq 4 ] || return 1
|
||||
|
||||
for octet do
|
||||
case "$octet" in
|
||||
0|[1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]) ;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
read_secret() {
|
||||
secret_path="$1"
|
||||
|
||||
if [ ! -r "$secret_path" ]; then
|
||||
echo "Required secret is not readable: $secret_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
secret_value="$(cat "$secret_path")"
|
||||
if [ -z "$secret_value" ]; then
|
||||
echo "Required secret is empty: $secret_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "$secret_value"
|
||||
}
|
||||
|
||||
if [ -z "${NETBIRD_DOMAIN:-}" ]; then
|
||||
echo "NETBIRD_DOMAIN must be set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$NETBIRD_DOMAIN" in
|
||||
*[!A-Za-z0-9.-]*)
|
||||
echo "NETBIRD_DOMAIN contains unsupported characters" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "${NETBIRD_PROXY_SUBNET:-}" ] || [ "$NETBIRD_PROXY_SUBNET" = "auto" ]; then
|
||||
echo "NETBIRD_PROXY_SUBNET must be an explicit IPv4 CIDR; run netbird/setup.sh first" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! is_valid_proxy_subnet "$NETBIRD_PROXY_SUBNET"; then
|
||||
echo "NETBIRD_PROXY_SUBNET must be a non-default IPv4 CIDR, for example 172.20.0.0/16" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$#" -ne 2 ] || [ "$1" != "--config" ] || [ "$2" != "$RENDERED_PATH" ]; then
|
||||
echo "Expected: --config $RENDERED_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
relay_secret="$(read_secret "$RELAY_SECRET_PATH")"
|
||||
encryption_key="$(read_secret "$ENCRYPTION_KEY_PATH")"
|
||||
|
||||
mkdir -p "$(dirname "$RENDERED_PATH")"
|
||||
sed \
|
||||
-e "s|__NETBIRD_DOMAIN__|${NETBIRD_DOMAIN}|g" \
|
||||
-e "s|__NETBIRD_AUTH_SECRET__|${relay_secret}|g" \
|
||||
-e "s|__NETBIRD_ENCRYPTION_KEY__|${encryption_key}|g" \
|
||||
-e "s|__NETBIRD_PROXY_SUBNET__|${NETBIRD_PROXY_SUBNET}|g" \
|
||||
"$TEMPLATE_PATH" >"$RENDERED_PATH"
|
||||
|
||||
if grep -q '__NETBIRD_' "$RENDERED_PATH"; then
|
||||
echo "Rendered NetBird configuration still contains unresolved placeholders" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec /go/bin/netbird-server "$@"
|
||||
@@ -32,6 +32,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: netbird-server-deployment
|
||||
namespace: netbird
|
||||
spec:
|
||||
@@ -126,6 +128,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: netbird-dashboard-deployment
|
||||
namespace: netbird
|
||||
spec:
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare local Compose configuration without replacing existing credentials.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
umask 077
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
fi
|
||||
|
||||
if grep -q '^NETBIRD_PROXY_SUBNET=auto$' .env; then
|
||||
subnet="$(docker network inspect proxy --format '{{range .IPAM.Config}}{{println .Subnet}}{{end}}' | awk '/^[0-9]+\./ { print; exit }')"
|
||||
if [ -z "$subnet" ]; then
|
||||
echo "No IPv4 subnet found on the Docker proxy network. Set NETBIRD_PROXY_SUBNET in .env." >&2
|
||||
exit 1
|
||||
fi
|
||||
# The detected value must be safe to substitute into the env file.
|
||||
if [[ ! "$subnet" =~ ^[0-9.]+/[0-9]+$ ]]; then
|
||||
echo "Unexpected Docker network subnet: $subnet" >&2
|
||||
exit 1
|
||||
fi
|
||||
sed -i "s|^NETBIRD_PROXY_SUBNET=auto$|NETBIRD_PROXY_SUBNET=$subnet|" .env
|
||||
fi
|
||||
|
||||
mkdir -p secrets
|
||||
chmod 700 secrets
|
||||
for name in relay-auth-secret datastore-encryption-key; do
|
||||
path="secrets/$name"
|
||||
if [ -e "$path" ]; then
|
||||
if [ ! -s "$path" ]; then
|
||||
echo "Existing secret is empty: $path. Restore it before continuing." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
openssl rand -base64 32 >"$path"
|
||||
fi
|
||||
chmod 600 "$path"
|
||||
done
|
||||
printf '%s\n' 'Local files are ready. Review .env, then run docker compose config --quiet.'
|
||||
+88
-35
@@ -1,43 +1,96 @@
|
||||
# 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
|
||||
`postgres.database.svc.cluster.local:5432`, database and role `netbox`.
|
||||
The database and application Secrets must contain the same password.
|
||||
Media, reports, scripts, and Valkey have persistent storage.
|
||||
| Runtime | Manifest | Purpose |
|
||||
| ------- | -------------- | -------------------------------------------------------------- |
|
||||
| Docker | `compose.yaml` | Local stand on `127.0.0.1:8000` (no public exposure) |
|
||||
| k8s | `k8s/` | Homelab service on `netbox.forust.xyz` (and the internal name) |
|
||||
|
||||
Compose has its own PostgreSQL container and Valkey instances. It publishes the
|
||||
web UI on `127.0.0.1:8000`; its Traefik labels can also expose it while a Docker
|
||||
proxy is running. Copy `.env.example` to `.env`, replace the credentials, and run
|
||||
`docker compose config --quiet` before starting it.
|
||||
Both use the same image (`netboxcommunity/netbox:v4.7-5.1.1`) and Valkey for tasks
|
||||
plus a second logical database for caching. The Docker stand keeps its own
|
||||
PostgreSQL container, while the k8s deployment uses the shared `database` cluster
|
||||
(`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
|
||||
shared database initializer on a fresh instance, or create the role and database
|
||||
manually on an existing instance; see [PostgreSQL](../postgres/README.md).
|
||||
The database NetworkPolicy already includes `netbox`.
|
||||
|
||||
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
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# replace CHANGE_ME
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: netbox-deployment
|
||||
namespace: netbox
|
||||
labels:
|
||||
@@ -118,6 +120,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: netbox-worker-deployment
|
||||
namespace: netbox
|
||||
labels:
|
||||
|
||||
@@ -17,6 +17,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: netbox-valkey
|
||||
namespace: netbox
|
||||
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.
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
netronome:
|
||||
image: ghcr.io/autobrr/netronome:v0.15.0
|
||||
image: ghcr.io/autobrr/netronome:v0.16.0
|
||||
restart: unless-stopped
|
||||
container_name: netronome
|
||||
ports:
|
||||
|
||||
@@ -14,6 +14,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: netronome-deployment
|
||||
namespace: netronome
|
||||
labels:
|
||||
@@ -32,7 +34,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: netronome
|
||||
image: ghcr.io/autobrr/netronome:v0.15.0
|
||||
image: ghcr.io/autobrr/netronome:v0.16.0
|
||||
ports:
|
||||
- name: netronome-port
|
||||
protocol: TCP
|
||||
|
||||
@@ -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.
|
||||
@@ -1,6 +1,7 @@
|
||||
POSTGRES_ADMIN_PASSWORD=
|
||||
AUTHENTIK_DB_PASSWORD=
|
||||
GITEA_DB_PASSWORD=
|
||||
NETBOX_DB_PASSWORD=
|
||||
NETRONOME_DB_PASSWORD=
|
||||
PENPOT_DB_PASSWORD=
|
||||
STATUSPAGE_DB_PASSWORD=
|
||||
+28
-56
@@ -1,63 +1,35 @@
|
||||
# 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
|
||||
`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.
|
||||
## Compatibility baseline
|
||||
|
||||
## 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
|
||||
directory. The Kubernetes copy is embedded in `k8s/postgres.yaml`.
|
||||
It also provisions Penpot and Statuspage roles, even though those are not active
|
||||
consumers in the current Kubernetes manifests.
|
||||
A major-version change must use a logical dump/restore; changing only the
|
||||
image tag while keeping a data directory is not supported.
|
||||
|
||||
The initializer requires every listed password. Prepare `k8s/secrets.yaml` from
|
||||
the example before applying the StatefulSet. Existing application Secrets keep
|
||||
copies of their own database passwords; they must match the corresponding role.
|
||||
For Compose, copy `.env.example` to `.env`, set all passwords, and start it with
|
||||
`docker compose -f shared-compose.yaml up -d`. This file is intentionally not
|
||||
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.
|
||||
Changing a Secret does not rotate the PostgreSQL role password. Rotate the role
|
||||
with SQL and update the application Secret together.
|
||||
|
||||
## Compose alternative
|
||||
|
||||
From this directory:
|
||||
|
||||
```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.
|
||||
For Kubernetes, create `k8s/secrets.yaml` from the example before applying the
|
||||
manifests. The `k8s/active` marker makes the normal deploy workflow include the
|
||||
namespace, StatefulSet, ConfigMap, and NetworkPolicy. Applications use
|
||||
`postgres.database.svc.cluster.local:5432`.
|
||||
Migrate each existing database with a tested logical dump/restore before
|
||||
switching an application. Do not reuse a PostgreSQL 14 or 17 data directory
|
||||
with PostgreSQL 15.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,17 @@
|
||||
# VictoriaMetrics
|
||||
|
||||
The `victoria-operator` Helm release converts Prometheus Operator
|
||||
`ServiceMonitor` resources into owned `VMServiceScrape` resources. The
|
||||
`VMAgent` selects converted scrapes labeled `release: prometheus-stack` in all
|
||||
namespaces and writes them to the existing single-node VictoriaMetrics
|
||||
instance. Changes to selected `ServiceMonitor` resources are reconciled
|
||||
automatically; there is no copied Prometheus scrape-config blob to regenerate.
|
||||
|
||||
The agent drops targets for the Prometheus server service to avoid duplicating
|
||||
its self-scrape. `scraper: victoria` identifies the samples ingested by this
|
||||
VMAgent.
|
||||
|
||||
The VictoriaMetrics Operator chart and its CRDs are installed before the
|
||||
Kubernetes manifests by the normal deploy workflow. On a cluster where the
|
||||
operator CRDs are not installed yet, CI skips the server-side dry-run of the
|
||||
`VMAgent` resource; the deploy installs the chart before applying that resource.
|
||||
@@ -38,6 +38,8 @@ grafana:
|
||||
|
||||
# One block covers both the dashboards and datasources sidecars (p95 91M / 80M).
|
||||
sidecar:
|
||||
datasources:
|
||||
defaultDatasourceEnabled: false
|
||||
resources:
|
||||
requests:
|
||||
memory: "96Mi"
|
||||
@@ -50,9 +52,18 @@ grafana:
|
||||
type: loki
|
||||
url: http://loki-gateway.prometheus.svc.cluster.local
|
||||
access: proxy
|
||||
- name: VictoriaMetrics
|
||||
type: prometheus
|
||||
url: http://victoria-metrics.prometheus.svc.cluster.local:8428
|
||||
access: proxy
|
||||
isDefault: true
|
||||
|
||||
prometheus:
|
||||
prometheusSpec:
|
||||
# VM trial: vmagent scrapes and remote-writes to VictoriaMetrics, so the
|
||||
# Prometheus server itself stands down. Encoded here (not a kubectl patch)
|
||||
# so helm keeps owning spec.replicas and upgrades do not conflict on it.
|
||||
replicas: 0
|
||||
retention: 60d
|
||||
retentionSize: 32GB
|
||||
storageSpec:
|
||||
|
||||
@@ -31,3 +31,105 @@ spec:
|
||||
port: 80
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: prometheus-local
|
||||
namespace: prometheus
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`prom.workstation.internal`) || Host(`prom.gigaforust.internal`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: prometheus-stack-kube-prom-prometheus
|
||||
port: 9090
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: alertmanager-local
|
||||
namespace: prometheus
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`am.workstation.internal`) || Host(`am.gigaforust.internal`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: prometheus-stack-kube-prom-alertmanager
|
||||
port: 9093
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: loki-local
|
||||
namespace: prometheus
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`loki.workstation.internal`) || Host(`loki.gigaforust.internal`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: loki-gateway
|
||||
port: 80
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: alloy-local
|
||||
namespace: prometheus
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`alloy.workstation.internal`) || Host(`alloy.gigaforust.internal`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: alloy
|
||||
port: 12345
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: victoria-local
|
||||
namespace: prometheus
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`victoria.workstation.internal`) || Host(`victoria.gigaforust.internal`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: victoria-metrics
|
||||
port: 8428
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: vmalert-local
|
||||
namespace: prometheus
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`vmalert.workstation.internal`) || Host(`vmalert.gigaforust.internal`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: vmalert
|
||||
port: 8880
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
@@ -0,0 +1,12 @@
|
||||
nameOverride: victoria-operator
|
||||
|
||||
operator:
|
||||
enable_converter_ownership: true
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
@@ -0,0 +1,79 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: victoria-metrics
|
||||
namespace: prometheus
|
||||
spec:
|
||||
selector:
|
||||
app: victoria-metrics
|
||||
ports:
|
||||
- port: 8428
|
||||
targetPort: 8428
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: victoria-pvc
|
||||
namespace: prometheus
|
||||
spec:
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: victoria-deployment
|
||||
namespace: prometheus
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: victoria-metrics
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: victoria-metrics
|
||||
spec:
|
||||
containers:
|
||||
- name: victoria
|
||||
image: victoriametrics/victoria-metrics:v1.153.0-scratch
|
||||
args:
|
||||
- -storageDataPath=/vmdata
|
||||
- -retentionPeriod=30d
|
||||
- -httpListenAddr=:8428
|
||||
ports:
|
||||
- containerPort: 8428
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8428
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8428
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: vmdata
|
||||
mountPath: /vmdata
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "256Mi"
|
||||
limits:
|
||||
cpu: "1000m"
|
||||
memory: "1Gi"
|
||||
volumes:
|
||||
- name: vmdata
|
||||
persistentVolumeClaim:
|
||||
claimName: victoria-pvc
|
||||
@@ -0,0 +1,29 @@
|
||||
apiVersion: operator.victoriametrics.com/v1beta1
|
||||
kind: VMAgent
|
||||
metadata:
|
||||
name: vmagent
|
||||
namespace: prometheus
|
||||
spec:
|
||||
image:
|
||||
tag: v1.153.0
|
||||
scrapeInterval: 30s
|
||||
externalLabels:
|
||||
scraper: victoria
|
||||
serviceScrapeNamespaceSelector: {}
|
||||
serviceScrapeSelector:
|
||||
matchLabels:
|
||||
release: prometheus-stack
|
||||
globalScrapeRelabelConfigs:
|
||||
- action: drop
|
||||
source_labels:
|
||||
- __meta_kubernetes_service_name
|
||||
regex: prometheus-stack-kube-prom-prometheus
|
||||
remoteWrite:
|
||||
- url: http://victoria-metrics.prometheus.svc.cluster.local:8428/api/v1/write
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1000m"
|
||||
memory: 1Gi
|
||||
@@ -0,0 +1,70 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: vmalert
|
||||
namespace: prometheus
|
||||
spec:
|
||||
selector:
|
||||
app: vmalert
|
||||
ports:
|
||||
- port: 8880
|
||||
targetPort: 8880
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: vmalert-deployment
|
||||
namespace: prometheus
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: vmalert
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: vmalert
|
||||
spec:
|
||||
containers:
|
||||
- name: vmalert
|
||||
image: victoriametrics/vmalert:v1.153.0
|
||||
args:
|
||||
- -datasource.url=http://victoria-metrics.prometheus.svc.cluster.local:8428
|
||||
- -remoteWrite.url=http://victoria-metrics.prometheus.svc.cluster.local:8428
|
||||
- -notifier.url=http://prometheus-stack-kube-prom-alertmanager.prometheus.svc.cluster.local:9093
|
||||
- -rule=/etc/vm/rules/*.yaml
|
||||
- -evaluationInterval=60s
|
||||
- -httpListenAddr=:8880
|
||||
ports:
|
||||
- containerPort: 8880
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /metrics
|
||||
port: 8880
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /metrics
|
||||
port: 8880
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: rules
|
||||
mountPath: /etc/vm/rules
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
memory: "64Mi"
|
||||
limits:
|
||||
cpu: "200m"
|
||||
memory: "256Mi"
|
||||
volumes:
|
||||
- name: rules
|
||||
configMap:
|
||||
name: prometheus-prometheus-stack-kube-prom-prometheus-rulefiles-0
|
||||
@@ -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).
|
||||
# Deployed by the deploy workflow, namespace reloader.
|
||||
# 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.
|
||||
|
||||
reloader:
|
||||
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:
|
||||
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
|
||||
CronJob executions forbidden. Prepare the bot PAT from the Secret example.
|
||||
Give the dedicated Gitea user access to the repositories it should update.
|
||||
## Kubernetes
|
||||
|
||||
`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
|
||||
.gitea/workflows/sync-renovate-configmap.sh
|
||||
.gitea/workflows/sync-renovate-configmap.sh --check
|
||||
cp renovate/k8s/secrets.yaml.example renovate/k8s/secrets.yaml
|
||||
$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
|
||||
that the generated configuration agrees with the source.
|
||||
The `renovate/k8s/active` marker makes the normal deployment workflow include
|
||||
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
|
||||
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
|
||||
from the CronJob and accepts repository, log-level, and dry-run inputs. Actions
|
||||
requires `RENOVATE_TOKEN`; `RENOVATE_GITHUB_COM_TOKEN` is optional.
|
||||
The Actions concurrency group and the CronJob policy are separate, so avoid
|
||||
starting both against the same repository at once.
|
||||
or the `renovate-run` Actions workflow (Actions tab → `renovate-run` →
|
||||
Run workflow). It runs the same image as the CronJob on the self-hosted runner
|
||||
via Docker — the tag is read out of `renovate/k8s/cronjob.yaml` at run time
|
||||
rather than hardcoded, so the two cannot drift apart. Required Actions secrets
|
||||
(repo or org settings):
|
||||
|
||||
For Compose, copy `.env.example` to `.env` in this directory and run
|
||||
`docker compose -f renovate-compose.yaml run --rm renovate`. That file is a
|
||||
manual entry point and is not selected by the deploy workflow.
|
||||
- `RENOVATE_TOKEN` — renovate-bot PAT (repository + issue read/write).
|
||||
- `RENOVATE_GITHUB_COM_TOKEN` — optional, for changelogs and GitHub rate limits.
|
||||
|
||||
The config also tracks chart versions in `deploy-lib.sh` and tool versions in
|
||||
`tool-versions.env`. Renovate opens pull requests; the normal CI and deploy
|
||||
workflows handle changes after merge.
|
||||
Inputs: `repositories` (default `forust/homelab`), `log_level`
|
||||
(`info`/`debug`). Only one run at a time (concurrency group
|
||||
`renovate-run`), same as the CronJob `Forbid` policy.
|
||||
|
||||
## Inspect
|
||||
|
||||
From the repository root:
|
||||
Inspect runs with:
|
||||
|
||||
```sh
|
||||
kubectl get pods,svc,pvc -n renovate
|
||||
kubectl get events -n renovate --sort-by=.metadata.creationTimestamp
|
||||
kubectl get cronjob,jobs,pods -n renovate
|
||||
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.
|
||||
+48
-25
@@ -19,6 +19,9 @@ data:
|
||||
"dependencyDashboard": true,
|
||||
"prCreation": "immediate",
|
||||
"labels": ["dependencies", "automated"],
|
||||
"docker-compose": {
|
||||
"managerFilePatterns": ["renovate/renovate-compose.yaml"]
|
||||
},
|
||||
"helm-values": {
|
||||
"managerFilePatterns": ["/k8s/.+values\\.ya?ml$/"]
|
||||
},
|
||||
@@ -29,7 +32,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "singlesource: playwright npm version pinned in npx command (k8s + compose)",
|
||||
"managerFilePatterns": ["^edu_master/k8s/playwright\\.yaml$", "^edu_master/compose\\.yaml$"],
|
||||
"managerFilePatterns": ["edu_master/k8s/playwright.yaml", "edu_master/compose.yaml"],
|
||||
"matchStrings": ["playwright@(?<currentValue>\\d+\\.\\d+\\.\\d+)"],
|
||||
"datasourceTemplate": "npm",
|
||||
"depNameTemplate": "playwright"
|
||||
@@ -37,24 +40,42 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "singlesource: PLAYWRIGHT_VERSION file",
|
||||
"managerFilePatterns": ["^edu_master/PLAYWRIGHT_VERSION$"],
|
||||
"matchStrings": ["^(?<currentValue>\\d+\\.\\d+\\.\\d+)$"],
|
||||
"managerFilePatterns": ["edu_master/PLAYWRIGHT_VERSION"],
|
||||
"matchStrings": ["^(?<currentValue>\\d+\\.\\d+\\.\\d+)(?:\\r?\\n)?$"],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "playwright"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "singlesource: playwright Python client version pinned in Dockerfile ARG",
|
||||
"managerFilePatterns": ["edu_master/webinar-checker/Dockerfile"],
|
||||
"matchStrings": ["(?:^|\\n)ARG PLAYWRIGHT_VERSION=(?<currentValue>\\d+\\.\\d+\\.\\d+)(?:\\r?\\n|$)"],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "playwright",
|
||||
"versioningTemplate": "pep440"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "kube-prometheus-stack chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|prometheus-community/kube-prometheus-stack\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "kube-prometheus-stack",
|
||||
"registryUrlTemplate": "https://prometheus-community.github.io/helm-charts"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "VictoriaMetrics Operator chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|victoriametrics/victoria-metrics-operator\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "victoria-metrics-operator",
|
||||
"registryUrlTemplate": "https://victoriametrics.github.io/helm-charts"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "grafana/loki chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|grafana/loki\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "loki",
|
||||
@@ -63,7 +84,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "grafana/alloy chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|grafana/alloy\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "alloy",
|
||||
@@ -72,7 +93,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "actionlint version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)ACTIONLINT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "rhysd/actionlint"
|
||||
@@ -80,7 +101,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "shellcheck version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)SHELLCHECK_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "koalaman/shellcheck"
|
||||
@@ -88,7 +109,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "kubeconform version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)KUBECONFORM_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "yannh/kubeconform"
|
||||
@@ -96,7 +117,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "uv version used to build the pytest venv",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)UV_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "astral-sh/uv"
|
||||
@@ -104,7 +125,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "prettier version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)PRETTIER_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "npm",
|
||||
"depNameTemplate": "prettier"
|
||||
@@ -112,7 +133,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "ruff version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)RUFF_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "ruff"
|
||||
@@ -120,7 +141,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "pip-audit version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)PIP_AUDIT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "pip-audit"
|
||||
@@ -128,7 +149,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "yamllint version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)YAMLLINT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "yamllint"
|
||||
@@ -136,7 +157,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "hadolint version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)HADOLINT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "hadolint/hadolint"
|
||||
@@ -144,7 +165,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "node version the ci workflow runs npm with",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)NODE_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "node",
|
||||
"depNameTemplate": "node"
|
||||
@@ -152,7 +173,7 @@ data:
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "stakater/reloader chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|stakater/reloader\\|reloader\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "reloader",
|
||||
@@ -161,7 +182,7 @@ data:
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Automerge digest and patch updates - safe by definition, review adds nothing, keeps the renovate queue and the deploy line short. Specific no-automerge rules below still override this for playwright, helm and majors.",
|
||||
"description": "Automerge ordinary digest and patch updates after successful checks; specific manual-review rules below override this.",
|
||||
"matchUpdateTypes": ["digest", "patch"],
|
||||
"automerge": true
|
||||
},
|
||||
@@ -172,6 +193,12 @@ data:
|
||||
"groupSlug": "all-minor",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Group ordinary patch updates; the specific groups and manual-review rules below take precedence",
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"groupName": "all patch updates",
|
||||
"groupSlug": "all-patch"
|
||||
},
|
||||
{
|
||||
"description": "Keep private homelab images unchanged",
|
||||
"matchDatasources": ["docker"],
|
||||
@@ -196,7 +223,7 @@ data:
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "CI runs npm on the node the panel image is built from - the NODE_VERSION pin in tool-versions.env and node:22-alpine in the Dockerfile are the same dependency and move as one",
|
||||
"description": "Keep CI Node runtime updates in a separate, manually reviewed group",
|
||||
"matchPackageNames": ["node"],
|
||||
"groupName": "node runtime",
|
||||
"groupSlug": "node",
|
||||
@@ -205,6 +232,8 @@ data:
|
||||
{
|
||||
"description": "Helm chart bumps change PVC fields and admission behaviour, keep them reviewable",
|
||||
"matchDatasources": ["helm"],
|
||||
"groupName": "Helm chart {{depName}}",
|
||||
"groupSlug": "helm-{{depName}}",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
@@ -213,12 +242,6 @@ data:
|
||||
"dependencyDashboardApproval": true,
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Group patch updates from all sources - automerge still applies via the digest/patch rule above (helm/playwright stay manual via their own rules)",
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"groupName": "all patch updates",
|
||||
"groupSlug": "all-patch"
|
||||
},
|
||||
{
|
||||
"description": "Python Y-bumps break compat (3.11->3.12->3.13->3.14) - keep the base image out of the shared minor/patch groups, review every bump separately. Placed last so its groupName wins.",
|
||||
"matchDatasources": ["docker"],
|
||||
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: renovate
|
||||
image: renovate/renovate:44.136.0
|
||||
image: renovate/renovate:44.140.0
|
||||
env:
|
||||
- name: RENOVATE_PLATFORM
|
||||
value: gitea
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
renovate:
|
||||
# Kept in step with renovate/k8s/cronjob.yaml by the "renovate self-update"
|
||||
# package rule in renovate/renovate.json.
|
||||
image: renovate/renovate:44.115.9
|
||||
image: renovate/renovate:44.136.0
|
||||
container_name: renovate
|
||||
restart: "no"
|
||||
env_file:
|
||||
|
||||
+48
-25
@@ -8,6 +8,9 @@
|
||||
"dependencyDashboard": true,
|
||||
"prCreation": "immediate",
|
||||
"labels": ["dependencies", "automated"],
|
||||
"docker-compose": {
|
||||
"managerFilePatterns": ["renovate/renovate-compose.yaml"]
|
||||
},
|
||||
"helm-values": {
|
||||
"managerFilePatterns": ["/k8s/.+values\\.ya?ml$/"]
|
||||
},
|
||||
@@ -18,7 +21,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "singlesource: playwright npm version pinned in npx command (k8s + compose)",
|
||||
"managerFilePatterns": ["^edu_master/k8s/playwright\\.yaml$", "^edu_master/compose\\.yaml$"],
|
||||
"managerFilePatterns": ["edu_master/k8s/playwright.yaml", "edu_master/compose.yaml"],
|
||||
"matchStrings": ["playwright@(?<currentValue>\\d+\\.\\d+\\.\\d+)"],
|
||||
"datasourceTemplate": "npm",
|
||||
"depNameTemplate": "playwright"
|
||||
@@ -26,24 +29,42 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "singlesource: PLAYWRIGHT_VERSION file",
|
||||
"managerFilePatterns": ["^edu_master/PLAYWRIGHT_VERSION$"],
|
||||
"matchStrings": ["^(?<currentValue>\\d+\\.\\d+\\.\\d+)$"],
|
||||
"managerFilePatterns": ["edu_master/PLAYWRIGHT_VERSION"],
|
||||
"matchStrings": ["^(?<currentValue>\\d+\\.\\d+\\.\\d+)(?:\\r?\\n)?$"],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "playwright"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "singlesource: playwright Python client version pinned in Dockerfile ARG",
|
||||
"managerFilePatterns": ["edu_master/webinar-checker/Dockerfile"],
|
||||
"matchStrings": ["(?:^|\\n)ARG PLAYWRIGHT_VERSION=(?<currentValue>\\d+\\.\\d+\\.\\d+)(?:\\r?\\n|$)"],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "playwright",
|
||||
"versioningTemplate": "pep440"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "kube-prometheus-stack chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|prometheus-community/kube-prometheus-stack\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "kube-prometheus-stack",
|
||||
"registryUrlTemplate": "https://prometheus-community.github.io/helm-charts"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "VictoriaMetrics Operator chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|victoriametrics/victoria-metrics-operator\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "victoria-metrics-operator",
|
||||
"registryUrlTemplate": "https://victoriametrics.github.io/helm-charts"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "grafana/loki chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|grafana/loki\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "loki",
|
||||
@@ -52,7 +73,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "grafana/alloy chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|grafana/alloy\\|prometheus\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "alloy",
|
||||
@@ -61,7 +82,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "actionlint version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)ACTIONLINT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "rhysd/actionlint"
|
||||
@@ -69,7 +90,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "shellcheck version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)SHELLCHECK_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "koalaman/shellcheck"
|
||||
@@ -77,7 +98,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "kubeconform version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)KUBECONFORM_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "yannh/kubeconform"
|
||||
@@ -85,7 +106,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "uv version used to build the pytest venv",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)UV_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "astral-sh/uv"
|
||||
@@ -93,7 +114,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "prettier version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)PRETTIER_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "npm",
|
||||
"depNameTemplate": "prettier"
|
||||
@@ -101,7 +122,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "ruff version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)RUFF_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "ruff"
|
||||
@@ -109,7 +130,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "pip-audit version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)PIP_AUDIT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "pip-audit"
|
||||
@@ -117,7 +138,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "yamllint version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)YAMLLINT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "pypi",
|
||||
"depNameTemplate": "yamllint"
|
||||
@@ -125,7 +146,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "hadolint version used by the ci workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)HADOLINT_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"depNameTemplate": "hadolint/hadolint"
|
||||
@@ -133,7 +154,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "node version the ci workflow runs npm with",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/tool-versions\\.env$"],
|
||||
"managerFilePatterns": [".gitea/workflows/tool-versions.env"],
|
||||
"matchStrings": ["(?:^|\\n)NODE_VERSION=\"(?<currentValue>[0-9.]+)\""],
|
||||
"datasourceTemplate": "node",
|
||||
"depNameTemplate": "node"
|
||||
@@ -141,7 +162,7 @@
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "stakater/reloader chart version pinned in the deploy workflow",
|
||||
"managerFilePatterns": ["^\\.gitea/workflows/deploy-lib\\.sh$"],
|
||||
"managerFilePatterns": [".gitea/workflows/deploy-lib.sh"],
|
||||
"matchStrings": ["\\|stakater/reloader\\|reloader\\|(?<currentValue>[0-9.]+)\\|"],
|
||||
"datasourceTemplate": "helm",
|
||||
"depNameTemplate": "reloader",
|
||||
@@ -150,7 +171,7 @@
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Automerge digest and patch updates - safe by definition, review adds nothing, keeps the renovate queue and the deploy line short. Specific no-automerge rules below still override this for playwright, helm and majors.",
|
||||
"description": "Automerge ordinary digest and patch updates after successful checks; specific manual-review rules below override this.",
|
||||
"matchUpdateTypes": ["digest", "patch"],
|
||||
"automerge": true
|
||||
},
|
||||
@@ -161,6 +182,12 @@
|
||||
"groupSlug": "all-minor",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Group ordinary patch updates; the specific groups and manual-review rules below take precedence",
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"groupName": "all patch updates",
|
||||
"groupSlug": "all-patch"
|
||||
},
|
||||
{
|
||||
"description": "Keep private homelab images unchanged",
|
||||
"matchDatasources": ["docker"],
|
||||
@@ -185,7 +212,7 @@
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "CI runs npm on the node the panel image is built from - the NODE_VERSION pin in tool-versions.env and node:22-alpine in the Dockerfile are the same dependency and move as one",
|
||||
"description": "Keep CI Node runtime updates in a separate, manually reviewed group",
|
||||
"matchPackageNames": ["node"],
|
||||
"groupName": "node runtime",
|
||||
"groupSlug": "node",
|
||||
@@ -194,6 +221,8 @@
|
||||
{
|
||||
"description": "Helm chart bumps change PVC fields and admission behaviour, keep them reviewable",
|
||||
"matchDatasources": ["helm"],
|
||||
"groupName": "Helm chart {{depName}}",
|
||||
"groupSlug": "helm-{{depName}}",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
@@ -202,12 +231,6 @@
|
||||
"dependencyDashboardApproval": true,
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Group patch updates from all sources - automerge still applies via the digest/patch rule above (helm/playwright stay manual via their own rules)",
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"groupName": "all patch updates",
|
||||
"groupSlug": "all-patch"
|
||||
},
|
||||
{
|
||||
"description": "Python Y-bumps break compat (3.11->3.12->3.13->3.14) - keep the base image out of the shared minor/patch groups, review every bump separately. Placed last so its groupName wins.",
|
||||
"matchDatasources": ["docker"],
|
||||
|
||||
@@ -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.
|
||||
@@ -12,11 +12,11 @@ services:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.searxng.loadbalancer.server.port=8080"
|
||||
# Prod Router
|
||||
- "traefik.http.routers.searxng.rule=Host(`s.forust.xyz` || `search.forust.xyz`)"
|
||||
- "traefik.http.routers.searxng.rule=Host(`s.forust.xyz`) || Host(`search.forust.xyz`)"
|
||||
- "traefik.http.routers.searxng.entrypoints=websecure"
|
||||
- "traefik.http.routers.searxng.tls.certresolver=letsencrypt"
|
||||
# Local Router
|
||||
- "traefik.http.routers.searxng-local.rule=Host(`s.workstation.internal` || `searxng.workstation.internal`)"
|
||||
- "traefik.http.routers.searxng-local.rule=Host(`s.workstation.internal`) || Host(`searxng.workstation.internal`)"
|
||||
- "traefik.http.routers.searxng-local.entrypoints=websecure"
|
||||
- "traefik.http.routers.searxng-local.tls=true"
|
||||
# Dev Router
|
||||
|
||||
@@ -13,6 +13,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: searxng-deployment
|
||||
namespace: searxng
|
||||
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.
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
- streaming
|
||||
|
||||
qbittorrent:
|
||||
image: lscr.io/linuxserver/qbittorrent:5.2.4
|
||||
image: lscr.io/linuxserver/qbittorrent:20.04.1
|
||||
container_name: qbittorrent
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
|
||||
@@ -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.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:2.9.1
|
||||
image: ghcr.io/lukegus/termix:2.9.2
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
# ports:
|
||||
|
||||
@@ -13,6 +13,8 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
name: termix-deployment
|
||||
namespace: termix
|
||||
spec:
|
||||
@@ -29,7 +31,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: termix
|
||||
image: ghcr.io/lukegus/termix:2.9.1
|
||||
image: ghcr.io/lukegus/termix:2.9.2
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: termix-config
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Exercise local setup and config rendering without a Docker daemon."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class NetbirdRuntimeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.stack = self.root / 'netbird'
|
||||
self.stack.mkdir()
|
||||
for name in ('setup.sh', '.env.example', 'config.template.yaml'):
|
||||
shutil.copy(ROOT / 'netbird' / name, self.stack / name)
|
||||
binary = self.root / 'bin'
|
||||
binary.mkdir()
|
||||
docker = binary / 'docker'
|
||||
docker.write_text('#!/bin/sh\nprintf "%s\\n" 172.20.0.0/16\n')
|
||||
docker.chmod(0o755)
|
||||
self.env = dict(os.environ, PATH=f'{binary}:{os.environ["PATH"]}')
|
||||
|
||||
def setup(self):
|
||||
return subprocess.run( # noqa: S603 - executes the repository script copied into this test's temp dir
|
||||
['/bin/bash', str(self.stack / 'setup.sh')], env=self.env, capture_output=True, check=False
|
||||
)
|
||||
|
||||
def test_setup_preserves_existing_secrets_and_env(self):
|
||||
self.assertEqual(self.setup().returncode, 0)
|
||||
paths = [self.stack / '.env', *sorted((self.stack / 'secrets').iterdir())]
|
||||
before = [p.read_bytes() for p in paths]
|
||||
self.assertIn(b'NETBIRD_PROXY_SUBNET=172.20.0.0/16', before[0])
|
||||
self.assertEqual(self.setup().returncode, 0)
|
||||
self.assertEqual(before, [p.read_bytes() for p in paths])
|
||||
for p in paths[1:]:
|
||||
self.assertEqual(p.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_setup_rejects_empty_existing_secret(self):
|
||||
(self.stack / 'secrets').mkdir()
|
||||
secret = self.stack / 'secrets/datastore-encryption-key'
|
||||
secret.touch()
|
||||
self.assertNotEqual(self.setup().returncode, 0)
|
||||
self.assertEqual(secret.read_bytes(), b'')
|
||||
|
||||
def render(self, subnet):
|
||||
self.assertEqual(self.setup().returncode, 0)
|
||||
rendered = self.root / 'run/config.yaml'
|
||||
script = (ROOT / 'netbird/entrypoint.sh').read_text()
|
||||
replacements = {
|
||||
'/opt/netbird/config.template.yaml': str(self.stack / 'config.template.yaml'),
|
||||
'/run/netbird/config.yaml': str(rendered),
|
||||
'/run/secrets/relay_auth_secret': str(self.stack / 'secrets/relay-auth-secret'),
|
||||
'/run/secrets/datastore_encryption_key': str(self.stack / 'secrets/datastore-encryption-key'),
|
||||
'/go/bin/netbird-server': '/bin/true',
|
||||
}
|
||||
for original, local in replacements.items():
|
||||
script = script.replace(original, local)
|
||||
result = subprocess.run( # noqa: S603 - repository renderer, with test-local paths
|
||||
['/bin/sh', '-c', script, 'entrypoint', '--config', str(rendered)],
|
||||
env=dict(self.env, NETBIRD_DOMAIN='nb.example.com', NETBIRD_PROXY_SUBNET=subnet),
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
return result, rendered
|
||||
|
||||
def test_renderer_replaces_placeholders_and_restricts_file_permissions(self):
|
||||
result, rendered = self.render('172.20.0.0/16')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertNotIn('__NETBIRD_', rendered.read_text())
|
||||
self.assertIn('nb.example.com', rendered.read_text())
|
||||
self.assertEqual(rendered.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_renderer_rejects_auto_and_default_route(self):
|
||||
for subnet in ('auto', '0.0.0.0/0', '999.1.1.1/24'):
|
||||
with self.subTest(subnet=subnet):
|
||||
result, _ = self.render(subnet)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -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.
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user