Docker er driftsformen, ikke bare en testrigg

Announcer skal kjøre standalone på klienten som er koblet til anlegget, i
Docker – aldri på utviklingsmaskina. install.sh installerte nativt med apt
og uv, og det passer ikke lenger.

- install.sh er nå en Docker-bootstrap: installerer Docker om den mangler,
  lager .env med riktig bruker-ID og portbinding, oppretter data/, bygger,
  starter, og setter ufw. Den rydder også bort gamle native enheter, både
  announcer.service og sg-announcer.service.
- Dockerfile kopierer koden inn istedenfor å låne den fra et bind-mount, så
  det som kjører i drift er uavhengig av hva som ligger i mappa.
- Alt som skal overleve en ny container ligger i data/ på verten. Bind-mount
  og ikke navngitt volum, med vilje: da kan man lese
  data/logs/announcements.jsonl og slippe egne jingler i data/chimes/ uten å
  gå inn i containeren, og eierskapet følger brukeren istedenfor root.
- Testriggen er flyttet til compose.test.yaml, med eget prosjektnavn, så den
  ikke kan forveksles med eller forstyrre drift.

Lyden ut av containeren krever tre ting, og de er dokumentert i AGENTS.md:
hele XDG_RUNTIME_DIR montert inn (ikke bare pipewire-socketen – PulseAudio
trenger pulse/native og en cookie i samme mappe, og da virker begge
lydservere), user: PUID:PGID lik eieren av socketen, og en innlogget grafisk
økt. Containeren starter ved boot, altså før innlogging, så avspilling feiler
til noen logger inn – det retter seg selv, siden player_cmd() slås opp per
melding. Verifisert: lyden spiller fra driftscontaineren.

Brannmuren er den viktige endringen fra nativ tid. Dockers publiserte porter
settes opp i nat-tabellen, før ufws regler, så en ufw-regel på porten gjør
ingenting. Tilgangen styres nå av BIND_IP i .env, og install.sh setter den til
NetBird-adressen når wt0 finnes. ufw settes fortsatt for ssh, men er ikke
involvert i tilgangen til announcer.

Testriggen bindes nå til 127.0.0.1. Den sto med PIN 1234 publisert på
0.0.0.0, og gikk altså forbi ufw ut på LAN-et.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent 2026-10-08 16:07:59 +02:00
parent 6aaa98d8b1
commit 6c1253f54c
7 changed files with 372 additions and 177 deletions

View file

@ -1,15 +1,20 @@
#!/usr/bin/env bash
# Installerer Announcer på en Debian/Ubuntu-maskin (testet mot Linux Mint).
# Setter opp Announcer på klienten som er koblet til høyttaleranlegget.
# Debian/Ubuntu/Mint. Appen kjører i Docker; dette skriptet ordner Docker,
# .env, datamapper, brannmur og autostart.
#
# git clone https://git.1x1.no/kent/announcer.git
# cd announcer && ./install.sh
#
# Kjør som vanlig bruker (ikke sudo); skriptet spør om passord der det trengs.
# Valgfritt: ANNOUNCER_PIN=1234 ./install.sh -> sida krever PIN
# ANNOUNCER_PORT=8090 ./install.sh -> annen port
# ANNOUNCER_TITLE="Utrop" ./install.sh
# Resten av konfigurasjonen hører hjemme i config.toml, se config.example.toml.
# Kan kjøres på nytt; da oppdateres avhengigheter og tjenesten startes om.
# Valgfritt:
# ANNOUNCER_PIN=1234 ./install.sh sida krever PIN
# ANNOUNCER_PORT=8090 ./install.sh annen port
# ANNOUNCER_TITLE="Utrop" ./install.sh annen tittel
# BIND_IP=127.0.0.1 ./install.sh bare maskina selv
#
# Kan kjøres på nytt; da bygges bildet om og containeren startes om. En .env
# som finnes blir ikke overskrevet.
set -euo pipefail
if [[ $EUID -eq 0 ]]; then
@ -17,129 +22,150 @@ if [[ $EUID -eq 0 ]]; then
exit 1
fi
APP="announcer"
OLD_APP="sg-announcer" # het dette før; enheten ryddes bort nedenfor
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PORT="${ANNOUNCER_PORT:-8090}"
PIN="${ANNOUNCER_PIN:-}"
TITLE="${ANNOUNCER_TITLE:-}"
UNIT_DIR="$HOME/.config/systemd/user"
UNIT="$UNIT_DIR/$APP.service"
cd "$REPO_DIR"
step() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
warn() { printf '\033[33m! %s\033[0m\n' "$*"; }
step "Systempakker (apt)"
sudo apt-get update -qq
sudo apt-get install -y -qq git curl ca-certificates ffmpeg pulseaudio-utils
# pw-play finnes i pipewire-bin på Mint 22 / Ubuntu 24.04. På eldre (PulseAudio)
# faller appen tilbake til paplay, så dette er ikke kritisk.
sudo apt-get install -y -qq pipewire-bin 2>/dev/null || true
# --------------------------------------------------------------------------
step "Rydder bort eventuell nativ installasjon"
# Tidligere versjoner kjørte appen direkte som en systemd-brukerenhet. To
# instanser ville slåss om porten, så den gamle skal bort.
for old in sg-announcer announcer; do
unit="$HOME/.config/systemd/user/$old.service"
if [[ -f "$unit" ]]; then
systemctl --user disable --now "$old" >/dev/null 2>&1 || true
rm -f "$unit"
echo "fjernet $old.service (nativ)"
fi
done
systemctl --user daemon-reload 2>/dev/null || true
step "uv (Python-pakkehåndterer)"
if command -v uv >/dev/null 2>&1; then
UV="$(command -v uv)"
elif [[ -x "$HOME/.local/bin/uv" ]]; then
UV="$HOME/.local/bin/uv"
else
curl -LsSf https://astral.sh/uv/install.sh | sh
UV="$HOME/.local/bin/uv"
# --------------------------------------------------------------------------
step "Docker"
if ! command -v docker >/dev/null 2>&1; then
echo "installerer Docker …"
curl -fsSL https://get.docker.com | sudo sh
fi
echo "uv: $UV ($("$UV" --version))"
if ! id -nG "$USER" | tr ' ' '\n' | grep -qx docker; then
sudo usermod -aG docker "$USER"
warn "La $USER i docker-gruppa. Du må logge ut og inn igjen,"
warn "og så kjøre ./install.sh på nytt. Stopper her."
exit 0
fi
if ! docker info >/dev/null 2>&1; then
warn "Docker svarer ikke. Er tjenesten startet? sudo systemctl enable --now docker"
exit 1
fi
docker --version
docker compose version | head -1
step "Python 3.13 og avhengigheter"
cd "$REPO_DIR"
"$UV" sync
mkdir -p cache chimes voices logs instance
step "Laster ned Piper-stemmene (lokal reserve, ca. 600 MB, kun første gang)"
"$UV" run python -c "import logging, announcer; logging.basicConfig(level=logging.INFO, format='%(message)s'); announcer.ensure_voices()"
step "Rydder bort gammel tjeneste"
# Tjenesten het sg-announcer før. Uten dette slåss to enheter om porten.
if [[ -f "$UNIT_DIR/$OLD_APP.service" ]]; then
systemctl --user disable --now "$OLD_APP" >/dev/null 2>&1 || true
rm -f "$UNIT_DIR/$OLD_APP.service"
echo "fjernet $OLD_APP.service"
# --------------------------------------------------------------------------
step "Oppsett (.env)"
# BIND_IP er det som avgjør hvem som når sida. ufw gjør det ikke: Dockers
# publiserte porter settes opp i nat-tabellen og går forbi ufws regler.
NB_IP="$(ip -4 addr show wt0 2>/dev/null | awk '/inet /{sub(/\/.*/,"",$2);print $2;exit}')"
if [[ -n "${BIND_IP:-}" ]]; then
:
elif [[ -n "$NB_IP" ]]; then
BIND_IP="$NB_IP"
echo "NetBird funnet – binder til $BIND_IP, så bare VPN-et når sida."
else
echo "ingenting å rydde."
BIND_IP="0.0.0.0"
fi
step "systemd-brukertjeneste"
mkdir -p "$UNIT_DIR"
{
cat <<UNIT
[Unit]
Description=Announcer – TTS-utrop på høyttaleranlegget
After=pipewire.service pulseaudio.service
if [[ -f .env ]]; then
echo ".env finnes allerede, lar den stå. Slett den for å lage en ny."
else
cat > .env <<ENV
# Laget av install.sh $(date +%Y-%m-%d). Se .env.example for forklaringer.
PUID=$(id -u)
PGID=$(id -g)
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
BIND_IP=$BIND_IP
ANNOUNCER_PORT=${ANNOUNCER_PORT:-8090}
ANNOUNCER_SINK=${ANNOUNCER_SINK:-}
ANNOUNCER_TITLE=${ANNOUNCER_TITLE:-}
ANNOUNCER_PUBLIC_URL=${ANNOUNCER_PUBLIC_URL:-}
ANNOUNCER_PIN=${ANNOUNCER_PIN:-}
ANNOUNCER_JWT_SECRET=${ANNOUNCER_JWT_SECRET:-}
ANNOUNCER_OIDC_CLIENT_SECRET=${ANNOUNCER_OIDC_CLIENT_SECRET:-}
ANNOUNCER_SECRET_KEY=
ENV
chmod 600 .env # PIN og nøkler står i klartekst
echo "skrev .env (PUID=$(id -u), BIND_IP=$BIND_IP)"
fi
[Service]
WorkingDirectory=$REPO_DIR
ExecStart=$UV run announcer.py
Environment=ANNOUNCER_PORT=$PORT
UNIT
if [[ -n "$PIN" ]]; then
echo "Environment=ANNOUNCER_PIN=$PIN"
else
echo "# Environment=ANNOUNCER_PIN=1234"
step "Datamapper"
mkdir -p data/{voices,cache,chimes,logs,instance}
[[ -f data/chimes/README.txt ]] || cp -n chimes/README.txt data/chimes/ 2>/dev/null || true
echo "data/ – stemmer, buffer, egne jingler, logg og sesjonsnøkkel"
if [[ ! -f data/config.toml ]]; then
echo "ingen data/config.toml: sida er åpen for alle som når den."
echo " vil du ha innlogging: cp config.example.toml data/config.toml"
fi
# --------------------------------------------------------------------------
step "Bygger og starter"
docker compose up -d --build
step "Venter på at tjenesten svarer"
# Første oppstart laster ned Piper-stemmene (~670 MB) før den svarer.
PORT="$(grep -E '^ANNOUNCER_PORT=' .env | cut -d= -f2)"
PROBE_IP="$BIND_IP"; [[ "$PROBE_IP" == "0.0.0.0" ]] && PROBE_IP=127.0.0.1
for i in $(seq 1 60); do
if curl -fsS -o /dev/null "http://$PROBE_IP:$PORT/api/config" 2>/dev/null; then
echo "svarer etter ~$((i*5)) s"
break
fi
if [[ -n "$TITLE" ]]; then
echo "Environment=ANNOUNCER_TITLE=$TITLE"
else
echo "# Environment=ANNOUNCER_TITLE=Utrop"
fi
cat <<'UNIT'
# Environment=ANNOUNCER_SINK=alsa_output.xxx
# Hemmeligheter hører hjemme her og ikke i config.toml:
# Environment=ANNOUNCER_JWT_SECRET=...
# Environment=ANNOUNCER_OIDC_CLIENT_SECRET=...
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
UNIT
} > "$UNIT"
chmod 600 "$UNIT" # PIN og nøkler står i klartekst her
systemctl --user daemon-reload
systemctl --user enable --now "$APP"
systemctl --user restart "$APP"
[[ $i -eq 1 ]] && echo "laster ned stemmer, dette tar noen minutter første gang …"
[[ $i -eq 60 ]] && warn "svarte ikke innen 5 min. Se: docker compose logs -f"
sleep 5
done
# --------------------------------------------------------------------------
step "Brannmur"
if ip link show wt0 >/dev/null 2>&1; then
# NetBird er installert: slipp inn kun via VPN-grensesnittet, blokker alt annet.
# Merk at ufw *ikke* styrer tilgangen til announcer – det gjør BIND_IP i
# .env. ufw settes her bare for maskinens øvrige porter.
if [[ -n "$NB_IP" ]]; then
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on wt0 to any port 22 proto tcp comment "ssh via netbird"
sudo ufw allow in on wt0 to any port 22022 proto tcp comment "netbird ssh-server"
sudo ufw allow in on wt0 to any port "$PORT" proto tcp comment "$APP via netbird"
sudo ufw delete allow "$PORT/tcp" >/dev/null 2>&1 || true
sudo ufw --force enable
echo "ufw aktiv: port $PORT, 22 og 22022 er åpne kun på wt0 (NetBird)."
echo "Vil du også nå sida fra lokalt LAN uten VPN:"
echo " sudo ufw allow from <lan-subnett> to any port $PORT proto tcp"
elif sudo ufw status 2>/dev/null | grep -q "Status: active"; then
sudo ufw allow "$PORT/tcp" comment "$APP"
echo "ufw aktiv: ssh kun på wt0."
echo "announcer er bundet til $BIND_IP:$PORT – det er NetBird-adressen,"
echo "så sida nås bare over VPN-et. ufw er ikke involvert i det."
elif command -v ufw >/dev/null 2>&1 && sudo ufw status 2>/dev/null | grep -q "Status: active"; then
warn "ufw er aktiv, men styrer ikke Docker-porter."
warn "Vil du begrense tilgangen: sett BIND_IP i .env og kjør docker compose up -d."
else
echo "ufw er ikke aktiv, ingen regel nødvendig."
echo "ufw er ikke aktiv."
[[ "$BIND_IP" == "0.0.0.0" ]] && warn "Sida er åpen på alle nett maskina er på."
fi
sleep 2
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
# --------------------------------------------------------------------------
IP="$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{sub(/\/.*/,"",$2);print $2;exit}')"
step "Ferdig"
systemctl --user --no-pager status "$APP" | head -5 || true
docker compose ps
cat <<MSG
Åpne http://${IP:-<denne-maskinens-ip>}:$PORT fra hva som helst på nettet.
Åpne http://${BIND_IP/0.0.0.0/${IP:-<denne-maskinens-ip>}}:$PORT
Uten config.toml er sida åpen for alle som når porten. Vil du ha innlogging:
cp config.example.toml config.toml og les kommentarene der.
Mot crew-panelet: docs/crew-integration.md
Lyden spilles i din innloggede økt, så maskinen må være logget inn grafisk.
Containeren starter ved boot, altså før innlogging – da feiler avspilling,
men den retter seg selv ved første utrop etter at du er logget inn.
Tips for en laptop på arrangementet:
- Lyden spilles i din innloggede økt, så maskinen må være logget inn.
Slå på automatisk innlogging (Innstillinger -> Påloggingsvindu).
- Slå på automatisk innlogging (Innstillinger -> Påloggingsvindu).
- Hindre at den sover med lokket igjen:
sudo sed -i 's/^#\?HandleLidSwitch=.*/HandleLidSwitch=ignore/' /etc/systemd/logind.conf
sudo systemctl restart systemd-logind
- Logg: journalctl --user -u $APP -f
Drift:
docker compose logs -f følg loggen
docker compose restart etter endringer i data/config.toml
docker compose up -d --build etter endringer i koden
cat data/logs/announcements.jsonl hvem som sendte hva
MSG