Files
idea.llm.gitea.repo.docker.…/README.md
Clint Masden dc8f392ff5 Add webhook auto-deploy: Gitea push → Coolify build
- 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>
2026-02-27 13:27:14 -06:00

13 KiB

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

  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:

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

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

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