user@homelab:~/docs$ cat 13-this-site.md

Tento web

Jak se web sestavuje z Markdownu a nasazuje do k3s, a jak ho případně zpřístupnit z internetu

# obsah

Architektura

text
Markdown (site/content/*.md)
   │  hugo --minify                      ← build stage (alpine + ověřený Hugo binár)
   ▼
statické HTML/CSS/JS
   │  COPY do nginx-unprivileged         ← runtime stage (uid 101, port 8080)
   ▼
image localhost/homelab-docs:<tag>
   │  buildah → docker-archive → k3s ctr images import
   ▼
Deployment → Service → Ingress (Traefik) → http://<ip-serveru>/
   │
   └─ cloudflared (tunel) → https://svejklab.dev

Struktura projektu

text
homelab-docs/
├── site/                 # Hugo projekt
│   ├── hugo.toml
│   ├── content/          # stránky v Markdownu
│   ├── data/status.yaml  # tabulky na úvodní stránce
│   ├── layouts/          # vlastní "terminálová" šablona
│   └── assets/           # CSS a JS
├── nginx/default.conf    # nginx: bezpečnostní hlavičky, /healthz
├── Dockerfile            # multi-stage build
├── k8s/                  # Namespace, Deployment, Service, Ingress, kustomization
├── cloudflared/          # Cloudflare Tunnel konektor (token je Secret, ne soubor)
└── deploy.sh             # build + import + apply na serveru

Nasazení

  1. Projekt nahrát na server:
shell @ mac
scp homelab-docs.zip homelab:~
ssh homelab
  1. Na serveru rozbalit a spustit deploy:
shell @ server
sudo apt install -y unzip
unzip homelab-docs.zip && cd homelab-docs
chmod +x deploy.sh
./deploy.sh 0.1.0

Podrobný rozbor obou kroků je níže v sekcích Co přesně dělá scp a Co přesně dělá deploy.sh.

Skript nainstaluje buildah, pokud chybí, sestaví image, naimportuje ho do containerd v k3s, nastaví tag v kustomization.yaml, provede kubectl apply -k k8s/ a počká na rollout.

  1. Otevřít http://<ip-serveru>/. Pro hezčí adresu přidej na Macu do /etc/hosts:
shell @ mac
echo "<ip-serveru> docs.homelab.lan" | sudo tee -a /etc/hosts

Co přesně dělá scp

shell @ mac
scp ~/Documents/Homelab/homelab-docs.zip homelab:~/homelab-docs.zip
ČástVýznam
scpsecure copy: kopírování souboru přes šifrované SSH spojení
~/Documents/Homelab/homelab-docs.zipodkud: soubor na Macu
homelab:kam: server, zkratka ze ~/.ssh/config (IP adresa, uživatel i SSH klíč)
~/homelab-docs.zipcesta na serveru: domovská složka uživatele

Existující soubor na serveru scp bez ptaní přepíše. Před nahráním ověř, že posíláš nejnovější verzi:

shell @ mac
ls -lt ~/Documents/Homelab/homelab-docs*.zip
unzip -l ~/Documents/Homelab/homelab-docs.zip | grep 14-public

Co přesně dělá deploy.sh

shell @ server
./deploy.sh 0.3.1

Parametr je tag (verze) image. Bez parametru se vygeneruje z data a času, např. 20261028-1330.

shell @ server
#!/usr/bin/env bash
# Sestaví image webu přímo na serveru a nasadí ho do k3s.
# Spouštět NA SERVERU ve složce projektu:   ./deploy.sh            (tag = datum a čas)
#                                           ./deploy.sh 0.2.0      (vlastní tag)
set -euo pipefail

cd "$(dirname "$(readlink -f "$0")")"

IMAGE="localhost/homelab-docs"
TAG="${1:-$(date +%Y%m%d-%H%M)}"
WORKTMP="$(mktemp -d)"
ARCHIVE="${WORKTMP}/image.tar"
trap 'sudo rm -rf "$WORKTMP"' EXIT

step() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; }

if ! command -v buildah >/dev/null; then
  step "Instaluji buildah (sestavení image bez Docker démona)"
  sudo apt-get update -qq && sudo apt-get install -y -qq buildah
fi

step "Build ${IMAGE}:${TAG}"
sudo buildah build --network=host --layers -t "${IMAGE}:${TAG}" -f Dockerfile .

step "Import do containerd v k3s"
sudo buildah push "${IMAGE}:${TAG}" "docker-archive:${ARCHIVE}:${IMAGE}:${TAG}"
sudo k3s ctr -n k8s.io images import "$ARCHIVE"
sudo k3s ctr -n k8s.io images ls -q | grep -F "${IMAGE}:${TAG}"

step "Nastavuji tag v k8s/kustomization.yaml"
sed -i "s/^\(\s*newTag:\).*/\1 \"${TAG}\"/" k8s/kustomization.yaml
grep newTag k8s/kustomization.yaml

step "Kontrola manifestů (server-side dry-run)"
kubectl apply -f k8s/namespace.yaml   # dry-run potřebuje existující namespace
kubectl apply -k k8s/ --dry-run=server

step "kubectl apply -k k8s/"
kubectl apply -k k8s/
kubectl -n homelab-docs rollout status deploy/homelab-docs --timeout=120s

step "Hotovo"
kubectl -n homelab-docs get pods,svc,ingress -o wide
echo
echo "Web: http://<ip-serveru>/   nebo   http://docs.homelab.lan/ (po úpravě /etc/hosts)"

Příprava (řádky 5–15)

  • set -euo pipefail: pojistka. Skript se zastaví při první chybě (-e), při nedefinované proměnné (-u) i při chybě uvnitř roury | (pipefail). Po neúspěšném buildu tak nikdy nepokračuje nasazením.
  • cd "$(dirname "$(readlink -f "$0")")": přepne se do složky skriptu. Funguje pak, i když ho spustíš odjinud.
  • TAG="${1:-$(date …)}": první parametr, jinak datum a čas.
  • mktemp -d + trap … EXIT: dočasná složka na archiv image, která se vždy smaže, i když skript spadne.

1. buildah (jen poprvé)

Když buildah chybí, nainstaluje ho z apt. Další běhy krok přeskočí.

2. Build image

buildah build sestaví image podle Dockerfile ve dvou fázích:

  1. V Alpine stáhne Hugo, ověří jeho SHA256 a vyrobí z Markdownu HTML.
  2. HTML zkopíruje do malého image nginx-unprivileged, který běží bez root práv.

Přepínače:

  • --network=host: build použije síť serveru. Bez něj ufw zahazoval přeposílaný provoz a apk nestáhl balíčky, viz Troubleshooting #16.
  • --layers: cache mezivýsledků, takže další buildy jsou rychlejší.

3. Import do k3s

buildah a k3s mají každý své úložiště image. Image se proto zabalí do archivu (docker-archive) a naimportuje do containerd v k3s (k3s ctr images import). Poslední řádek kroku ověří, že tam image opravdu je. Když ne, grep selže a skript skončí.

Proto má Deployment imagePullPolicy: Never: image se nestahuje z registry, bere se z místního úložiště.

4. Verze v manifestu

sed přepíše newTag v k8s/kustomization.yaml. Kustomize tag při nasazení dosadí do Deploymentu, takže se manifest Deploymentu ručně neupravuje.

5. Kontrola na nečisto

kubectl apply -k k8s/ --dry-run=server: API server manifesty zkontroluje, ale nic nezmění. Ověří syntaxi, pravidla Pod Security (restricted) i existenci namespace. Proto se namespace vytvoří těsně předtím.

--dry-run=client--dry-run=server
kde se kontrolujejen v kubectlv API serveru
ověří schéma, admission (PSS), existenci namespaceneano
typické použitígenerování YAML (-o yaml)kontrola před nasazením

6. Nasazení a rolling update

kubectl apply -k k8s/ pošle do clusteru Namespace, Deployment, Service a Ingress. Změnil se jen tag image, takže Kubernetes spustí rolling update:

  1. spustí nový pod s novou verzí,
  2. počká, až projde readiness probe (/healthz),
  3. teprve pak ukončí starý pod (maxUnavailable: 0). Web ani na chvíli nevypadne.

rollout status čeká, až se výměna dokončí. Když nový pod do 120 s nenaběhne, skript skončí chybou a starý pod běží dál.

7. Výpis stavu

kubectl get pods,svc,ingress ukáže výsledný stav.

Celý tok

text
Markdown ─(Hugo)─▶ HTML ─(buildah)─▶ image:tag ─(import)─▶ containerd k3s
   ─(kubectl apply -k)─▶ rolling update ─▶ nový pod ─▶ Traefik ─▶ cloudflared ─▶ svejklab.dev

Kontrola a návrat k předchozí verzi

shell @ server
kubectl -n homelab-docs get deploy homelab-docs -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
kubectl -n homelab-docs rollout history deploy/homelab-docs
kubectl -n homelab-docs rollout undo deploy/homelab-docs

rollout undo vrátí předchozí verzi. Image starších verzí zůstávají v containerd, takže návrat je okamžitý.

Proč buildah a ne Docker

k3s používá containerd, ne Docker. buildah sestaví image bez běžícího démona, takže na serveru nezabírá RAM. Hotový image se do containerd přenese jako archiv. Registry není potřeba, proto má Deployment imagePullPolicy: Never.

Úprava obsahu

Upravit .md v site/content/ a znovu spustit ./deploy.sh. Tag se bez parametru vygeneruje z data a času a Deployment udělá rolling update bez výpadku (maxUnavailable: 0).

Lokální náhled na Macu (brew install hugo):

shell @ mac
cd homelab-docs/site && hugo server

Bezpečnost podu

NastaveníProč
namespace pod-security…/enforce: restrictedcluster odmítne pod, který nesplňuje přísný profil
runAsNonRoot, uid 101nginx neběží jako root
readOnlyRootFilesystem + emptyDir /tmpkontejner nemůže měnit svůj systém souborů
capabilities: drop ALL, allowPrivilegeEscalation: falsežádná zvláštní práva jádra
seccompProfile: RuntimeDefaultomezené systémové volání
automountServiceAccountToken: falsepod nemá token k Kubernetes API
requests/limitsbez swapu je limit paměti nutnost
readiness/liveness na /healthzKubernetes pozná, že web žije
CSP a další hlavičky v nginxprohlížeč nespustí cizí skripty

Přístup z internetu

Web je veřejně na https://svejklab.dev přes Cloudflare Tunnel. Postup, výběr domény a co se při tom pokazilo, je v kapitole Veřejný web.

Lokálně v domácí síti funguje dál i http://<ip-serveru>/.