Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>` (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

Expand Down Expand Up @@ -244,15 +244,15 @@ 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

### 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
Expand Down Expand Up @@ -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) |
Expand Down Expand Up @@ -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 `<link>` elements managed via `data-precedence`. A MutationObserver script in `src/app/layout.tsx` `<head>` 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

Expand Down
26 changes: 6 additions & 20 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `drumsergio/lynxprompt:X.Y.Z@sha256:<digest>` (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.

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

Expand Down Expand Up @@ -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
Expand All @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading