From 8668f909297f12654b152dbf2a0e4689449a7d3f Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:02:32 +0200 Subject: [PATCH 1/2] docs: deploy through GitOps, not the retired Portainer API Production deploys when the image pin in the GitOps repo moves (Renovate automerges it a day after each release) and the webhook redeploys the stack. CLAUDE.md and RELEASING.md now say that, including the rollback, and the unused PORTAINER_* secrets leave the secrets table. --- CLAUDE.md | 20 ++++++++++---------- RELEASING.md | 26 ++++++-------------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cf942af7..0242b5b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -176,19 +176,19 @@ If CI/CD fails, investigate and fix before considering deployment complete. ### NEVER Restart Docker Containers -**General rule**: Prefer `reload` commands over container restarts. Use Portainer GitOps to redeploy, not manual docker commands. +**General rule**: Prefer `reload` commands over container restarts. Redeploy through GitOps, not manual docker commands. **Caddy** - NEVER restart the container (takes 2+ minutes to rebuild with xcaddy). Instead: ```bash ssh root@watchtower.mango-alpha.ts.net "docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile && docker exec caddy caddy reload --config /etc/caddy/Caddyfile" ``` -**LynxPrompt** - Use Portainer GitOps to redeploy: -1. Update docker-compose.yml in private gitea repo -2. Push changes -3. Trigger Portainer redeploy via API (or wait for auto-sync) +**LynxPrompt** - Production deploys through GitOps: +1. Production runs the image pinned by tag and digest in `lynxprompt/docker-compose.yml` of the private `giteaer/watchtower` Gitea repo. +2. After each release, Renovate opens and automerges the pin bump once the image is a day old. To ship sooner, set the pin yourself to `drumsergio/lynxprompt:X.Y.Z@sha256:` (digest from `docker buildx imagetools inspect drumsergio/lynxprompt:X.Y.Z --format '{{.Manifest.Digest}}'`), commit and push. Changing only the tag deploys nothing new, because Docker resolves the image by digest. +3. The push makes the deploy webhook on watchtower run the stack. Wait for its `=== Deploy Summary ===` line in `docker logs webhook`, then check `https://lynxprompt.com/api/health` reports the new version. -Never manually run `docker compose up` or `docker restart` - Portainer loses track of stack state. +Never manually run `docker compose up` or `docker restart` on the stack. A hand-run compose races the webhook, and the webhook's rollback then leaves the checkout and the containers out of step. ### Branching: `develop` for features, `main` for deps/security @@ -244,7 +244,7 @@ git merge develop - Self-hosted solutions (Umami analytics) - Privacy-focused approaches (cookieless analytics, minimal data collection) - Semver versioning for Docker images (e.g., `2.0.22`, never `:latest`) -- GitOps with Portainer for infrastructure management +- GitOps for infrastructure (a Gitea repo per server, redeployed by a webhook on push) - Docker Hub for all images (custom images built by GHA, pushed to `drumsergio/*`) - Tailwind CSS for styling - TypeScript with strict types @@ -252,7 +252,7 @@ git merge develop ### Things I Dislike ❌ - **Restarting containers** when reload is possible (use `caddy reload`, not container restart) -- **Manual docker commands** for deployments (use Portainer GitOps) +- **Manual docker commands** for deployments (push to the GitOps repo instead) - Over-engineering or unnecessary abstractions - Adding features I didn't ask for - Verbose explanations when action is needed @@ -297,7 +297,7 @@ git merge develop | Component | Details | |-----------|---------| | Docker | Multi-stage builds, images on Docker Hub (`drumsergio/lynxprompt`) | -| Portainer | Container management with GitOps | +| Gitea + deploy webhook | GitOps: a push to a server's repo redeploys the stacks it changed | | Tailscale | VPN for internal services (always use MagicDNS hostnames) | | Umami | Self-hosted analytics (EU, cookieless) | | Caddy | Reverse proxy (production + dev) | @@ -592,7 +592,7 @@ npm run test:coverage # With coverage 5. **React 19 hydration CSS flash**: React 19's hydration recovery (error #418) unmounts and remounts the component tree, temporarily removing CSS `` elements managed via `data-precedence`. A MutationObserver script in `src/app/layout.tsx` `` clones CSS links without `data-precedence` to preserve styles during recovery. 6. **shields.io retired `visual-studio-marketplace` badge** — use static `img.shields.io/badge/` badges for VS Code marketplace links instead 7. **Chocolatey `nodejs` vs `nodejs-lts`** — the `nodejs` package (latest, currently v25) hangs in Chocolatey test VMs; always use `nodejs-lts` (stable v22.x) as a dependency in `.nuspec` files -8. **Portainer TLS certs** — Tailscale-issued Let's Encrypt certs expire every 90 days. Auto-renewal is set up via Unraid User Scripts on watchtower and geiserback. GHA deploy workflows use Tailscale MagicDNS hostnames (not IPs) for proper TLS validation +8. **Tailscale TLS certs** — Tailscale-issued Let's Encrypt certs expire every 90 days. Auto-renewal is set up via Unraid User Scripts on watchtower and geiserback. GHA deploy workflows use Tailscale MagicDNS hostnames (not IPs) for proper TLS validation ## Satellite Repos — Known Workarounds diff --git a/RELEASING.md b/RELEASING.md index cdc08632..04ea84ff 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -62,19 +62,12 @@ docker buildx build --platform linux/amd64 \ ### 4. Deploy -#### Option A: GitHub Actions (Recommended) +Production runs the image pinned by tag and digest in the GitOps repo's `docker-compose.yml`, and a webhook redeploys the stack on every push to that repo. -Use the "Deploy to Production" workflow: -1. Go to Actions → Deploy to Production -2. Click "Run workflow" -3. Enter the version number -4. Confirm deployment +- **Automatic.** After a release, Renovate opens and automerges the pin bump once the image is a day old. +- **Sooner.** Set the pin yourself to `lynxprompt:X.Y.Z@sha256:` (digest from `docker buildx imagetools inspect :X.Y.Z --format '{{.Manifest.Digest}}'`), commit and push. Changing only the tag deploys nothing new, because Docker resolves the image by digest. -#### Option B: Manual Deployment - -1. Update `docker-compose.yml` in your GitOps repo -2. Commit and push -3. Trigger Portainer redeploy via API +The "Verify Production Deployment" workflow then polls `/api/health` for the new version. It gives up after 20 minutes, so it fails whenever the deploy waits on Renovate's one-day delay. That means not deployed yet, not broken. ### 5. Post-Release @@ -117,11 +110,8 @@ If a release has issues: # Change: image: registry/lynxprompt:X.Y.Z # To: image: registry/lynxprompt:PREVIOUS_VERSION -# Trigger redeploy -curl -X PUT "$PORTAINER_URL/api/stacks/$STACK_ID/git/redeploy?endpointId=$ENDPOINT_ID" \ - -H "X-API-Key: $API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"PullImage": true}' +# Use the previous version's tag and digest, commit and push to the GitOps repo. +# The webhook redeploys the stack; no API call is needed. ``` ### Git Rollback @@ -143,10 +133,6 @@ npm version patch | `DOCKER_REGISTRY` | Docker registry URL | | `DOCKER_USERNAME` | Registry username | | `DOCKER_PASSWORD` | Registry password | -| `PORTAINER_URL` | Portainer API URL | -| `PORTAINER_API_KEY` | Portainer API key | -| `PORTAINER_STACK_ID` | Stack ID to deploy | -| `PORTAINER_ENDPOINT_ID` | Endpoint ID | | `GITEA_USERNAME` | GitOps repo username | | `GITEA_TOKEN` | GitOps repo access token | | `CLOUDFLARE_ZONE_ID` | Cloudflare zone ID | From d8ca736c9d47caf4f0048e51143eb13dda8bf920 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:15:01 +0200 Subject: [PATCH 2/2] docs: name the drumsergio/lynxprompt image in the manual pin, drop Portainer from the roadmap Without the namespace, lynxprompt:X.Y.Z resolves to Docker's library image. --- RELEASING.md | 2 +- docs/ROADMAP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 04ea84ff..60fb6fc5 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -65,7 +65,7 @@ docker buildx build --platform linux/amd64 \ Production runs the image pinned by tag and digest in the GitOps repo's `docker-compose.yml`, and a webhook redeploys the stack on every push to that repo. - **Automatic.** After a release, Renovate opens and automerges the pin bump once the image is a day old. -- **Sooner.** Set the pin yourself to `lynxprompt:X.Y.Z@sha256:` (digest from `docker buildx imagetools inspect :X.Y.Z --format '{{.Manifest.Digest}}'`), commit and push. Changing only the tag deploys nothing new, because Docker resolves the image by digest. +- **Sooner.** Set the pin yourself to `drumsergio/lynxprompt:X.Y.Z@sha256:` (digest from `docker buildx imagetools inspect drumsergio/lynxprompt:X.Y.Z --format '{{.Manifest.Digest}}'`), commit and push. Changing only the tag deploys nothing new, because Docker resolves the image by digest. The "Verify Production Deployment" workflow then polls `/api/health` for the new version. It gives up after 20 minutes, so it fails whenever the deploy waits on Renovate's one-day delay. That means not deployed yet, not broken. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 42d936f9..502cdef3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -583,7 +583,7 @@ POST /api/generate - Generate config files from wizard data - [x] PostgreSQL (4 databases: app, users, blog, support) — **v2.0: single DB default, multi-DB optional** - [x] ~~ClickHouse (self-hosted EU, analytics)~~ → **Removed in v2.0** - [x] Umami (self-hosted EU, cookieless analytics) — **v2.0: configurable via env var** -- [x] Docker deployment with GitOps (Portainer) +- [x] Docker deployment with GitOps - [x] Cloudflare DDoS protection and WAF - [x] TLS 1.3 encryption in transit - [x] Network isolation (databases not exposed to internet)