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 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 18:28:17 +01:00
parent 972137acfb
commit 03f07ebee5
5 changed files with 110 additions and 25 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
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

81
deploy.sh Executable file
View File

@@ -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!"

View File

@@ -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: