name: Deploy to TrueNAS # ───────────────────────────────────────────────────────────────────────────── # HOMELAB APP TEMPLATE — push-to-deploy workflow. # Proven on InspectFlow + CannaManage. See homelab-release-runbook.md. # # Before first push, replace these placeholders everywhere in the repo: # __PROJECT__ compose project name + container prefix (e.g. "myapp") # __FRONTEND_PORT__ LAN host port the frontend publishes (e.g. 3001) — must be # unique across all stacks on TrueNAS (see runbook §2 registry) # __BACKEND_PORT__ LAN host port for backend debug (e.g. 8082) — unique too, # or remove the backend ports block entirely if not needed # # Auto-deploys on push to main via the INSTANCE-LEVEL self-hosted Gitea Actions # runner on TrueNAS (no per-repo runner registration needed). The runner mounts # the host Docker socket, so `docker compose` acts on the TrueNAS daemon and # (re)builds + restarts the live __PROJECT__ stack from the exact pushed commit. # # db is internal-only (no host publish) — reachable as db:5432 on the compose net. # ───────────────────────────────────────────────────────────────────────────── on: push: branches: [main] # Avoid overlapping deploys if pushes land in quick succession. concurrency: group: truenas-deploy-__PROJECT__ cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest # Skip on the template repo itself (placeholders unsubstituted → would fail). # Generated repos have a different name, so this guard passes for them. if: ${{ gitea.repository != 'pplate/homelab-app-template' }} env: COMPOSE: docker compose -f docker-compose.yml -f docker-compose.truenas.yml -p __PROJECT__ # Production secrets — set in Gitea repo Settings → Actions → Secrets. # AUTH_SECRET : NextAuth v5 session secret (rotating invalidates sessions) # JWT_SECRET : base64 backend HMAC key (rotating invalidates all tokens) # DB_PASSWORD : Postgres role password (must match the live DB role) AUTH_SECRET: ${{ secrets.AUTH_SECRET }} JWT_SECRET: ${{ secrets.JWT_SECRET }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} steps: - name: Check out pushed commit uses: actions/checkout@v4 - name: Show toolchain run: | set -euo pipefail docker version --format 'docker {{.Server.Version}}' docker compose version # NOTE: Backend tests and frontend lint are a LOCAL-ONLY gate. The # self-hosted act runner uses Docker-in-Docker which doesn't support volume # mounts for nested containers. Run them before pushing. - name: Build images run: | set -euo pipefail $COMPOSE build - name: Ensure DB up & reconcile role password run: | set -euo pipefail # Start just the db first (idempotent — reuses the running container # and the persistent __PROJECT___pgdata volume). $COMPOSE up -d db echo "Waiting for db to accept connections ..." for i in $(seq 1 20); do if docker exec __PROJECT__-db pg_isready -U __PROJECT__ -q; then break; fi echo " attempt $i/20 — waiting 3s"; sleep 3 done # POSTGRES_PASSWORD only applies on FIRST volume init, so an existing # volume still holds the old role password. Force the live role to match # the rotated ${DB_PASSWORD} so the backend can authenticate. Local # socket connections inside the container use trust auth (no password). # Skipped when the secret is unset to avoid blanking the dev password. if [ -n "${DB_PASSWORD:-}" ]; then docker exec __PROJECT__-db psql -U __PROJECT__ -d __PROJECT__ \ -c "ALTER USER __PROJECT__ WITH PASSWORD '${DB_PASSWORD}';" echo "✅ DB role password reconciled" else echo "⚠️ DB_PASSWORD secret not set — leaving role password unchanged" fi - name: Roll out stack run: | set -euo pipefail $COMPOSE up -d --remove-orphans - name: Wait for backend health run: | set -euo pipefail echo "Waiting for backend health on :__BACKEND_PORT__ ..." for i in $(seq 1 20); do if wget -q -O /dev/null http://192.168.188.119:__BACKEND_PORT__/actuator/health; then echo "✅ Backend healthy after ${i} attempt(s)" exit 0 fi echo " attempt $i/20 — waiting 6s" sleep 6 done echo "❌ Backend did not become healthy — recent logs:" $COMPOSE logs --tail=40 backend exit 1 - name: Verify frontend run: | set -euo pipefail # Probe the frontend on its own loopback INSIDE the container via the # bundled node runtime. Network-namespace-independent (no reliance on the # host port being wired during a mid-recreate window, which caused a # transient false-failure previously) and needs no wget/curl in the image. # Any HTTP status < 500 counts as "up" — root returns 307 -> /login when # unauthenticated, which is healthy. echo "Waiting for frontend on container loopback :3000 ..." for i in $(seq 1 20); do if docker exec __PROJECT__-frontend node -e "require('http').get('http://127.0.0.1:3000/',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))"; then echo "✅ Frontend responding after ${i} attempt(s)" exit 0 fi echo " attempt $i/20 — waiting 5s" sleep 5 done echo "❌ Frontend did not respond — recent logs:" $COMPOSE logs --tail=40 frontend exit 1 - name: Prune dangling images run: docker image prune -f || true - name: Deployment summary run: | echo "=== __PROJECT__ deployed to TrueNAS ===" echo "Commit: ${GITHUB_SHA}" echo "Backend: http://192.168.188.119:__BACKEND_PORT__" echo "Frontend: http://192.168.188.119:__FRONTEND_PORT__"