From 03f07ebee5f83a6c7cd2d796f809934ce9d80150 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Mon, 9 Feb 2026 18:28:17 +0100 Subject: [PATCH] Add deploy script and update prod compose Deploy script builds and pushes images to Gitea registry, then triggers Portainer stack redeployment via API. Includes preflight checks for branch and uncommitted changes. Also renames prod DB volume to avoid conflicts with dev and changes frontend port to 9080. Co-Authored-By: Claude Opus 4.6 --- ...locke-tracker-ahza--deployment-strategy.md | 10 +-- ...ocke-tracker-aiw6--create-deploy-script.md | 4 +- ...ortainer-webhook-for-automated-redeploy.md | 24 ++++-- deploy.sh | 81 +++++++++++++++++++ docker-compose.prod.yml | 16 ++-- 5 files changed, 110 insertions(+), 25 deletions(-) create mode 100755 deploy.sh diff --git a/.beans/nuzlocke-tracker-ahza--deployment-strategy.md b/.beans/nuzlocke-tracker-ahza--deployment-strategy.md index 7220cd4..cf7f321 100644 --- a/.beans/nuzlocke-tracker-ahza--deployment-strategy.md +++ b/.beans/nuzlocke-tracker-ahza--deployment-strategy.md @@ -22,12 +22,12 @@ Define and implement a deployment strategy for running the nuzlocke-tracker in p **Docker Compose + Portainer + Gitea (source hosting, container registry, CI/CD)** -1. **Gitea** runs on Unraid behind Nginx Proxy Manager with SSL (e.g., `gitea.yourdomain.com`). It serves as the self-hosted Git remote, container registry, and (optionally) CI/CD via Gitea Actions. -2. **Images are built on the dev machine** and pushed to Gitea's container registry as **user-level packages** (e.g., `gitea.yourdomain.com/julian/nuzlocke-tracker-api:latest`, `gitea.yourdomain.com/julian/nuzlocke-tracker-frontend:latest`). +1. **Gitea** runs on Unraid behind Nginx Proxy Manager with SSL (e.g., `gitea.nerdboden.de`). It serves as the self-hosted Git remote, container registry, and (optionally) CI/CD via Gitea Actions. +2. **Images are built on the dev machine** and pushed to Gitea's container registry as **user-level packages** (e.g., `gitea.nerdboden.de/thefurya/nuzlocke-tracker-api:latest`, `gitea.nerdboden.de/thefurya/nuzlocke-tracker-frontend:latest`). 3. **Production runs docker-compose** on Unraid, pulling images from the Gitea container registry instead of mounting source. 4. **Portainer** is installed on Unraid to manage stacks, provide a web UI, and enable webhook-triggered redeployments. 5. **A deploy script** on the dev machine automates the full flow: build images → push to Gitea registry → trigger Portainer webhook to redeploy. -6. **Nginx Proxy Manager** handles routing on the LAN (e.g., `nuzlocke.yourdomain.com` → frontend container, `gitea.yourdomain.com` → Gitea). +6. **Nginx Proxy Manager** handles routing on the LAN (e.g., `nuzlocke.nerdboden.de` → frontend container, `gitea.nerdboden.de` → Gitea). 7. **Database** uses a named Docker volume for persistence; migrations run automatically on API container startup. ## Branching Strategy @@ -48,9 +48,9 @@ Define and implement a deployment strategy for running the nuzlocke-tracker in p - [ ] **Set up branching structure** — create `develop` branch from `main`, establish the `main`/`develop`/`feature/*` workflow - [ ] **Update CLAUDE.md with branching rules** — once the branching structure is in place, add instructions to CLAUDE.md that the branching strategy must be adhered to (always work on feature branches, never commit directly to `main`, merge flow is `feature/*` → `develop` → `main`) -- [ ] **Configure Gitea container registry** — create an access token with `read:package` and `write:package` scopes, verify `docker login gitea.yourdomain.com` works, test pushing and pulling an image as a user-level package +- [ ] **Configure Gitea container registry** — create an access token with `read:package` and `write:package` scopes, verify `docker login gitea.nerdboden.de` works, test pushing and pulling an image as a user-level package - [x] **Create production docker-compose file** (`docker-compose.prod.yml`) — uses images from the Gitea container registry, production env vars, no source volume mounts, proper restart policies -- [ ] **Create production Dockerfiles (or multi-stage builds)** — ensure frontend is built and served statically (e.g., via the API or a lightweight nginx container), API runs without debug mode +- [x] **Create production Dockerfiles (or multi-stage builds)** — ensure frontend is built and served statically (e.g., via the API or a lightweight nginx container), API runs without debug mode - [x] **Set up Portainer on Unraid** — install Portainer CE as a Docker container, configure the stack from the production compose file - [ ] **Configure Portainer webhook for automated redeployment** — add a webhook trigger in Portainer that pulls latest images and restarts the stack - [ ] **Create deploy script** — a script (e.g., `./deploy.sh`) that builds images from `main`, tags them for the Gitea registry, pushes them, and triggers the Portainer webhook to redeploy diff --git a/.beans/nuzlocke-tracker-aiw6--create-deploy-script.md b/.beans/nuzlocke-tracker-aiw6--create-deploy-script.md index 93b9df6..2f7391e 100644 --- a/.beans/nuzlocke-tracker-aiw6--create-deploy-script.md +++ b/.beans/nuzlocke-tracker-aiw6--create-deploy-script.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-aiw6 title: Create deploy script -status: todo +status: in-progress type: task priority: normal created_at: 2026-02-09T15:30:48Z -updated_at: 2026-02-09T15:31:15Z +updated_at: 2026-02-09T17:22:53Z parent: nuzlocke-tracker-ahza blocking: - nuzlocke-tracker-izf6 diff --git a/.beans/nuzlocke-tracker-jzqz--configure-portainer-webhook-for-automated-redeploy.md b/.beans/nuzlocke-tracker-jzqz--configure-portainer-webhook-for-automated-redeploy.md index b4cc7bb..ea5c74a 100644 --- a/.beans/nuzlocke-tracker-jzqz--configure-portainer-webhook-for-automated-redeploy.md +++ b/.beans/nuzlocke-tracker-jzqz--configure-portainer-webhook-for-automated-redeploy.md @@ -1,18 +1,28 @@ --- # nuzlocke-tracker-jzqz -title: Configure Portainer webhook for automated redeployment -status: todo +title: Configure Portainer API for automated redeployment +status: in-progress type: task priority: normal created_at: 2026-02-09T15:30:45Z -updated_at: 2026-02-09T15:31:15Z +updated_at: 2026-02-09T17:22:17Z parent: nuzlocke-tracker-ahza blocking: - nuzlocke-tracker-hwyk --- -Set up a webhook in Portainer that triggers a stack redeployment when called. +Use the Portainer CE REST API to trigger stack redeployments from the deploy script. -- Create a webhook trigger in Portainer for the nuzlocke-tracker stack -- The webhook should pull the latest images from the local registry and restart the stack -- Note the webhook URL for use in the deploy script \ No newline at end of file +Portainer webhooks are a Business-only feature, so we use the API directly instead. + +## Approach + +1. Authenticate with the Portainer API to get a JWT token +2. Call the stack update endpoint with `pullImage: true` to pull latest images and recreate containers + +## Checklist + +- [ ] Identify the stack ID in Portainer (via API or UI) +- [ ] Test API authentication (`POST /api/auth`) +- [ ] Test triggering a stack redeploy via API +- [ ] Integrate into the deploy script \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6944a9a --- /dev/null +++ b/deploy.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Configuration ────────────────────────────────────────────── +REGISTRY="gitea.nerdboden.de" +OWNER="thefurya" +IMAGES=("nuzlocke-tracker-api" "nuzlocke-tracker-frontend") +DOCKERFILES=("backend/Dockerfile.prod" "frontend/Dockerfile.prod") +CONTEXTS=("./backend" "./frontend") + +PORTAINER_URL="${PORTAINER_URL:-https://portainer.nerdboden.de}" +PORTAINER_API_KEY="${PORTAINER_API_KEY:-}" +PORTAINER_STACK_ID="${PORTAINER_STACK_ID:-}" +PORTAINER_ENDPOINT_ID="${PORTAINER_ENDPOINT_ID:-1}" + +# ── Helpers ──────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[✓]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +error() { echo -e "${RED}[✗]${NC} $1"; exit 1; } + +# ── Preflight checks ────────────────────────────────────────── +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [[ "$BRANCH" != "main" ]]; then + warn "You are on branch '$BRANCH', not 'main'." + read -rp "Continue anyway? [y/N] " confirm + [[ "$confirm" =~ ^[Yy]$ ]] || exit 0 +fi + +if ! git diff --quiet || ! git diff --cached --quiet; then + warn "You have uncommitted changes." + read -rp "Continue anyway? [y/N] " confirm + [[ "$confirm" =~ ^[Yy]$ ]] || exit 0 +fi + +# ── Build and push images ───────────────────────────────────── +for i in "${!IMAGES[@]}"; do + IMAGE="${REGISTRY}/${OWNER}/${IMAGES[$i]}:latest" + info "Building ${IMAGES[$i]}..." + docker build -t "$IMAGE" -f "${DOCKERFILES[$i]}" "${CONTEXTS[$i]}" + info "Pushing ${IMAGES[$i]}..." + docker push "$IMAGE" +done + +info "All images built and pushed." + +# ── Trigger Portainer redeployment ───────────────────────────── +if [[ -z "$PORTAINER_API_KEY" ]]; then + warn "PORTAINER_API_KEY not set — skipping Portainer redeployment." + warn "Set it in your environment or .env.deploy file to enable auto-redeploy." + exit 0 +fi + +if [[ -z "$PORTAINER_STACK_ID" ]]; then + warn "PORTAINER_STACK_ID not set — skipping Portainer redeployment." + warn "Find your stack ID in Portainer and set it in your environment." + exit 0 +fi + +info "Fetching stack file from Portainer..." +STACK_FILE=$(curl -sf \ + -H "X-API-Key: ${PORTAINER_API_KEY}" \ + "${PORTAINER_URL}/api/stacks/${PORTAINER_STACK_ID}/file") \ + || error "Failed to fetch stack file from Portainer." + +STACK_CONTENT=$(echo "$STACK_FILE" | jq -r '.StackFileContent') + +info "Triggering stack redeployment..." +curl -sf -X PUT \ + -H "X-API-Key: ${PORTAINER_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg content "$STACK_CONTENT" '{"pullImage": true, "stackFileContent": $content}')" \ + "${PORTAINER_URL}/api/stacks/${PORTAINER_STACK_ID}?endpointId=${PORTAINER_ENDPOINT_ID}" \ + > /dev/null \ + || error "Failed to trigger Portainer redeployment." + +info "Stack redeployment triggered successfully!" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index dd1d0df..75845c2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,9 +1,6 @@ services: api: - image: gitea.nerdboden.de/julian/nuzlocke-tracker-api:latest - build: - context: ./backend - dockerfile: Dockerfile.prod + image: gitea.nerdboden.de/thefurya/nuzlocke-tracker-api:latest command: > sh -c "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --app-dir src" environment: @@ -15,12 +12,9 @@ services: restart: unless-stopped frontend: - image: gitea.nerdboden.de/julian/nuzlocke-tracker-frontend:latest - build: - context: ./frontend - dockerfile: Dockerfile.prod + image: gitea.nerdboden.de/thefurya/nuzlocke-tracker-frontend:latest ports: - - "8080:80" + - "9080:80" depends_on: - api restart: unless-stopped @@ -32,7 +26,7 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=nuzlocke volumes: - - postgres_data:/var/lib/postgresql/data + - prod_postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s @@ -41,4 +35,4 @@ services: restart: unless-stopped volumes: - postgres_data: + prod_postgres_data: