Compare commits

..
Author SHA1 Message Date
forust a2ff9515a3 fix(deploy): validate compose without workstation secrets
renovate-ci / validate-renovate (push) Skipped
deploy / validate (push) Skipped
ci / lint-prettier (push) Successful in 2s
ci / validate (push) Successful in 2s
ci / build (push) Skipped
ci / lint-ruff (push) Successful in 1s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
docker compose config required real values for gitignored .env files and secrets, so validate always failed on stacks with :? guards (netbird, netbox). Validate structure only via --no-interpolate, --no-env-resolution and --no-path-resolution, keeping normalization and consistency checks.
2026-09-26 17:00:43 +02:00
forust 27ab6b859e chore(deploy): enable netbird
deploy / validate (push) Skipped
renovate-ci / validate-renovate (push) Skipped
ci / lint-prettier (pull_request) Successful in 3s
ci / lint-ruff (pull_request) Successful in 1s
ci / validate (pull_request) Successful in 1s
ci / build (pull_request) Skipped
renovate-ci / validate-renovate (pull_request) Successful in 13s
ci / lint-yaml (pull_request) Successful in 2s
ci / lint-dockerfiles (pull_request) Successful in 0s
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 1s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 0s
ci / validate (push) Successful in 2s
ci / build (push) Skipped
2026-09-26 00:04:50 +02:00
forust a4a4bb4cc5 feat(netbird): add tailscale-alternative
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / validate (push) Successful in 2s
ci / build (push) Skipped
deploy / validate (push) Skipped
renovate-ci / validate-renovate (push) Skipped
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 1s
ci / lint-prettier (pull_request) Successful in 3s
ci / lint-ruff (pull_request) Successful in 1s
ci / lint-yaml (pull_request) Successful in 5s
ci / lint-dockerfiles (pull_request) Successful in 1s
ci / validate (pull_request) Successful in 1s
ci / build (pull_request) Skipped
renovate-ci / validate-renovate (pull_request) Successful in 10s
2026-09-25 20:26:11 +02:00
forust 648b354951 refactor(deploy): marker-driven selection (k8s/active, root active); enable headscale/nextcloud hybrid, disable dockmon/kener/downtify/n8n
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 1s
ci / validate (push) Successful in 2s
deploy / preflight (push) Successful in 1s
renovate-ci / validate-renovate (push) Successful in 8s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / build (push) Successful in 1s
deploy / validate (push) Successful in 1m40s
deploy / apply-k8s (push) Successful in 1m41s
deploy / apply-compose (push) Successful in 13s
2026-09-23 18:11:51 +02:00
forust b0a9b3476d fix(deploy): drop broken %q quoting that wrapped remote vars in literal quotes
ci / lint-prettier (push) Successful in 2s
ci / lint-ruff (push) Successful in 1s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / validate (push) Successful in 2s
renovate-ci / validate-renovate (push) Successful in 8s
ci / build (push) Successful in 1s
deploy / redeploy (push) Failing after 1m43s
2026-09-23 16:33:51 +02:00
forust ac3bf4a55a fix(deploy): strip quotes and CR from DEPLOY_PATH
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 1s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / validate (push) Successful in 2s
renovate-ci / validate-renovate (push) Successful in 8s
ci / build (push) Successful in 1s
deploy / redeploy (push) Failing after 1s
2026-09-23 16:31:06 +02:00
forust 34fb6f85ba Merge pull request 'chore(deps): update darthnorse/dockmon docker tag to v2.5.0' (#39) from renovate/darthnorse-dockmon-2.x into main
renovate-ci / validate-renovate (push) Successful in 6s
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 1s
ci / lint-yaml (push) Successful in 2s
ci / build (push) Successful in 1s
ci / lint-dockerfiles (push) Successful in 1s
ci / validate (push) Successful in 1s
deploy / redeploy (push) Failing after 1s
Reviewed-on: #39
2026-09-23 14:16:21 +00:00
renovate-bot 66eacd86d1 chore(deps): update darthnorse/dockmon docker tag to v2.5.0 2026-09-23 14:16:21 +00:00
forust 54f43fc2c7 Merge pull request 'chore(deps): update ghcr.io/lukegus/termix docker tag to v2.8.0' (#40) from renovate/ghcr.io-lukegus-termix-2.x into main
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / lint-prettier (push) Successful in 2s
ci / lint-ruff (push) Successful in 1s
ci / validate (push) Successful in 2s
deploy / redeploy (push) Failing after 0s
renovate-ci / validate-renovate (push) Successful in 7s
ci / build (push) Successful in 1s
Reviewed-on: #40
2026-09-23 14:16:06 +00:00
renovate-bot 451d0dc6b7 chore(deps): update ghcr.io/lukegus/termix docker tag to v2.8.0 2026-09-23 14:16:06 +00:00
forust 88ae20a543 Merge pull request 'chore(deps): update ghcr.io/henriquesebastiao/downtify docker tag to v3' (#42) from renovate/ghcr.io-henriquesebastiao-downtify-3.x into main
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 1s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 0s
ci / validate (push) Successful in 1s
deploy / redeploy (push) Failing after 0s
ci / build (push) Canceled after 0s
renovate-ci / validate-renovate (push) Successful in 7s
Reviewed-on: #42
2026-09-23 14:15:51 +00:00
renovate-bot 6bd183ba73 chore(deps): update ghcr.io/henriquesebastiao/downtify docker tag to v3
renovate-ci / validate-renovate (push) Skipped
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 1s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / validate (push) Successful in 1s
ci / lint-ruff (pull_request) Successful in 1s
ci / lint-yaml (pull_request) Successful in 2s
ci / lint-dockerfiles (pull_request) Successful in 1s
ci / validate (pull_request) Successful in 2s
ci / build (pull_request) Skipped
renovate-ci / validate-renovate (pull_request) Successful in 10s
ci / build (push) Skipped
ci / lint-prettier (pull_request) Successful in 3s
2026-09-23 14:14:58 +00:00
forust aeefdd8560 Merge pull request 'chore(deps): update docker.n8n.io/n8nio/n8n docker tag to v2.41.0' (#43) from renovate/docker.n8n.io-n8nio-n8n-2.x into main
ci / lint-prettier (push) Successful in 2s
ci / lint-ruff (push) Successful in 0s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / validate (push) Successful in 1s
deploy / redeploy (push) Failing after 1s
renovate-ci / validate-renovate (push) Successful in 8s
ci / build (push) Successful in 1s
Reviewed-on: #43
2026-09-23 14:14:39 +00:00
renovate-bot c10d344fd7 chore(deps): update docker.n8n.io/n8nio/n8n docker tag to v2.41.0 2026-09-23 14:14:39 +00:00
forust 6e5f80611b Merge pull request 'Cicd/deploy rework' (#44) from cicd/deploy-rework into main
ci / lint-prettier (push) Successful in 3s
ci / lint-ruff (push) Successful in 0s
ci / lint-yaml (push) Successful in 2s
ci / lint-dockerfiles (push) Successful in 1s
ci / validate (push) Successful in 1s
deploy / redeploy (push) Failing after 0s
renovate-ci / validate-renovate (push) Successful in 6s
ci / build (push) Successful in 1m47s
Reviewed-on: #44
2026-09-23 14:11:42 +00:00
31 changed files with 1159 additions and 290 deletions

No files matched your search

+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env bash
# Shared stages for the deploy workflow. Runs on the workstation, invoked as:
# REPO=/srv/homelab APPLY_PRUNE=false bash -se <<'EOF'
# source "$REPO/.gitea/workflows/deploy-lib.sh"
# run_stage "$STAGE"
# EOF
set -euo pipefail
: "${REPO:?REPO must be set}"
APPLY_PRUNE="${APPLY_PRUNE:-false}"
log() {
echo "== $* =="
}
collect_k8s() {
git -C "$REPO" ls-files -- "$1" \
| grep -E '\.ya?ml$' \
| grep -Ev '/overlays/' \
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$' \
| grep -Ev '(^|/)[^/]*secret[^/]*\.ya?ml$' \
| sort
}
kustomize_overlay() {
if [ -f "$1/overlays/prod/kustomization.yaml" ]; then
echo "$1/overlays/prod"
elif [ -f "$1/base/kustomization.yaml" ]; then
echo "$1/base"
fi
}
select_manifests() {
K8S_MANIFESTS=()
KUSTOMIZE_APPS=()
COMPOSE_STACKS=()
local kd_rel kd overlay cf_rel cf f
while IFS= read -r kd_rel; do
kd="$REPO/$kd_rel"
if [ ! -f "$kd/active" ]; then
echo "skip (no k8s/active): $kd_rel"
continue
fi
overlay="$(kustomize_overlay "$kd" || true)"
if [ -n "${overlay:-}" ]; then
echo "kustomize app: ${overlay#$REPO/}"
KUSTOMIZE_APPS+=("$overlay")
else
while IFS= read -r f; do
[ -n "$f" ] && K8S_MANIFESTS+=("$REPO/$f")
done < <(collect_k8s "$kd_rel" || true)
fi
done < <(
git -C "$REPO" ls-files '*.yaml' '*.yml' \
| grep -E '(^|/)k8s/' \
| sed -E 's#((^|.*/)k8s)/.*#\1#' \
| sort -u
)
while IFS= read -r cf_rel; do
cf="$REPO/$cf_rel"
if [ -f "$(dirname "$cf")/active" ]; then
echo "compose: $cf_rel"
COMPOSE_STACKS+=("$cf")
else
echo "skip (no root active): $cf_rel"
fi
done < <(git -C "$REPO" ls-files '*/compose.yaml' '*/compose.yml' compose.yaml compose.yml | sort)
}
stage_preflight() {
if [ ! -d "$REPO/.git" ]; then
echo "Repository not found at $REPO"
exit 1
fi
git -C "$REPO" fetch origin main
log "Workstation state"
echo " local: $(git -C "$REPO" rev-parse --short HEAD)"
echo " remote: $(git -C "$REPO" rev-parse --short origin/main)"
if [ -n "$(git -C "$REPO" status --porcelain --untracked-files=no)" ]; then
echo "ERROR: workstation has local tracked modifications, refusing reset:"
git -C "$REPO" status --porcelain --untracked-files=no
git -C "$REPO" diff --stat
exit 1
fi
git -C "$REPO" reset --hard origin/main
}
stage_validate() {
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 them. Validate structure only:
# skip interpolation, env-file resolution, and path resolution so that
# required-variable guards (:?) and missing local files don't fail CI.
# Normalization and consistency checks stay enabled.
local compose_validate_flags=()
local compose_config_help
compose_config_help="$(docker compose config --help 2>/dev/null || true)"
if printf '%s' "$compose_config_help" | grep -q -- '--no-interpolate'; then
compose_validate_flags+=(--no-interpolate)
fi
if printf '%s' "$compose_config_help" | grep -q -- '--no-env-resolution'; then
compose_validate_flags+=(--no-env-resolution)
fi
if printf '%s' "$compose_config_help" | grep -q -- '--no-path-resolution'; then
compose_validate_flags+=(--no-path-resolution)
fi
log "Validate compose stacks"
for cf in ${COMPOSE_STACKS[@]+"${COMPOSE_STACKS[@]}"}; do
echo " config: $cf"
docker compose -f "$cf" config --quiet "${compose_validate_flags[@]}"
done
log "Validate k8s manifests (kubectl dry-run=client)"
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
kubectl apply --dry-run=client -f "$m" >/dev/null
done
for k in ${KUSTOMIZE_APPS[@]+"${KUSTOMIZE_APPS[@]}"}; do
kubectl apply -k "$k" --dry-run=client >/dev/null
done
log "Validate k8s manifests (kubectl dry-run=server)"
for m in ${K8S_MANIFESTS[@]+"${K8S_MANIFESTS[@]}"}; do
kubectl apply --dry-run=server -f "$m" >/dev/null
done
for k in ${KUSTOMIZE_APPS[@]+"${KUSTOMIZE_APPS[@]}"}; do
kubectl apply -k "$k" --dry-run=server >/dev/null
done
log "Checking referenced Secrets exist"
echo " (deploy never applies *secret*.yaml; create missing ones from the laptop)"
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
}
stage_apply_k8s() {
cd "$REPO"
select_manifests >/dev/null
local ns_files=() other_files=() m k prune_opts=()
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
if [ "${#ns_files[@]}" -gt 0 ]; then
log "Applying namespaces (${#ns_files[@]} files)"
for m in "${ns_files[@]}"; do
kubectl apply -f "$m"
done
fi
if [ -f "$REPO/prometheus-stack/k8s/active" ]; then
if [ ! -f "$REPO/prometheus-stack/k8s/grafana-values.yaml" ]; then
echo "ERROR: prometheus-stack/k8s/grafana-values.yaml (gitignored) missing on workstation, restore it first."
exit 1
fi
log "Upgrading kube-prometheus-stack"
helm upgrade --install prometheus-stack prometheus-community/kube-prometheus-stack \
--namespace prometheus \
--version 86.2.3 \
--values "$REPO/prometheus-stack/k8s/grafana-values.yaml" \
--wait --timeout 10m
fi
if [ -f "$REPO/loki/k8s/active" ]; then
log "Upgrading loki/alloy"
helm repo add grafana https://grafana.github.io/helm-charts >/dev/null 2>&1 || true
helm repo update grafana >/dev/null 2>&1 || true
helm upgrade --install loki grafana/loki \
--version 7.3.0 \
--namespace prometheus \
--values "$REPO/loki/k8s/loki-values.yaml" \
--wait --timeout 10m
helm upgrade --install alloy grafana/alloy \
--version 1.12.1 \
--namespace prometheus \
--values "$REPO/loki/k8s/alloy-values.yaml" \
--wait --timeout 10m
fi
if [ "${#other_files[@]}" -gt 0 ]; then
log "Applying resources (${#other_files[@]} files)"
for m in "${other_files[@]}"; do
kubectl apply "${prune_opts[@]}" -f "$m"
done
fi
for k in ${KUSTOMIZE_APPS[@]+"${KUSTOMIZE_APPS[@]}"}; do
log "Applying kustomize app: ${k#$REPO/}"
kubectl apply -k "$k"
done
if [ -f "$REPO/userbot/k8s/active" ]; then
log "userbot panel hook"
if kubectl get secret userbot-common-secrets -n userbot >/dev/null 2>&1; then
echo " userbot-common-secrets already present in userbot ns, not touching"
elif kubectl get secret userbot-common-secrets -n default >/dev/null 2>&1; then
echo " bootstrapping userbot-common-secrets into userbot ns"
kubectl get secret userbot-common-secrets -n default -o json \
| jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
| kubectl apply -f -
else
echo " WARNING: userbot-common-secrets missing in both default and userbot ns; create it manually from the laptop"
fi
kubectl rollout restart deployment/userbot-panel -n userbot
kubectl rollout status deployment/userbot-panel -n userbot --timeout=180s
fi
}
stage_apply_compose() {
cd "$REPO"
select_manifests >/dev/null
local cf
log "Redeploying docker compose stacks (${#COMPOSE_STACKS[@]} stacks)"
for cf in ${COMPOSE_STACKS[@]+"${COMPOSE_STACKS[@]}"}; do
echo " compose: $cf"
if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then
docker compose -f "$cf" build
docker compose -f "$cf" push
fi
docker compose -f "$cf" up -d --pull always --remove-orphans
done
}
run_stage() {
case "${1:?stage required}" in
preflight) stage_preflight ;;
validate) stage_validate ;;
apply-k8s) stage_apply_k8s ;;
apply-compose) stage_apply_compose ;;
*)
echo "ERROR: unknown stage: $1"
exit 1
;;
esac
}
+46 -282
View File
@@ -10,298 +10,62 @@ concurrency:
group: deploy-main group: deploy-main
cancel-in-progress: false cancel-in-progress: false
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
APPLY_PRUNE: ${{ vars.APPLY_PRUNE }}
jobs: jobs:
redeploy: preflight:
runs-on: [self-hosted, linux, arch, homelab, prod] runs-on: [self-hosted, linux, arch, homelab, prod]
steps: steps:
- name: Redeploy workstation - name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Fetch and reset workstation
shell: bash shell: bash
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
APPLY_PRUNE: ${{ vars.APPLY_PRUNE }}
run: | run: |
set -euo pipefail set -euo pipefail
./.gitea/workflows/ssh-run.sh preflight
: "${DEPLOY_HOST:?missing DEPLOY_HOST}" validate:
: "${DEPLOY_USER:?missing DEPLOY_USER}" needs: [preflight]
: "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}" runs-on: [self-hosted, linux, arch, homelab, prod]
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
deploy_port="${DEPLOY_PORT:-22}" - name: Dry-run manifests and check Secrets
deploy_path="${DEPLOY_PATH:-/srv/homelab}" shell: bash
run: |
ssh_key="$RUNNER_TEMP/deploy_key"
mkdir -p "$RUNNER_TEMP"
printf '%s\n' "$DEPLOY_KEY" > "$ssh_key"
chmod 600 "$ssh_key"
ssh_opts=(
-i "$ssh_key"
-p "$deploy_port"
-o BatchMode=yes
-o StrictHostKeyChecking=accept-new
)
ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
"DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF'
set -euo pipefail set -euo pipefail
./.gitea/workflows/ssh-run.sh validate
repo="${DEPLOY_PATH:-/srv/homelab}" apply-k8s:
needs: [validate]
runs-on: [self-hosted, linux, arch, homelab, prod]
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
if [ ! -d "$repo/.git" ]; then - name: Apply Kubernetes manifests
echo "Repository not found at $repo" shell: bash
exit 1 run: |
fi set -euo pipefail
./.gitea/workflows/ssh-run.sh apply-k8s
git -C "$repo" fetch origin main apply-compose:
needs: [validate]
runs-on: [self-hosted, linux, arch, homelab, prod]
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
echo "== Workstation state ==" - name: Redeploy docker compose stacks
echo " local: $(git -C "$repo" rev-parse --short HEAD)" shell: bash
echo " remote: $(git -C "$repo" rev-parse --short origin/main)" run: |
set -euo pipefail
if [ -n "$(git -C "$repo" status --porcelain --untracked-files=no)" ]; then ./.gitea/workflows/ssh-run.sh apply-compose
echo "ERROR: workstation has local tracked modifications, refusing reset:"
git -C "$repo" status --porcelain --untracked-files=no
git -C "$repo" diff --stat
exit 1
fi
git -C "$repo" reset --hard origin/main
cd "$repo"
is_disabled() {
local target="$1"
if [ -f "$target" ]; then
target="$(dirname "$target")"
fi
while true; do
if [ -f "$target/DISABLED" ]; then
return 0
fi
if [ "$target" = "$repo" ]; then
break
fi
target="$(dirname "$target")"
case "$target" in
"$repo"/*) ;;
*) break ;;
esac
done
return 1
}
collect_k8s() {
git ls-files -- "$1" \
| grep -E '\.ya?ml$' \
| grep -Ev '/routing/|/overlays/' \
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$' \
| grep -Ev '(^|/)[^/]*secret[^/]*\.ya?ml$' \
| sort
}
collect_k8s_inactive() {
collect_k8s "$1" \
| grep -E '(^|/)namespace\.ya?ml$|/routing/'
}
kustomize_overlay() {
if [ -f "$1/overlays/prod/kustomization.yaml" ]; then
echo "$1/overlays/prod"
elif [ -f "$1/base/kustomization.yaml" ]; then
echo "$1/base"
fi
}
mapfile -t k8s_dirs < <(
git ls-files '*.yaml' '*.yml' \
| grep -E '(^|/)k8s/' \
| sed -E 's#((^|.*/)k8s)/.*#\1#' \
| sort -u
)
k8s_manifests=()
kustomize_apps=()
for kd_rel in "${k8s_dirs[@]}"; do
kd="$repo/$kd_rel"
if is_disabled "$kd"; then
echo "skip (DISABLED): $kd_rel"
continue
fi
if [ -f "$kd/active" ]; then
overlay="$(kustomize_overlay "$kd" || true)"
if [ -n "${overlay:-}" ]; then
echo "kustomize app: ${overlay#$repo/}"
kustomize_apps+=("$overlay")
else
while IFS= read -r f; do
[ -n "$f" ] && k8s_manifests+=("$repo/$f")
done < <(collect_k8s "$kd_rel" || true)
fi
else
while IFS= read -r f; do
[ -n "$f" ] && k8s_manifests+=("$repo/$f")
done < <(collect_k8s_inactive "$kd_rel" || true)
fi
done
mapfile -t compose_rel < <(
git ls-files '*/compose.yaml' '*/compose.yml' compose.yaml compose.yml | sort
)
compose_stacks=()
for cf_rel in "${compose_rel[@]}"; do
cf="$repo/$cf_rel"
if is_disabled "$cf"; then
echo "skip (DISABLED): $cf_rel"
continue
fi
if [ -f "$(dirname "$cf")/k8s/active" ]; then
echo "skip (k8s-managed): $cf_rel"
continue
fi
compose_stacks+=("$cf")
done
echo "== Validate compose stacks =="
for cf in "${compose_stacks[@]}"; do
echo " config: $cf"
docker compose -f "$cf" config --quiet
done
echo "== Validate k8s manifests (kubectl dry-run=client) =="
for m in "${k8s_manifests[@]}"; do
echo " apply --dry-run=client $m"
kubectl apply --dry-run=client -f "$m" >/dev/null
done
for k in "${kustomize_apps[@]}"; do
echo " apply -k --dry-run=client $k"
kubectl apply -k "$k" --dry-run=client >/dev/null
done
echo "== Validate k8s manifests (kubectl dry-run=server) =="
for m in "${k8s_manifests[@]}"; do
echo " apply --dry-run=server $m"
kubectl apply --dry-run=server -f "$m" >/dev/null
done
for k in "${kustomize_apps[@]}"; do
echo " apply -k --dry-run=server $k"
kubectl apply -k "$k" --dry-run=server >/dev/null
done
echo "== Checking referenced Secrets exist =="
echo " (deploy never applies *secret*.yaml; create missing ones from the laptop)"
ref_secrets=()
if [ "${#k8s_manifests[@]}" -gt 0 ]; then
while IFS= read -r s; do
[ -n "$s" ] && ref_secrets+=("$s")
done < <(
{
grep -h -A1 -E 'secretRef:|secretKeyRef:' "${k8s_manifests[@]}" 2>/dev/null || true
grep -h -E 'secretName:' "${k8s_manifests[@]}" 2>/dev/null || true
} | grep -E 'name:' | sed -E 's/.*name:[[:space:]]*//' | tr -d '"'"'"' "'"'" | sed -E 's/[[:space:]]*#.*//' | awk 'NF' | sort -u || true
)
fi
missing_secrets=()
all_secrets="$(kubectl get secrets -A --no-headers -o custom-columns=:metadata.name 2>/dev/null || true)"
for s in "${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
echo "== Applying Kubernetes manifests =="
ns_files=()
other_files=()
for m in "${k8s_manifests[@]}"; do
case "$m" in
*/namespace.y?ml) ns_files+=("$m") ;;
*) other_files+=("$m") ;;
esac
done
prune_opts=()
if [ "${APPLY_PRUNE:-false}" = "true" ]; then
prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy)
fi
if [ "${#ns_files[@]}" -gt 0 ]; then
echo " namespaces first: ${ns_files[*]}"
kubectl apply -f "${ns_files[@]}"
fi
if [ -f "$repo/prometheus-stack/k8s/active" ] && ! is_disabled "$repo/prometheus-stack/k8s"; then
if [ ! -f "$repo/prometheus-stack/k8s/grafana-values.yaml" ]; then
echo "ERROR: prometheus-stack/k8s/grafana-values.yaml (gitignored) missing on workstation, restore it first."
exit 1
fi
echo "== Upgrading kube-prometheus-stack =="
helm upgrade --install prometheus-stack prometheus-community/kube-prometheus-stack \
--namespace prometheus \
--version 86.2.3 \
--values "$repo/prometheus-stack/k8s/grafana-values.yaml" \
--wait --timeout 10m
fi
if [ -f "$repo/loki/k8s/active" ] && ! is_disabled "$repo/loki/k8s"; then
echo "== Upgrading loki/alloy =="
helm repo add grafana https://grafana.github.io/helm-charts >/dev/null 2>&1 || true
helm repo update grafana >/dev/null 2>&1 || true
helm upgrade --install loki grafana/loki \
--version 7.3.0 \
--namespace prometheus \
--values "$repo/loki/k8s/loki-values.yaml" \
--wait --timeout 10m
helm upgrade --install alloy grafana/alloy \
--version 1.12.1 \
--namespace prometheus \
--values "$repo/loki/k8s/alloy-values.yaml" \
--wait --timeout 10m
fi
if [ "${#other_files[@]}" -gt 0 ]; then
echo " resources: ${other_files[*]}"
kubectl apply "${prune_opts[@]}" -f "${other_files[@]}"
fi
for k in "${kustomize_apps[@]}"; do
echo "== Applying kustomize app: ${k#$repo/} =="
kubectl apply -k "$k"
done
if [ -f "$repo/userbot/k8s/active" ] && ! is_disabled "$repo/userbot"; then
echo "== userbot panel hook =="
if kubectl get secret userbot-common-secrets -n userbot >/dev/null 2>&1; then
echo " userbot-common-secrets already present in userbot ns, not touching"
elif kubectl get secret userbot-common-secrets -n default >/dev/null 2>&1; then
echo " bootstrapping userbot-common-secrets into userbot ns"
kubectl get secret userbot-common-secrets -n default -o json \
| jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
| kubectl apply -f -
else
echo " WARNING: userbot-common-secrets missing in both default and userbot ns; create it manually from the laptop"
fi
kubectl rollout restart deployment/userbot-panel -n userbot
kubectl rollout status deployment/userbot-panel -n userbot --timeout=180s
fi
echo "== Redeploying docker compose stacks =="
for cf in "${compose_stacks[@]}"; do
echo " compose: $cf"
if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then
docker compose -f "$cf" build
docker compose -f "$cf" push
fi
docker compose -f "$cf" up -d --pull always --remove-orphans
done
EOF
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# usage: ssh-run.sh <stage>
# Runs one deploy-lib.sh stage on the workstation over SSH.
set -euo pipefail
: "${DEPLOY_HOST:?missing DEPLOY_HOST}"
: "${DEPLOY_USER:?missing DEPLOY_USER}"
: "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}"
deploy_port="${DEPLOY_PORT:-22}"
deploy_path="${DEPLOY_PATH:-/srv/homelab}"
deploy_path="$(printf '%s' "$deploy_path" | tr -d '\"' | tr -d '\r' | xargs)"
ssh_key="$RUNNER_TEMP/deploy_key"
mkdir -p "$RUNNER_TEMP"
printf '%s\n' "$DEPLOY_KEY" > "$ssh_key"
chmod 600 "$ssh_key"
ssh -i "$ssh_key" -p "$deploy_port" \
-o BatchMode=yes -o StrictHostKeyChecking=accept-new \
"${DEPLOY_USER}@${DEPLOY_HOST}" \
"REPO=$deploy_path APPLY_PRUNE=${APPLY_PRUNE:-false} STAGE=$1 bash -se" <<'EOF'
source "$REPO/.gitea/workflows/deploy-lib.sh"
run_stage "$STAGE"
EOF
+3
View File
@@ -21,6 +21,9 @@ checkmk/checkmk/*
downtify/Downtify_downloads downtify/Downtify_downloads
headscale/config/* headscale/config/*
headscale/data/* headscale/data/*
# NetBird local hostnames and generated secrets
netbird/.env
netbird/secrets/
searxng/core-config/* searxng/core-config/*
# Steaming services files # Steaming services files
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
dockmon: dockmon:
image: darthnorse/dockmon:2.4.5 image: darthnorse/dockmon:2.5.0
container_name: dockmon container_name: dockmon
restart: unless-stopped restart: unless-stopped
# ports: # ports:
+1 -1
View File
@@ -29,7 +29,7 @@ spec:
spec: spec:
containers: containers:
- name: dockmon - name: dockmon
image: darthnorse/dockmon:2.4.5 image: darthnorse/dockmon:2.5.0
ports: ports:
- containerPort: 443 - containerPort: 443
volumeMounts: volumeMounts:
+1 -1
View File
@@ -1,7 +1,7 @@
services: services:
downtify: downtify:
container_name: downtify container_name: downtify
image: ghcr.io/henriquesebastiao/downtify:2.13.0 image: ghcr.io/henriquesebastiao/downtify:3.1.0
restart: unless-stopped restart: unless-stopped
# ports: # ports:
# - '7077:8000' # - '7077:8000'
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec: spec:
containers: containers:
- name: downtify - name: downtify
image: ghcr.io/henriquesebastiao/downtify:2.13.0 image: ghcr.io/henriquesebastiao/downtify:3.1.0
ports: ports:
- containerPort: 8000 - containerPort: 8000
volumeMounts: volumeMounts:
File renamed without changes.
View File
File renamed without changes.
View File
Whitespace-only changes.
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
n8n: n8n:
image: docker.n8n.io/n8nio/n8n:2.40.3 image: docker.n8n.io/n8nio/n8n:2.41.0
container_name: n8n container_name: n8n
restart: unless-stopped restart: unless-stopped
environment: environment:
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec: spec:
containers: containers:
- name: n8n - name: n8n
image: docker.n8n.io/n8nio/n8n:2.40.3 image: docker.n8n.io/n8nio/n8n:2.41.0
envFrom: envFrom:
- configMapRef: - configMapRef:
name: n8n-config name: n8n-config
+13
View File
@@ -0,0 +1,13 @@
# Public hostname advertised to NetBird clients and used for TLS/OAuth.
NETBIRD_DOMAIN=nb.forust.xyz
# Internal-only aliases routed by the existing Traefik instance.
NETBIRD_LOCAL_DOMAIN=netbird.workstation.internal
NETBIRD_DEV_DOMAIN=netbird.gigaforust.internal
NETBIRD_PROXY_SUBNET=auto
NETBIRD_CLIENT_HOSTNAME=hostname
# Add a dashboard-generated setup key before starting client.compose.yaml.
# NB_SETUP_KEY=
+123
View File
@@ -0,0 +1,123 @@
# NetBird
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.
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.
## Files
- `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.
## First deployment
Run these commands on the Docker host before merging the activating branch. The deploy preflight resets tracked files but preserves ignored local state.
```bash
cd /srv/homelab/netbird
./setup.sh
$EDITOR .env
docker compose config --quiet
docker compose up -d
```
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
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.
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:
```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
```
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.
View File
Whitespace-only changes.
+31
View File
@@ -0,0 +1,31 @@
name: netbird-client
services:
netbird-client:
image: netbirdio/netbird:0.79.0
container_name: netbird-client
hostname: "${NETBIRD_CLIENT_HOSTNAME:?Set NETBIRD_CLIENT_HOSTNAME in netbird/.env}"
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_ADMIN
- SYS_RESOURCE
devices:
- /dev/net/tun
network_mode: host
environment:
NB_SETUP_KEY: "${NB_SETUP_KEY:?Set NB_SETUP_KEY in netbird/.env after creating a peer setup key}"
NB_MANAGEMENT_URL: "https://${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}"
volumes:
- netbird-client:/var/lib/netbird
healthcheck:
test: ["CMD", "/usr/local/bin/netbird", "status", "--check", "live"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
stop_grace_period: 30s
volumes:
netbird-client:
name: netbird-client
+152
View File
@@ -0,0 +1,152 @@
name: netbird
services:
netbird-server:
image: netbirdio/netbird-server:0.79.0
container_name: netbird-server
restart: unless-stopped
environment:
NETBIRD_DOMAIN: "${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}"
NETBIRD_PROXY_SUBNET: "${NETBIRD_PROXY_SUBNET:?Set NETBIRD_PROXY_SUBNET in netbird/.env (run setup.sh)}"
entrypoint:
- /bin/sh
- /opt/netbird/entrypoint.sh
command:
- --config
- /run/netbird/config.yaml
ports:
- "3478:3478/udp"
volumes:
- netbird_data:/var/lib/netbird
- ./config.template.yaml:/opt/netbird/config.template.yaml:ro
- ./entrypoint.sh:/opt/netbird/entrypoint.sh:ro
secrets:
- relay_auth_secret
- datastore_encryption_key
tmpfs:
- /run/netbird:mode=0700
healthcheck:
test:
- CMD
- bash
- -ec
- exec 3<>/dev/tcp/127.0.0.1/80
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
stop_grace_period: 30s
labels:
- "traefik.enable=true"
- "traefik.http.services.netbird-server.loadbalancer.server.port=80"
- "traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80"
- "traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c"
# gRPC routers
# Prod Router
- "traefik.http.routers.netbird-grpc.rule=Host(`${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
- "traefik.http.routers.netbird-grpc.entrypoints=websecure"
- "traefik.http.routers.netbird-grpc.service=netbird-server-h2c"
- "traefik.http.routers.netbird-grpc.priority=100"
- "traefik.http.routers.netbird-grpc.tls=true"
- "traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.netbird-grpc-local.rule=Host(`${NETBIRD_LOCAL_DOMAIN:?Set NETBIRD_LOCAL_DOMAIN in netbird/.env}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
- "traefik.http.routers.netbird-grpc-local.entrypoints=websecure"
- "traefik.http.routers.netbird-grpc-local.service=netbird-server-h2c"
- "traefik.http.routers.netbird-grpc-local.priority=100"
- "traefik.http.routers.netbird-grpc-local.tls=true"
# Dev Router
- "traefik.http.routers.netbird-grpc-dev.rule=Host(`${NETBIRD_DEV_DOMAIN:?Set NETBIRD_DEV_DOMAIN in netbird/.env}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
- "traefik.http.routers.netbird-grpc-dev.entrypoints=websecure"
- "traefik.http.routers.netbird-grpc-dev.service=netbird-server-h2c"
- "traefik.http.routers.netbird-grpc-dev.priority=100"
- "traefik.http.routers.netbird-grpc-dev.tls=true"
# Backend routers
# Prod Router
- "traefik.http.routers.netbird-backend.rule=Host(`${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
- "traefik.http.routers.netbird-backend.entrypoints=websecure"
- "traefik.http.routers.netbird-backend.service=netbird-server"
- "traefik.http.routers.netbird-backend.priority=100"
- "traefik.http.routers.netbird-backend.tls=true"
- "traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.netbird-backend-local.rule=Host(`${NETBIRD_LOCAL_DOMAIN:?Set NETBIRD_LOCAL_DOMAIN in netbird/.env}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
- "traefik.http.routers.netbird-backend-local.entrypoints=websecure"
- "traefik.http.routers.netbird-backend-local.service=netbird-server"
- "traefik.http.routers.netbird-backend-local.priority=100"
- "traefik.http.routers.netbird-backend-local.tls=true"
# Dev Router
- "traefik.http.routers.netbird-backend-dev.rule=Host(`${NETBIRD_DEV_DOMAIN:?Set NETBIRD_DEV_DOMAIN in netbird/.env}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
- "traefik.http.routers.netbird-backend-dev.entrypoints=websecure"
- "traefik.http.routers.netbird-backend-dev.service=netbird-server"
- "traefik.http.routers.netbird-backend-dev.priority=100"
- "traefik.http.routers.netbird-backend-dev.tls=true"
networks:
- proxy
dashboard:
image: netbirdio/dashboard:v2.90.10
container_name: netbird-dashboard
restart: unless-stopped
environment:
NETBIRD_MGMT_API_ENDPOINT: "https://${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}"
NETBIRD_MGMT_GRPC_API_ENDPOINT: "https://${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}"
AUTH_AUDIENCE: netbird-dashboard
AUTH_CLIENT_ID: netbird-dashboard
AUTH_CLIENT_SECRET: ""
AUTH_AUTHORITY: "https://${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}/oauth2"
AUTH_SUPPORTED_SCOPES: openid profile email groups
AUTH_REDIRECT_URI: /nb-auth
AUTH_SILENT_REDIRECT_URI: /nb-silent-auth
USE_AUTH0: "false"
LETSENCRYPT_DOMAIN: none
depends_on:
netbird-server:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://127.0.0.1/"]
interval: 30s
timeout: 5s
retries: 5
start_period: 15s
labels:
- "traefik.enable=true"
- "traefik.http.services.netbird-dashboard.loadbalancer.server.port=80"
# Dashboard catch-all routers
# Prod Router
- "traefik.http.routers.netbird-dashboard.rule=Host(`${NETBIRD_DOMAIN:?Set NETBIRD_DOMAIN in netbird/.env}`)"
- "traefik.http.routers.netbird-dashboard.entrypoints=websecure"
- "traefik.http.routers.netbird-dashboard.service=netbird-dashboard"
- "traefik.http.routers.netbird-dashboard.priority=1"
- "traefik.http.routers.netbird-dashboard.tls=true"
- "traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.netbird-dashboard-local.rule=Host(`${NETBIRD_LOCAL_DOMAIN:?Set NETBIRD_LOCAL_DOMAIN in netbird/.env}`)"
- "traefik.http.routers.netbird-dashboard-local.entrypoints=websecure"
- "traefik.http.routers.netbird-dashboard-local.service=netbird-dashboard"
- "traefik.http.routers.netbird-dashboard-local.priority=1"
- "traefik.http.routers.netbird-dashboard-local.tls=true"
# Dev Router
- "traefik.http.routers.netbird-dashboard-dev.rule=Host(`${NETBIRD_DEV_DOMAIN:?Set NETBIRD_DEV_DOMAIN in netbird/.env}`)"
- "traefik.http.routers.netbird-dashboard-dev.entrypoints=websecure"
- "traefik.http.routers.netbird-dashboard-dev.service=netbird-dashboard"
- "traefik.http.routers.netbird-dashboard-dev.priority=1"
- "traefik.http.routers.netbird-dashboard-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
volumes:
netbird_data:
name: netbird_data
secrets:
relay_auth_secret:
file: ./secrets/relay-auth-secret
datastore_encryption_key:
file: ./secrets/datastore-encryption-key
+26
View File
@@ -0,0 +1,26 @@
server:
listenAddress: ":80"
exposedAddress: "https://__NETBIRD_DOMAIN__:443"
stunPorts:
- 3478
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: info
logFile: console
authSecret: "__NETBIRD_AUTH_SECRET__"
dataDir: "/var/lib/netbird"
disableAnonymousMetrics: true
auth:
issuer: "https://__NETBIRD_DOMAIN__/oauth2"
signKeyRefreshEnabled: true
dashboardRedirectURIs:
- "https://__NETBIRD_DOMAIN__/nb-auth"
- "https://__NETBIRD_DOMAIN__/nb-silent-auth"
reverseProxy:
trustedHTTPProxies:
- "__NETBIRD_PROXY_SUBNET__"
trustedPeers:
- "__NETBIRD_PROXY_SUBNET__"
store:
engine: sqlite
encryptionKey: "__NETBIRD_ENCRYPTION_KEY__"
+28
View File
@@ -0,0 +1,28 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: netbird-prod-tls
namespace: netbird
spec:
secretName: netbird-prod-tls
dnsNames:
- nb.forust.xyz
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: internal-wildcard-tls
namespace: netbird
spec:
secretName: internal-wildcard-tls
dnsNames:
- "*.workstation.internal"
- "*.gigaforust.internal"
- workstation.internal
- gigaforust.internal
issuerRef:
name: internal-ca
kind: ClusterIssuer
+160
View File
@@ -0,0 +1,160 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: netbird-config
namespace: netbird
data:
# Public hostname, rendered into the server config by entrypoint.sh.
NETBIRD_DOMAIN: "nb.forust.xyz"
NETBIRD_PROXY_SUBNET: "10.244.0.0/16"
NETBIRD_MGMT_API_ENDPOINT: "https://nb.forust.xyz"
NETBIRD_MGMT_GRPC_API_ENDPOINT: "https://nb.forust.xyz"
AUTH_AUDIENCE: "netbird-dashboard"
AUTH_CLIENT_ID: "netbird-dashboard"
AUTH_CLIENT_SECRET: ""
AUTH_AUTHORITY: "https://nb.forust.xyz/oauth2"
AUTH_SUPPORTED_SCOPES: "openid profile email groups"
AUTH_REDIRECT_URI: "/nb-auth"
AUTH_SILENT_REDIRECT_URI: "/nb-silent-auth"
USE_AUTH0: "false"
LETSENCRYPT_DOMAIN: "none"
config.template.yaml: |
server:
listenAddress: ":80"
exposedAddress: "https://__NETBIRD_DOMAIN__:443"
stunPorts:
- 3478
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: info
logFile: console
authSecret: "__NETBIRD_AUTH_SECRET__"
dataDir: "/var/lib/netbird"
disableAnonymousMetrics: true
auth:
issuer: "https://__NETBIRD_DOMAIN__/oauth2"
signKeyRefreshEnabled: true
dashboardRedirectURIs:
- "https://__NETBIRD_DOMAIN__/nb-auth"
- "https://__NETBIRD_DOMAIN__/nb-silent-auth"
reverseProxy:
trustedHTTPProxies:
- "__NETBIRD_PROXY_SUBNET__"
trustedPeers:
- "__NETBIRD_PROXY_SUBNET__"
store:
engine: sqlite
encryptionKey: "__NETBIRD_ENCRYPTION_KEY__"
entrypoint.sh: |
#!/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 "$@"
+83
View File
@@ -0,0 +1,83 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: netbird-prod
namespace: netbird
spec:
entryPoints:
- websecure
routes:
- match: Host(`nb.forust.xyz`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))
kind: Rule
priority: 100
middlewares:
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: netbird-server-service
port: 80
scheme: h2c
- match: Host(`nb.forust.xyz`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))
kind: Rule
priority: 100
middlewares:
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: netbird-server-service
port: 80
- match: Host(`nb.forust.xyz`)
kind: Rule
priority: 1
middlewares:
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: netbird-dashboard-service
port: 80
tls:
secretName: netbird-prod-tls
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: netbird-local
namespace: netbird
spec:
entryPoints:
- websecure
routes:
- match: (Host(`netbird.workstation.internal`) || Host(`netbird.gigaforust.internal`)) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))
kind: Rule
priority: 100
services:
- name: netbird-server-service
port: 80
scheme: h2c
- match: (Host(`netbird.workstation.internal`) || Host(`netbird.gigaforust.internal`)) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))
kind: Rule
priority: 100
services:
- name: netbird-server-service
port: 80
- match: Host(`netbird.workstation.internal`) || Host(`netbird.gigaforust.internal`)
kind: Rule
priority: 1
services:
- name: netbird-dashboard-service
port: 80
tls:
secretName: internal-wildcard-tls
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteUDP
metadata:
name: netbird-stun
namespace: netbird
spec:
entryPoints:
- netbird-stun
routes:
- services:
- name: netbird-server-service
port: 3478
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: netbird
+181
View File
@@ -0,0 +1,181 @@
apiVersion: v1
kind: Service
metadata:
name: netbird-server-service
namespace: netbird
spec:
selector:
app: netbird-server
ports:
- port: 80
name: http
targetPort: 80
protocol: TCP
- port: 3478
name: stun
targetPort: 3478
protocol: UDP
---
apiVersion: v1
kind: Service
metadata:
name: netbird-dashboard-service
namespace: netbird
spec:
selector:
app: netbird-dashboard
ports:
- port: 80
name: http
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: netbird-server-deployment
namespace: netbird
spec:
replicas: 1
selector:
matchLabels:
app: netbird-server
template:
metadata:
labels:
app: netbird-server
spec:
containers:
- name: netbird-server
image: netbirdio/netbird-server:0.79.0
command: ["/bin/sh", "/opt/netbird/entrypoint.sh", "--config", "/run/netbird/config.yaml"]
envFrom:
- configMapRef:
name: netbird-config
ports:
- containerPort: 80
name: http
protocol: TCP
- containerPort: 3478
name: stun
protocol: UDP
volumeMounts:
- name: netbird-data
mountPath: /var/lib/netbird
- name: netbird-files
mountPath: /opt/netbird
readOnly: true
- name: netbird-secrets
mountPath: /run/secrets/relay_auth_secret
subPath: relay_auth_secret
readOnly: true
- name: netbird-secrets
mountPath: /run/secrets/datastore_encryption_key
subPath: datastore_encryption_key
readOnly: true
- name: netbird-run
mountPath: /run/netbird
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
volumes:
- name: netbird-data
persistentVolumeClaim:
claimName: netbird-pvc
- name: netbird-files
configMap:
name: netbird-config
defaultMode: 0755
items:
- key: config.template.yaml
path: config.template.yaml
- key: entrypoint.sh
path: entrypoint.sh
- name: netbird-secrets
secret:
secretName: netbird-secrets
items:
- key: relay_auth_secret
path: relay_auth_secret
- key: datastore_encryption_key
path: datastore_encryption_key
- name: netbird-run
emptyDir:
medium: Memory
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: netbird-dashboard-deployment
namespace: netbird
spec:
replicas: 1
selector:
matchLabels:
app: netbird-dashboard
template:
metadata:
labels:
app: netbird-dashboard
spec:
containers:
- name: dashboard
image: netbirdio/dashboard:v2.90.10
envFrom:
- configMapRef:
name: netbird-config
ports:
- containerPort: 80
name: http
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "300m"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: netbird-pvc
namespace: netbird
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: netbird-secrets
namespace: netbird
type: Opaque
stringData:
# hex, 64 chars: openssl rand -hex 32
relay_auth_secret: "REPLACE_ME"
# base64, 44 chars: openssl rand -base64 32
datastore_encryption_key: "REPLACE_ME"
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
termix: termix:
image: ghcr.io/lukegus/termix:2.7.1 image: ghcr.io/lukegus/termix:2.8.0
container_name: termix container_name: termix
restart: unless-stopped restart: unless-stopped
# ports: # ports:
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec: spec:
containers: containers:
- name: termix - name: termix
image: ghcr.io/lukegus/termix:2.7.1 image: ghcr.io/lukegus/termix:2.8.0
envFrom: envFrom:
- configMapRef: - configMapRef:
name: termix-config name: termix-config
+1
View File
@@ -20,6 +20,7 @@ services:
- "--entryPoints.web.http.redirections.entryPoint.scheme=https" - "--entryPoints.web.http.redirections.entryPoint.scheme=https"
- "--entryPoints.web.http.redirections.entryPoint.to=websecure" - "--entryPoints.web.http.redirections.entryPoint.to=websecure"
- "--entryPoints.websecure.address=:443" - "--entryPoints.websecure.address=:443"
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
- "--entryPoints.websecure.http.middlewares=error-pages@docker" - "--entryPoints.websecure.http.middlewares=error-pages@docker"
- "--entryPoints.websecure.http.tls=true" - "--entryPoints.websecure.http.tls=true"
- "--entryPoints.ssh.address=:2221" - "--entryPoints.ssh.address=:2221"
+6
View File
@@ -86,6 +86,12 @@ ports:
protocol: UDP protocol: UDP
expose: expose:
default: true default: true
netbird-stun:
port: 3478
exposedPort: 3478
protocol: UDP
expose:
default: true
checkmk-agent: checkmk-agent:
port: 8000 port: 8000
protocol: TCP protocol: TCP