deploy-agent
Optional. This is a host-level self-update mechanism for a specific kind
of production deployment (own VPS, git-based deploy, no external
orchestrator). It is not required to run this project — docker compose up
alone is a complete base install, and the Settings → Обновления tab in the
CRM (internal/selfupdate) simply won't do anything if deploy-agent isn't
installed (DEPLOY_AGENT_TOKEN unset → those API routes 503, fail-closed).
Skip this directory entirely unless you specifically want the in-app
"check/apply update" button.
Standalone host process (not a Docker service, not part of backend's Go
module) that runs git fetch/git pull/docker compose build/docker compose up for /root/production on request, over a Unix domain socket.
Exists so the CRM's "Обновления" tab can offer a check/apply-update button
without giving the backend container a docker.sock mount or a copy of
this repo's .git — either would turn a compromised backend process into
full host control. Only the one socket file this agent listens on gets
bind-mounted into backend (see ../docker-compose.yml's backend.volumes
and DEPLOY_AGENT_SOCKET_PATH in .env); no TCP port, nothing else about
the host reachable through it.
What it does
GET /check—git fetch+ list commits betweenHEADandorigin/<branch>. Read-only.POST /apply— fetch, fast-forward-only pull,docker compose build,docker compose up -dforbackend+webonly (neverpostgres/minio/telegram-bot-api— narrower blast radius, faster). Serialized: a second/applywhile one is running gets a 409, not a second concurrent deploy stepping on the same working tree.
Both require Authorization: Bearer <DEPLOY_AGENT_TOKEN>.
Install
cd /root/production/deploy-agent
go build -o deploy-agent .
cp deploy-agent.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now deploy-agent
journalctl -u deploy-agent -f # confirm it's listening, agent.sock now exists
agent.env (this directory) already holds a generated DEPLOY_AGENT_TOKEN
— the same value must be set as DEPLOY_AGENT_TOKEN in ../.env so
backend can authenticate to the socket. Neither agent.env nor the
compiled deploy-agent binary nor agent.sock are committed (see
../.gitignore).
Order matters: ../docker-compose.yml bind-mounts the specific file
deploy-agent/agent.sock into backend (not the whole directory — see that
file's own comment on why), and Docker can only bind-mount a path that
already exists at container start. So:
- Install/start
deploy-agentfirst (above) — this createsagent.sock. - Only then
docker compose up -d(or restart)backend.
If backend was already running before agent.sock existed, its container
was created without that mount — docker compose up -d backend again
(after the agent is up) to recreate it with the socket attached; a plain
restart of an existing container won't pick up a new bind mount.
Security model, and what it does NOT protect against
- Runs as root, same as everything else on this host today (see
deploy-agent.service's own comment). The actual boundary is: only a process that can (a) reachagent.sockon the filesystem AND (b) present the bearer token can trigger a deploy. On this host that's exactly thebackendcontainer (via the bind mount) — nothing else. backenditself gates both endpoints behindauth.RequireRole("owner")(seeinternal/selfupdate) — a manager/master token can't reach this even indirectly, regardless of what's in the granular permissions system core issues (deliberately not reusing that system here — this is more sensitive than any existing permission, a hardcoded role check is a smaller, easier-to-audit surface than "whichever role happens to have this checkbox ticked").- Does not protect against: a compromised
backendcontainer's own process triggering deploys (it holds the token by design, so it can). If that container's trust boundary is ever a concern, the next hardening step is movingagent.env's token to something backend fetches per-request from a secrets manager instead of a static env var — not done here, out of scope for the MVP this ships as. - Does not protect against a bad commit on
origin/main—/applypulls and deploys whatever's there, same as a human runninggit pull && docker compose up -d --buildwould. This button doesn't run tests or gate on CI status; it's exactly the manual deploy step, just one click instead of an SSH session. /applyonly rebuilds and restartsbackend+web(seeDEPLOY_AGENT_COMPOSE_SERVICES) — it never rebuilds this agent itself. A commit that changesdeploy-agent/main.gostill gets pulled onto disk, but the already-running agent process keeps executing its old compiled behavior until someone manually doesgo build -o deploy-agent . && systemctl restart deploy-agent. Self-restarting mid-request is more complexity than this MVP is worth; if deploy-agent's own logic changes, that's the one case still needing a manual step.
Follow-up hardening ideas (not implemented)
- Dedicated non-root
deployuser with a narrow sudoers entry for exactlygit -C /root/production ...anddocker compose ...in this directory, instead of running the whole agent as root. - Rotate
DEPLOY_AGENT_TOKENperiodically (systemd timer + restart both the agent andbackend).