- Add webhook endpoints (setup/teardown/status) for batch management - Add step 10 (webhook) to upsert pipeline for automatic setup - Add DELETE /routes endpoint for Traefik route removal - Add giteaFetch helper for Gitea API calls - Document webhook flow, CIFS hooks fix, IPv6 healthcheck gotcha - Update port allocation table (all 11 apps deployed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
347 lines
13 KiB
Markdown
347 lines
13 KiB
Markdown
# Docker Deployment Manager
|
|
|
|
Unified REST API + Electron desktop app for managing Docker deployments across two servers via Gitea + Coolify. The API is the single source of truth — the UI, CLI, and LLM agents all use the same HTTP endpoints.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
idea.llm.gitea.repo.docker.deployment/
|
|
├── api/ # Express API server (:3100)
|
|
│ ├── server.js # Entry point, Scalar docs at /reference
|
|
│ ├── routes/
|
|
│ │ ├── coolify.js # /api/coolify/* — Coolify app management
|
|
│ │ ├── projects.js # /api/projects/* — local scanning & comparison
|
|
│ │ ├── servers.js # /api/servers/* — server CRUD, containers, logs
|
|
│ │ └── docker.js # /api/docker/* — build, deploy, pull files
|
|
│ ├── lib/
|
|
│ │ ├── config.js # Loads Coolify + deployment configs
|
|
│ │ ├── coolify-client.js # coolifyFetch() — Coolify API wrapper
|
|
│ │ └── ssh.js # SSHService class (exec + SFTP)
|
|
│ └── openapi.json # OpenAPI 3.1 spec (24 endpoints)
|
|
├── app/
|
|
│ ├── main/ # Electron main process (legacy IPC)
|
|
│ ├── renderer/ # React UI (Vite + TanStack Query)
|
|
│ │ └── src/
|
|
│ │ ├── lib/api.js # All API calls via HTTP fetch
|
|
│ │ ├── hooks/ # React Query hooks
|
|
│ │ └── components/
|
|
│ │ ├── coolify/ # Coolify dashboard + deploy dialog
|
|
│ │ ├── projects/ # Project management
|
|
│ │ ├── servers/ # Server settings
|
|
│ │ └── dashboard/ # Overview
|
|
│ └── .env # SSH credentials (not committed)
|
|
├── cli/ # CLI tool for project scaffolding
|
|
├── deployment-config.json # Server list + project root config
|
|
└── package.json
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Install dependencies
|
|
npm install
|
|
cd app/renderer && npm install && cd ../..
|
|
|
|
# Run everything (API + Vite + Electron)
|
|
npm run dev
|
|
|
|
# Or without Electron (browser only at http://localhost:5173)
|
|
npm run dev:no-electron
|
|
|
|
# API only
|
|
npm run api:dev
|
|
```
|
|
|
|
- **API server**: http://localhost:3100
|
|
- **Scalar API docs**: http://localhost:3100/reference
|
|
- **OpenAPI spec**: http://localhost:3100/openapi.json
|
|
- **Vite dev server**: http://localhost:5173 (proxies `/api/*` to :3100)
|
|
|
|
## API Endpoints (24 total)
|
|
|
|
All endpoints return JSON. The Vite dev server proxies `/api/*` to the API server, so the renderer uses relative paths.
|
|
|
|
### Projects
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/api/projects` | Scan local projects (finds Dockerfiles, coolify.json, etc.) |
|
|
| POST | `/api/projects/compare` | Compare local vs remote files (docker-compose, .env, data/) |
|
|
| POST | `/api/projects/init` | Initialize a project with deployment config via CLI |
|
|
|
|
### Servers
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/api/servers` | List configured deployment servers |
|
|
| POST | `/api/servers` | Add or update a server |
|
|
| DELETE | `/api/servers/:id` | Delete a server |
|
|
| GET | `/api/servers/:id/scan` | Scan remote ~/containers for deployed projects |
|
|
| GET | `/api/servers/:id/containers` | List running Docker containers (docker ps) |
|
|
| GET | `/api/servers/:id/logs` | Get container logs (query: containerName, remotePath, lines) |
|
|
|
|
### Docker Operations
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| POST | `/api/docker/build` | Build tar via build-image-tar.ps1 |
|
|
| POST | `/api/docker/deploy` | Upload files + docker load + docker compose up |
|
|
| POST | `/api/docker/pull` | Pull files/directories from remote server via SFTP |
|
|
| POST | `/api/docker/vscode-diff` | Download remote file and open VS Code diff |
|
|
|
|
### Coolify
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/api/coolify/apps` | List all Coolify apps (enriched with HOST_PORT) |
|
|
| GET | `/api/coolify/apps/find/:name` | Find app by name |
|
|
| POST | `/api/coolify/apps` | Create new Coolify application |
|
|
| PATCH | `/api/coolify/apps/:uuid` | Update existing app |
|
|
| DELETE | `/api/coolify/apps/:uuid` | Delete app |
|
|
| GET | `/api/coolify/apps/:uuid/envs` | List env vars |
|
|
| POST | `/api/coolify/apps/:uuid/envs` | Set env var |
|
|
| POST | `/api/coolify/apps/:uuid/deploy` | Trigger deployment |
|
|
| GET | `/api/coolify/servers` | List Coolify servers |
|
|
| GET | `/api/coolify/next-port` | Get next available HOST_PORT |
|
|
| POST | `/api/coolify/routes` | Add Traefik route via SSH |
|
|
| DELETE | `/api/coolify/routes` | Remove Traefik route via SSH |
|
|
| POST | `/api/coolify/drift` | Check drift between coolify.json and live state |
|
|
| POST | `/api/coolify/upsert` | Full pipeline: config → create/update → env → route → webhook → deploy |
|
|
| GET | `/api/coolify/config?path=...` | Read coolify.json from a project |
|
|
|
|
### Webhooks
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| POST | `/api/coolify/webhooks/setup` | Set Coolify webhook secret + create Gitea webhook on all apps |
|
|
| DELETE | `/api/coolify/webhooks/teardown` | Remove webhook secret + delete Gitea webhooks |
|
|
| GET | `/api/coolify/webhooks/status` | Check webhook configuration status per app |
|
|
|
|
### System
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/api/health` | Health check |
|
|
| GET | `/openapi.json` | OpenAPI 3.1 spec |
|
|
| GET | `/reference` | Scalar interactive API docs |
|
|
|
|
## Configuration
|
|
|
|
### Coolify Config (`gitea.repo.management/config.json`)
|
|
|
|
Shared with the gitea.repo.management project. Contains Coolify API credentials, server UUIDs, SSH config, and webhook/Gitea settings:
|
|
|
|
```json
|
|
{
|
|
"coolify": {
|
|
"apiUrl": "http://192.168.69.4:2010",
|
|
"apiToken": "<bearer-token>",
|
|
"serverUuid": "c88okoc4g4css0c4ooo04ko4",
|
|
"projectUuid": "bcso4cocokswgo804gogoswo",
|
|
"privateKeyUuid": "j0w08woc8s8c0sgok8ccow4w",
|
|
"sshHost": "192.168.69.4",
|
|
"sshUser": "clint",
|
|
"sshPassword": "<password>",
|
|
"traefikPath": "/home/clint/containers/coolify/data/proxy/dynamic/custom.yml",
|
|
"targetIp": "192.168.69.5",
|
|
"domain": "dotrepo.com",
|
|
"webhookSecret": "<shared-hmac-secret>"
|
|
},
|
|
"gitea": {
|
|
"url": "http://192.168.69.4:2003",
|
|
"token": "<gitea-api-token>"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Deployment Config (`deployment-config.json`)
|
|
|
|
Local to this project. Stores server list and project root:
|
|
|
|
```json
|
|
{
|
|
"servers": [
|
|
{ "id": "1771305558920", "name": "Repo Server", "host": "192.168.69.5", "username": "clint", "useSudo": true }
|
|
],
|
|
"projectsRoot": "C:\\.bucket\\repos.gitea"
|
|
}
|
|
```
|
|
|
|
### SSH Credentials (`app/.env`)
|
|
|
|
```
|
|
SSH_USERNAME=clint
|
|
SSH_PASSWORD=<password>
|
|
```
|
|
|
|
## Coolify Upsert Pipeline
|
|
|
|
The `POST /api/coolify/upsert` endpoint runs the full deploy pipeline:
|
|
|
|
1. **config** — Read coolify.json from the project
|
|
2. **server** — Resolve server name to UUID/IP via Coolify API
|
|
3. **check** — Find existing app by name
|
|
4. **validate** — Compare coolify.json vs live state, report diffs
|
|
5. **sync** — Create or update the Coolify application
|
|
6. **env** — Set HOST_PORT environment variable
|
|
7. **route** — Add Traefik route (remote) or set FQDN (local)
|
|
8. **changelog** — Write coolify.changelog.json to repo
|
|
9. **deploy** — Trigger Coolify deployment
|
|
10. **webhook** — Set Coolify webhook secret + create/update Gitea webhook for auto-deploy on push
|
|
|
|
## Docker Deploy Pipeline (Legacy/Direct)
|
|
|
|
The `POST /api/docker/deploy` endpoint handles tar-based deployment:
|
|
|
|
1. Connect to server via SSH
|
|
2. Create remote directory (mkdir -p)
|
|
3. Upload tar + docker-compose.yml + .env + additional files via SFTP
|
|
4. Run `docker load -i <name>.tar`
|
|
5. Run `docker compose down && docker compose up -d`
|
|
6. Poll health check (10 attempts, 2s apart)
|
|
|
|
## Infrastructure
|
|
|
|
| Server | IP | Role |
|
|
|--------|-----|------|
|
|
| box-stableapps | 192.168.69.4 | Coolify host, Gitea, Traefik |
|
|
| box-repoapps | 192.168.69.5 | Deployed apps |
|
|
|
|
### Port Allocation
|
|
|
|
| Port | App | Status |
|
|
|------|-----|--------|
|
|
| 2001 | Audio Events | Deployed |
|
|
| 2002 | Audio Capsule | Deployed |
|
|
| 2003 | Audio Sphere | Deployed |
|
|
| 2004 | Every Noise at Once | Deployed |
|
|
| 2005 | Learn X In Y | Deployed |
|
|
| 2006 | Actual Budget Report | Deployed |
|
|
| 2007 | dotrepo-timer | Deployed |
|
|
| 2008 | dotrepo-travel | Deployed |
|
|
| 2009 | dotrepo-racker | Deployed |
|
|
| 2011 | spike-breakdown | Deployed |
|
|
| 2012 | dotrepo-site | Deployed |
|
|
|
|
## LLM / Agent Usage
|
|
|
|
This API is designed to be called by LLMs and agents. Key endpoints for automation:
|
|
|
|
```bash
|
|
# List all Coolify apps with their ports and status
|
|
curl http://localhost:3100/api/coolify/apps
|
|
|
|
# Deploy a project from its coolify.json
|
|
curl -X POST http://localhost:3100/api/coolify/upsert \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"projectPath": "C:/.bucket/repos.gitea/dotrepo.racker"}'
|
|
|
|
# Scan local projects
|
|
curl http://localhost:3100/api/projects
|
|
|
|
# Deploy via tar+SSH to a server
|
|
curl -X POST http://localhost:3100/api/docker/deploy \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"projectPath": "C:/.bucket/repos.gitea/myapp", "serverId": "1771305558920", "remotePath": "~/containers/myapp"}'
|
|
|
|
# Get container logs
|
|
curl "http://localhost:3100/api/servers/1771305558920/logs?remotePath=~/containers/myapp&lines=50"
|
|
```
|
|
|
|
**Always use this API** to access/change Coolify and Gitea deployment state. Do not call the Coolify API directly — this API wraps it with enrichment (HOST_PORT), safety (drift detection, changelog), and routing (Traefik management).
|
|
|
|
## Webhook Auto-Deploy
|
|
|
|
Every `git push` to Gitea automatically triggers a Coolify deployment. The pipeline:
|
|
|
|
```
|
|
git push → Gitea post-receive hook → Gitea webhook POST → Coolify webhook handler → Docker build + deploy
|
|
```
|
|
|
|
### How It Works
|
|
|
|
1. Each Coolify app has a `manual_webhook_secret_gitea` — a shared HMAC-SHA256 secret
|
|
2. Each Gitea repo has a webhook pointing to `http://192.168.69.4:2010/webhooks/source/gitea/events/manual`
|
|
3. On push, Gitea signs the payload with the secret and POSTs to Coolify
|
|
4. Coolify validates the signature, matches the repo+branch, and queues a deployment
|
|
|
|
### Setup
|
|
|
|
Webhooks are automatically configured during `upsert` (step 10). To set up webhooks on all existing apps:
|
|
|
|
```bash
|
|
# Set up webhooks on all Coolify apps
|
|
curl -X POST http://localhost:3100/api/coolify/webhooks/setup
|
|
|
|
# Check status
|
|
curl http://localhost:3100/api/coolify/webhooks/status
|
|
|
|
# Remove all webhooks
|
|
curl -X DELETE http://localhost:3100/api/coolify/webhooks/teardown
|
|
```
|
|
|
|
### Key Details
|
|
|
|
- **Webhook URL must use internal IP** (`http://192.168.69.4:2010`), NOT the public URL (`https://coolify.clintmasden.duckdns.org`). The public URL fails due to hairpin NAT — Gitea and Coolify are on the same box (192.168.69.4).
|
|
- **Gitea ports**: 2003 = HTTP API, 2004 = SSH. Do not confuse them.
|
|
- **Branch**: All repos use `master` (not `main`). Coolify's `git_branch` must match. The upsert pipeline defaults to `master`.
|
|
|
|
## Gotchas and Troubleshooting
|
|
|
|
### Docker Healthcheck IPv6 Issue
|
|
|
|
Alpine Linux resolves `localhost` to `::1` (IPv6) before `127.0.0.1` (IPv4). If your container only listens on IPv4 (e.g., nginx on `0.0.0.0:3000`), healthchecks using `localhost` will fail silently.
|
|
|
|
**Fix**: Always use `127.0.0.1` instead of `localhost` in healthcheck commands:
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
|
|
```
|
|
|
|
```dockerfile
|
|
# Dockerfile
|
|
HEALTHCHECK CMD wget -qO- http://127.0.0.1:3000/ || exit 1
|
|
```
|
|
|
|
### Gitea CIFS Hooks Not Executable
|
|
|
|
Gitea's git data is stored on a CIFS/SMB mount (`//192.168.69.2/archive`). CIFS doesn't support per-file permissions — all files inherit `file_mode` from the mount options. If `file_mode=0664` (the default), git hooks won't execute because they lack the execute bit. This silently breaks:
|
|
|
|
- Webhook delivery (post-receive hook doesn't fire)
|
|
- Branch tracking (Gitea sees repos as "empty")
|
|
- All git server-side hooks
|
|
|
|
**Fix**: Change `file_mode=0664` to `file_mode=0775` in `/etc/fstab`:
|
|
|
|
```
|
|
//192.168.69.2/archive /home/clint/archive cifs ...,file_mode=0775,... 0 0
|
|
```
|
|
|
|
Then remount: `sudo mount -o remount /home/clint/archive`
|
|
|
|
After remounting, run `resync_all_hooks` via Gitea admin API to regenerate hook files:
|
|
|
|
```bash
|
|
curl -X POST -H "Authorization: token <token>" \
|
|
"http://192.168.69.4:2003/api/v1/admin/cron/resync_all_hooks"
|
|
```
|
|
|
|
### Gitea Default Branch Mismatch
|
|
|
|
All repos use `master` as their default branch. If Gitea's `default_branch` setting is `main` (the Gitea default for new repos), it will report the repo as `empty: true` and won't deliver webhooks for pushes to `master`.
|
|
|
|
**Fix**: Set `default_branch` to `master` via API:
|
|
|
|
```bash
|
|
curl -X PATCH -H "Authorization: token <token>" \
|
|
-H "Content-Type: application/json" \
|
|
"http://192.168.69.4:2003/api/v1/repos/clint/<repo>" \
|
|
-d '{"default_branch":"master"}'
|
|
```
|
|
|
|
### dockerComposeLocation vs baseDirectory
|
|
|
|
In coolify.json, `dockerComposeLocation` is **relative to** `baseDirectory`. If your `baseDirectory` is `/myapp`, set `dockerComposeLocation` to `/docker-compose.yml`, NOT `/myapp/docker-compose.yml`.
|