- 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>
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
# 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:
{
"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:
{
"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:
- config — Read coolify.json from the project
- server — Resolve server name to UUID/IP via Coolify API
- check — Find existing app by name
- validate — Compare coolify.json vs live state, report diffs
- sync — Create or update the Coolify application
- env — Set HOST_PORT environment variable
- route — Add Traefik route (remote) or set FQDN (local)
- changelog — Write coolify.changelog.json to repo
- deploy — Trigger Coolify deployment
- 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:
- Connect to server via SSH
- Create remote directory (mkdir -p)
- Upload tar + docker-compose.yml + .env + additional files via SFTP
- Run
docker load -i <name>.tar - Run
docker compose down && docker compose up -d - 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:
# 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
- Each Coolify app has a
manual_webhook_secret_gitea— a shared HMAC-SHA256 secret - Each Gitea repo has a webhook pointing to
http://192.168.69.4:2010/webhooks/source/gitea/events/manual - On push, Gitea signs the payload with the secret and POSTs to Coolify
- 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:
# 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(notmain). Coolify'sgit_branchmust match. The upsert pipeline defaults tomaster.
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:
# docker-compose.yml
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
# 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:
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:
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.