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: