diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4177d60 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +# Do image patří jen to, co appka za běhu potřebuje. +.git/ +.github/ +.gitea/ +.venv/ +.build/ +.pytest_cache/ +__pycache__/ +dist/ +tests/ +deploy/ +*.db +*.spec +build.sh +dice.py +verze.txt +README.md +requirements*.in +requirements-dev.txt +requirements-build.txt diff --git a/README.md b/README.md index a9b4069..77044e5 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,84 @@ hlášku vysypal varování, že takhle se to nemá, a měl by pravdu. Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi může zapisovat. +## Nasazení na server + +Kdo nechce každý večer spouštět binárku na notebooku, může to nechat běžet +pořád. Obojí níž počítá **jen s domácí sítí** — appka nemá přihlašování +a ani jedno nasazení ji nijak nezabezpečuje. Do internetu to nepatří. + +### Proxmox (LXC) + +Jeden příkaz v terminálu uzlu Proxmoxu, jako root. Založí kontejner, +nastaví ho, stáhne poslední vydanou binárku, ověří kontrolní součet a zapne +službu: + +```bash +bash -c "$(curl -fsSL https://gitea.spacilovi.eu/david-spacil/dice-counter/raw/branch/main/deploy/pve-kostky.sh)" +``` + +Na konci vypíše adresu, na které počitadlo poslouchá. Výchozí nastavení je +1 jádro, 512 MB, 4 GB disku a Debian 13; úložiště si najde samo. Přebíjí se +proměnnými před příkazem: + +```bash +MEMORY=1024 STORAGE=local-zfs PORT=8080 bash -c "$(curl -fsSL ...)" +``` + +| Proměnná | Výchozí | +|---|---| +| `CTID` | první volné číslo | +| `CT_HOSTNAME` | `kostky` | +| `CORES`, `MEMORY`, `DISK` | `1`, `512`, `4` | +| `STORAGE` | první aktivní úložiště pro kontejnery | +| `TEMPLATE_STORAGE`, `BRIDGE` | `local`, `vmbr0` | +| `PORT` | `8000` | +| `UNPRIVILEGED`, `OSVERSION` | `1`, `13` | +| `NESTING` | `1` — systemd v Debianu 13 ho potřebuje | +| `VERSION` | poslední vydání | + +Aktualizace na novější vydání je ten samý příkaz s číslem kontejneru: + +```bash +CTID=123 MODE=update bash -c "$(curl -fsSL ...)" +``` + +Podoba je odkoukaná od [community-scripts.org](https://community-scripts.org), +ale nic z jejich frameworku se nestahuje — jejich `build.func` si instalační +skript hledá natvrdo ve vlastním repozitáři, takže mimo něj nefunguje. + +Instalátor, který běží uvnitř kontejneru, je ve skriptu vložený jako text — +přes rouru z curlu na disku hostitele žádný druhý soubor není. O Proxmoxu nic +neví, takže se dá použít i v kontejneru, který sis založil sám: + +```bash +bash pve-kostky.sh instalator > install.sh +``` + +Je idempotentní; druhé spuštění jen vymění binárku za nejnovější. + +### Docker + +```bash +cd deploy && docker compose up -d +``` + +Staví se ze zdrojáků, takže image jde sestavit pro amd64 i arm64 a nezávisí +na tom, jestli release pro danou architekturu proběhl. Databáze leží +v pojmenovaném svazku, appka běží pod nerootovým uživatelem. + +**Pozor na síť.** Compose schválně používá `network_mode: host`. V bridge +režimu kontejner uvnitř vidí adresu jako `172.17.0.2`, tabule ji poctivě +nabídne a vygeneruje na ni QR kód — jenže z telefonu je nedosažitelná. +Kdo bridge potřebuje, musí nastavit `DICE_HOST` na adresu hostitele v LAN. + +| | LXC | Docker | +|---|---|---| +| Odkud | vydaná binárka z releasu | zdrojáky | +| Databáze | `/var/lib/kostky/dice.db` | svazek `kostky-data` | +| Aktualizace | `pve-kostky.sh update ` | `docker compose build --pull` | +| Adresa a QR | funguje samo | potřeba síť hostitele | + ## Když se telefon nepřipojí Adresy se hledají při každém načtení tabule, takže přechod z domácí WiFi na @@ -154,6 +232,7 @@ při remíze na prvním místě se hraje dál. | `build.sh`, `kostky.spec` | stavba binárky | | `.gitea/workflows/` | testy a linuxová binárka doma | | `.github/workflows/` | binárky pro Windows a macOS | +| `deploy/` | nasazení na server: Proxmox LXC a Docker | Terminálová verze zůstává funkční jako záloha: diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..4342e00 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,47 @@ +# Počitadlo kostek jako kontejner. +# +# Staví se ze zdrojáků, ne z vydané binárky: image jde pak sestavit pro +# amd64 i arm64 a nezávisí na tom, jestli pro danou architekturu release +# proběhl. Závislosti jsou zamčené v requirements.txt, takže je sestavení +# opakovatelné. +# +# docker build -f deploy/Dockerfile -t kostky . +# +# Sestavuje se z kořene repozitáře, ne z deploy/. +FROM python:3.11-slim + +# Verze se jinak nemá kde vzít — v image není git ani zabalená binárka. +ARG VERSION=neznámá + +RUN useradd --system --uid 1000 --home-dir /app --shell /usr/sbin/nologin kostky + +WORKDIR /app + +# Závislosti zvlášť a dřív než kód, ať se vrstva s pipem přestavuje jen tehdy, +# když se opravdu změnily. +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY console.py core.py net.py stats.py storage.py version.py web.py ./ +COPY templates/ templates/ +COPY static/ static/ +RUN echo "$VERSION" > verze.txt + +# Adresář pro databázi musí vzniknout už tady a patřit správnému uživateli — +# svazek, který Docker založí sám, by jinak patřil rootovi a appka běžící pod +# kostky by do něj nezapsala. +RUN install -d -o kostky -g kostky /data +VOLUME ["/data"] + +ENV DICE_DB=/data/dice.db \ + DICE_PORT=8000 + +EXPOSE 8000 +USER kostky + +# Bez curlu, ten ve slim image není. Stačí, že stránka odpoví — sahá i do +# databáze, takže se tím kontroluje i ta. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import os,urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ.get('DICE_PORT','8000') + '/')" + +CMD ["python", "web.py"] diff --git a/deploy/compose.yaml b/deploy/compose.yaml new file mode 100644 index 0000000..afffe17 --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,45 @@ +# Počitadlo kostek v Dockeru. +# +# cd deploy && docker compose up -d +# +# Počítá se s domácí sítí. Appka nemá přihlašování, tohle ji nijak +# nezabezpečuje a do internetu nepatří. + +services: + kostky: + build: + context: .. + dockerfile: deploy/Dockerfile + args: + VERSION: ${VERSION:-vlastní sestavení} + image: kostky:latest + container_name: kostky + restart: unless-stopped + + # Python jako PID 1 nesklízí osiřelé procesy a hůř reaguje na signály. + init: true + + # Síť hostitele schválně. V bridge režimu kontejner uvnitř vidí adresu + # jako 172.17.0.2, tabule ji poctivě nabídne a vygeneruje na ni QR kód — + # jenže z telefonu je nedosažitelná. Se sítí hostitele appka najde tu + # adresu, na kterou se telefon opravdu dovolá. + # + # Kdo bridge potřebuje, ať zakomentuje řádek níž, odkomentuje ports + # a nastaví DICE_HOST na adresu hostitele v LAN: + # + # ports: + # - "8000:8000" + # environment: + # DICE_HOST: 192.168.1.10 + network_mode: host + + environment: + DICE_PORT: 8000 + DICE_DB: /data/dice.db + + volumes: + - kostky-data:/data + +volumes: + # Zálohu si stáhneš rovnou z appky přes /export, takže se sem nemusí lézt. + kostky-data: diff --git a/deploy/pve-kostky.sh b/deploy/pve-kostky.sh new file mode 100755 index 0000000..d7ecffb --- /dev/null +++ b/deploy/pve-kostky.sh @@ -0,0 +1,336 @@ +#!/usr/bin/env bash +# Založí na Proxmoxu LXC kontejner s počitadlem kostek. +# +# Pouští se v terminálu uzlu Proxmoxu jako root, jedním příkazem: +# +# bash -c "$(curl -fsSL https://gitea.spacilovi.eu/david-spacil/dice-counter/raw/branch/main/deploy/pve-kostky.sh)" +# +# Aktualizace stávajícího kontejneru na novější vydání: +# +# CTID=123 MODE=update bash -c "$(curl -fsSL .../pve-kostky.sh)" +# +# Ze staženého souboru jde i obojí postaru: `bash pve-kostky.sh` a +# `bash pve-kostky.sh update 123`. +# +# Podoba je odkoukaná od community-scripts.org, ale nic z jejich frameworku +# se tu nestahuje — jejich build.func si instalační skript hledá natvrdo ve +# vlastním repozitáři, takže mimo něj nefunguje. +# +# Počítá se s domácí sítí. Appka nemá přihlašování a tenhle skript ji nijak +# nezabezpečuje; do internetu ji nepouštěj. +set -euo pipefail + +APP="Kostky" +GITEA="${GITEA:-https://gitea.spacilovi.eu}" +REPO="${REPO:-david-spacil/dice-counter}" + +# Výchozí hodnoty. Všechny se dají přebít proměnnou prostředí, třeba: +# MEMORY=1024 STORAGE=local-zfs bash -c "$(curl -fsSL ...)" +CT_HOSTNAME="${CT_HOSTNAME:-kostky}" +CORES="${CORES:-1}" +MEMORY="${MEMORY:-512}" +DISK="${DISK:-4}" +STORAGE="${STORAGE:-}" +TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" +BRIDGE="${BRIDGE:-vmbr0}" +PORT="${PORT:-8000}" +UNPRIVILEGED="${UNPRIVILEGED:-1}" +OSVERSION="${OSVERSION:-13}" +NESTING="${NESTING:-1}" +VERSION="${VERSION:-}" # prázdné = poslední vydání + +# --- výpisy ------------------------------------------------------------------ + +# Bez tohohle Ctrl+C zabije jen rozdělaný podproces a skript jede vesele dál +# — třeba rovnou zakládat kontejner, na který se čekat nemá. +trap 'echo; echo -e "\e[1;31m ✗\e[0m Přerušeno." >&2; exit 130' INT TERM + +msg() { echo -e "\e[1;34m →\e[0m $*"; } +ok() { echo -e "\e[1;32m ✓\e[0m $*"; } +die() { echo -e "\e[1;31m ✗\e[0m $*" >&2; exit 1; } + +header() { + echo + echo -e "\e[1;36m $APP\e[0m — počitadlo kostek do LXC" + echo -e " \e[2m$GITEA/$REPO\e[0m" + echo +} + +# --- instalátor, který poběží uvnitř kontejneru ------------------------------ +# +# Je tu jako text schválně. Skript se pouští i rourou z curlu, kdy na disku +# hostitele žádný soubor není a nebylo by co poslat dovnitř. Uvozovky kolem +# 'INSTALL' jsou podstatné: nic z toho se tady nesmí rozvinout, všechny +# proměnné patří až tomu, co poběží v kontejneru. +# +# Vypsat se dá i samostatně: `bash pve-kostky.sh instalator > install.sh`, +# což se hodí, když chceš appku dostat do kontejneru, který sis založil sám. +installer() { +cat <<'INSTALL' +#!/usr/bin/env bash +# Instalace počitadla uvnitř kontejneru. O Proxmoxu nic neví, takže se dá +# pustit v jakémkoli Debianu. Je idempotentní — druhé spuštění jen vymění +# binárku za nejnovější. +set -euo pipefail + +GITEA="${GITEA:-https://gitea.spacilovi.eu}" +REPO="${REPO:-david-spacil/dice-counter}" +PORT="${PORT:-8000}" +VERSION="${VERSION:-}" + +USER_NAME=kostky +HOME_DIR=/opt/kostky +DATA_DIR=/var/lib/kostky + +msg() { echo -e "\e[1;34m →\e[0m $*"; } +ok() { echo -e "\e[1;32m ✓\e[0m $*"; } +die() { echo -e "\e[1;31m ✗\e[0m $*" >&2; exit 1; } + +case "$(dpkg --print-architecture)" in + amd64) ASSET=kostky-linux-x86_64 ;; + arm64) ASSET=kostky-linux-arm64 ;; + *) die "Pro architekturu $(dpkg --print-architecture) binárka není." ;; +esac + +msg "Doinstalovávám, co je potřeba ke stažení" +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq curl ca-certificates >/dev/null +ok "Základ je hotový" + +if [ -z "$VERSION" ]; then + msg "Zjišťuji poslední vydanou verzi" + # Jen tag; adresa přílohy se z něj složí, takže tu nepotřebujeme jq. + VERSION=$(curl -fsSL "$GITEA/api/v1/repos/$REPO/releases/latest" \ + | grep -o '"tag_name":[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4) + [ -n "$VERSION" ] || die "Nepodařilo se zjistit poslední verzi z $GITEA." +fi +ok "Instaluje se $VERSION ($ASSET)" + +BASE="$GITEA/$REPO/releases/download/$VERSION" +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +msg "Stahuji binárku" +curl -fsSL -o "$TMP/$ASSET" "$BASE/$ASSET" \ + || die "$ASSET pro $VERSION na releasu není. Vydání pro tuhle architekturu možná ještě neproběhlo." +curl -fsSL -o "$TMP/$ASSET.sha256" "$BASE/$ASSET.sha256" \ + || die "Chybí kontrolní součet — radši nic neinstaluju." + +msg "Ověřuji kontrolní součet" +( cd "$TMP" && sha256sum -c "$ASSET.sha256" >/dev/null ) \ + || die "Kontrolní součet nesedí. Stažený soubor zahazuju." +ok "Součet sedí" + +id -u "$USER_NAME" >/dev/null 2>&1 \ + || useradd --system --home-dir "$HOME_DIR" --shell /usr/sbin/nologin "$USER_NAME" + +install -d -o root -g root -m 755 "$HOME_DIR" + +# Zastavit se dá jen to, co běží — při první instalaci služba ještě není. +if systemctl is-active --quiet kostky; then + systemctl stop kostky +fi + +install -o root -g root -m 755 "$TMP/$ASSET" "$HOME_DIR/kostky" +echo "$VERSION" > "$HOME_DIR/verze" +ok "Binárka je na místě" + +msg "Zapisuji službu" +cat > /etc/systemd/system/kostky.service </dev/null && break + sleep 1 +done +curl -fsS -o /dev/null "http://127.0.0.1:$PORT/" \ + || die "Služba nastartovala, ale neodpovídá. Mrkni na journalctl -u kostky." + +# Verzi bereme ze souboru, ne z binárky. Starší vydání --version neumí +# a místo výpisu by nastartovala server, který by tenhle skript zavěsil. +ok "Počitadlo $(cat "$HOME_DIR/verze") odpovídá na portu $PORT" +INSTALL +} + +# --- společné části ---------------------------------------------------------- + +install_into() { + local ctid="$1" tmp + tmp=$(mktemp) + installer > "$tmp" + + msg "Posílám instalátor do kontejneru $ctid" + pct push "$ctid" "$tmp" /root/kostky-install.sh --perms 0755 + rm -f "$tmp" + + pct exec "$ctid" -- env PORT="$PORT" GITEA="$GITEA" REPO="$REPO" \ + VERSION="$VERSION" bash /root/kostky-install.sh +} + +address_of() { + pct exec "$1" -- hostname -I 2>/dev/null | awk '{print $1}' +} + +hotovo() { + local ctid="$1" ip jmeno + ip=$(address_of "$ctid") + jmeno=$(pct config "$ctid" | awk -F': ' '/^hostname:/ {print $2}') + + echo + ok "Hotovo. Kontejner $ctid ($jmeno) běží." + echo + echo -e " Zápisník na notebook: \e[1;32mhttp://${ip}:${PORT}/board\e[0m" + echo -e " Zadávání z telefonu: \e[1;32mhttp://${ip}:${PORT}/\e[0m" + echo + echo -e " \e[2mDatabáze: /var/lib/kostky/dice.db (uvnitř kontejneru)" + echo -e " Log: pct exec $ctid -- journalctl -u kostky -f" + echo -e " Konzole: pct enter $ctid" + echo -e " Zrušit: pct stop $ctid && pct destroy $ctid\e[0m" + echo +} + +# --- co se má vlastně dělat -------------------------------------------------- +# +# Rourou z curlu se argumenty předávají mizerně, tak jde všechno i proměnnou: +# MODE=update CTID=123. Ze souboru funguje i `bash pve-kostky.sh update 123`. +MODE="${MODE:-${1:-create}}" +[ "$MODE" = "update" ] && CTID="${CTID:-${2:-}}" + +if [ "$MODE" = "instalator" ]; then + installer + exit 0 +fi + +command -v pct >/dev/null || die "Tohle patří na uzel Proxmoxu — pct tu není." +[ "$(id -u)" -eq 0 ] || die "Spusť to jako root." + +# --- aktualizace ------------------------------------------------------------- + +if [ "$MODE" = "update" ]; then + [ -n "${CTID:-}" ] || die "Řekni který: CTID=123 MODE=update ..." + pct status "$CTID" >/dev/null 2>&1 || die "Kontejner $CTID neexistuje." + + header + if [ "$(pct status "$CTID")" != "status: running" ]; then + msg "Startuji kontejner" + pct start "$CTID" + sleep 5 + fi + + install_into "$CTID" + hotovo "$CTID" + exit 0 +fi + +# --- nový kontejner ---------------------------------------------------------- + +header + +# Úložiště se nehádá. local-lvm na ZFS instalacích neexistuje a pct create by +# spadlo až po půlce práce, tak se radši zeptáme systému, co tu opravdu je. +if [ -z "$STORAGE" ]; then + STORAGE=$(pvesm status -content rootdir 2>/dev/null \ + | awk 'NR>1 && $3=="active" {print $1; exit}') + [ -n "$STORAGE" ] || die "Nenašel jsem úložiště pro kontejnery. Zadej STORAGE=; co máš, ukáže pvesm status." + msg "Úložiště: $STORAGE (nalezeno automaticky)" +else + pvesm status -content rootdir 2>/dev/null | awk 'NR>1 {print $1}' | grep -qx "$STORAGE" \ + || die "Úložiště $STORAGE pro kontejnery nesedí. Co máš, ukáže: pvesm status -content rootdir" + msg "Úložiště: $STORAGE" +fi + +CTID="${CTID:-$(pvesh get /cluster/nextid)}" +msg "Kontejner dostane číslo $CTID" + +# Architektura uzlu, ne první šablona v seznamu. pveam nabízí amd64 i arm64 +# vedle sebe a bez tohohle filtru by výběr padl na tu abecedně poslední. +ARCH=$(dpkg --print-architecture) +SABLONA="debian-${OSVERSION}-standard[^[:space:]]*_${ARCH}\.tar\.[a-z]*" + +msg "Hledám šablonu Debianu $OSVERSION pro $ARCH" + +# Nejdřív co už na uzlu leží — stahovat 120 MB znovu je zbytečné. +TEMPLATE=$(pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -o "$SABLONA" | sort -V | tail -1) + +if [ -n "$TEMPLATE" ]; then + ok "Šablona $TEMPLATE (už je na uzlu)" +else + pveam update >/dev/null 2>&1 || true + TEMPLATE=$(pveam available --section system 2>/dev/null | grep -o "$SABLONA" | sort -V | tail -1) + [ -n "$TEMPLATE" ] || die "Šablona pro Debian $OSVERSION a $ARCH není k dispozici." + + msg "Stahuji $TEMPLATE (asi 120 MB, chvíli to trvá)" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null \ + || die "Stažení šablony selhalo." + ok "Šablona $TEMPLATE" +fi + +# Nesting: Debian 13 veze systemd 257 a ten v neprivilegovaném kontejneru bez +# něj nedostane, co potřebuje — Proxmox na to při startu sám upozorňuje. +# Stejnou výchozí hodnotu mají i community-scripts. +msg "Zakládám kontejner" +pct create "$CTID" "$TEMPLATE_STORAGE:vztmpl/$TEMPLATE" \ + --hostname "$CT_HOSTNAME" \ + --cores "$CORES" \ + --memory "$MEMORY" \ + --swap 256 \ + --rootfs "$STORAGE:$DISK" \ + --net0 "name=eth0,bridge=$BRIDGE,ip=dhcp" \ + --unprivileged "$UNPRIVILEGED" \ + --features "nesting=$NESTING" \ + --ostype debian \ + --onboot 1 \ + --description "$APP — $GITEA/$REPO" >/dev/null +ok "Kontejner $CTID založen" + +msg "Startuji" +pct start "$CTID" + +msg "Čekám na síť" +for _ in $(seq 60); do + if [ -n "$(address_of "$CTID")" ] \ + && pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then + break + fi + sleep 2 +done +[ -n "$(address_of "$CTID")" ] || die "Kontejner nedostal adresu. Sedí bridge $BRIDGE?" +ok "Adresa $(address_of "$CTID")" + +install_into "$CTID" +hotovo "$CTID"