This service deploys cadcc/ramitos-web releases into a web root. When it gets a GitHub release webhook, it:
- Downloads the release's
dist.zipand checks it against the SHA-256 digest GitHub publishes for the asset. If the checksum doesn't match, nothing in the web root changes. - Installs the files in this order: everything under
assets/, thenindex.html, then the rest. Each file is written beside its destination and renamed into place, so the web server never serves a partially written file. - Deletes every file in the web root that belongs to neither this deploy nor the previous one, then removes directories left empty. The previous deploy's files stay so that browsers still running the old
index.htmlcan load its chunks. The first deploy skips this step.
Tags are treated as opaque strings and can be in any format. The service doesn't use GitHub's "latest" flag. It deploys the release that triggered the webhook, or the tag you name on the command line.
controller serve # receive webhooks (the container's default)
controller deploy [--force] [--allow-prerelease] [--wait 5m] <tag>
controller healthcheck [--url URL] # exit 0 if /healthz answers 200
deploy installs a specific release. Use it for the first deploy, or to roll back to an older tag. It skips a release whose digest is already the current deploy, unless you pass --force. It refuses pre-releases unless you pass --allow-prerelease, which is useful for pushing a release candidate to a QA environment. Deploys take a lock on STATE_DIR/deploy.lock, so a deploy started with docker exec and a webhook deploy never overlap. --wait sets how long the command waits for a deploy that is already running.
docker compose exec controller /controller deploy <tag>healthcheck is the image's HEALTHCHECK, because the distroless image has no curl. GET /healthz returns {"status":"ok","current_tag":…,"deployed_at":…}.
| Variable | Default | Purpose |
|---|---|---|
WEBHOOK_SECRET / WEBHOOK_SECRET_FILE |
required for serve |
GitHub webhook secret |
GITHUB_REPO |
cadcc/ramitos-web |
the only repository accepted |
GITHUB_TOKEN |
empty | needed for a private repository; also raises the API rate limit |
GITHUB_API_URL |
https://api.github.com |
API base URL |
ASSET_NAME |
dist.zip |
release asset to deploy |
TARGET_DIR |
/srv/www |
web root; it must already exist |
STATE_DIR |
/var/lib/controller |
deploy history (deploys.json), lock file, staging area |
LISTEN_ADDR |
:8080 |
HTTP listen address |
ALLOW_PRERELEASE |
false |
also deploy pre-releases, for a QA environment (see below) |
MAX_ZIP_BYTES |
209715200 (200 MiB) |
maximum size of the download, and of its extracted contents |
-
Create the web root and the state directory on the host. Both must be owned by the UID/GID the container runs as. The reverse proxy serves the web root. It shouldn't serve the state directory.
-
Copy
.env.exampleto.envand setWEBHOOK_SECRET. You can generate one withopenssl rand -hex 32. -
Run
DEPLOY_UID=$(id -u www) DEPLOY_GID=$(id -g www) WEB_ROOT=/srv/ramitos-web docker compose up -d --build. -
Point a route on the reverse proxy at the controller. For example, with nginx:
location = /_deploy/webhook { proxy_pass http://127.0.0.1:8080/webhook; }
-
In
cadcc/ramitos-web, go to Settings → Webhooks → Add webhook:- Payload URL:
https://<host>/_deploy/webhook - Content type:
application/json - Secret: the value of
WEBHOOK_SECRET - Events: choose Let me select individual events and select Releases only.
- Payload URL:
-
Deploy the current release once by hand with
docker compose exec controller /controller deploy <tag>. After that, every release deploys automatically when it's published, or when a pre-release is promoted to a release. Pre-releases are not deployed by the webhook unlessALLOW_PRERELEASE=true.
QA environments: set ALLOW_PRERELEASE=true to deploy every release and pre-release as soon as it's published. In this mode the webhook triggers on published instead of released, and deploy accepts pre-releases without --allow-prerelease. Promoting a pre-release doesn't trigger a deploy, because its contents were already deployed when it was published.
The webhook answers with 202 as soon as a release is queued, and the deploy runs in the background. Watch progress with docker compose logs -f controller. Deliveries that aren't relevant get a 200 with ignored: <reason>; these include other events, actions other than released (published also fires for pre-releases), drafts, pre-releases, other repositories, and releases without ASSET_NAME. If a release arrives while a deploy is running, it waits. A newer release replaces one that is still waiting.
The controller takes only the release ID from a webhook. It fetches the asset URL and digest from the GitHub API, and it only downloads from GITHUB_API_URL/repos/GITHUB_REPO/releases/assets/…. So someone who has the webhook secret can trigger a deploy of an existing release, but can't choose what gets downloaded or where GITHUB_TOKEN is sent.
With Podman, build with podman build --format docker so the HEALTHCHECK is kept.
cmd/controller/: the binary. It holds the CLI (main.go), the environment configuration (config.go), and the webhook handler with its deploy worker (server.go).internal/deploy/: the deploy pipeline. It covers the GitHub release client, the download and digest check, zip validation, the ordered install, cleanup, the deploy history and the lock.
go vet ./... && go test -race ./...