agent¶
Contributor documentation
This section explains how the Manager, Nodes, and cluster work internally and how to contribute to the opensource cluster itself. If you are a developer who just wants to run your own code on the cluster, see the Users documentation instead.
Node.js (TypeScript) check-in agent that reports host/service status to the manager and applies nginx and dnsmasq configuration. Installed on agent and manager containers. For deployment instructions, see Deploying Agents.
Architecture¶
A systemd timer launches the oneshot agent every 30 seconds. Each run checks in with the manager (POST /api/v1/agents), applies any config changes, and exits.
TypeScript sources live in agent/src (compiled to dist/), the locally rendered EJS templates in agent/templates, and the systemd service/timer units in agent/contrib/systemd. The agent/Makefile builds the opensource-agent package (see Release Pipeline). Service reloads and state queries go through the systemd D-Bus API; config validation uses nginx -t and dnsmasq --test.
Check-in Protocol¶
sequenceDiagram
participant Timer as systemd timer
participant Agent
participant Manager
Timer->>Agent: start (every 30s)
Agent->>Manager: POST /api/v1/agents<br/>{siteId, hostname, ipv4Address, services}<br/>If-None-Match: {etag}
alt Config changed
Manager-->>Agent: 200 OK + config snapshot + ETag
Agent->>Agent: Render templates, validate<br/>(nginx -t, dnsmasq --test)
Agent->>Agent: Apply + reload (rollback on failure)
Agent->>Manager: POST again (reports apply results)
Manager-->>Agent: 304 Not Modified
else No changes
Manager-->>Agent: 304 Not Modified
end
Agent->>Timer: exit
The check-in body carries system info, per-service status, and — when the agent provisioned volume directories this pass — per-volume results:
{
"siteId": 1,
"hostname": "agent",
"currentTime": 1771234560,
"ipv4Address": "172.17.0.2",
"services": {
"nginx": { "state": "active", "lastApply": "success" },
"dnsmasq": { "state": "active", "lastApply": "success" }
},
"volumes": {
"42": { "applied": true },
"43": { "applied": false, "message": "mkdir: permission denied" }
}
}
The manager records every check-in in the Agents table (shown on the web client's /agents page) and responds with the site's config snapshot as JSON. A strong ETag covers the snapshot; the agent stores it in /var/lib/opensource-agent/state.json and sends it back via If-None-Match, so unchanged configs cost a single 304 round trip.
Volumes¶
The config snapshot includes, at the site level, the volume directories the
agent must ensure exist (site.volumes[] = { id, hostPath, mode, uid, gid }).
There is one agent per site, and the shared volumes root is bind-mounted into the
agent container, so this single agent provisions volumes for every node in the
site. Each pass the agent first checks, via /proc/self/mountinfo, that the
path lies on a mount other than its own root filesystem (otherwise the volumes
root isn't bind-mounted and the volume is reported failed rather than created
inside the agent). It then mkdir -ps the directory, chowns it to the
supplied uid/gid (the consuming containers' id-mapped root, 100000)
best-effort, and chmods it per mode. The chown is a tolerated no-op
(EINVAL/EPERM) in an unprivileged agent guest — the norm, since pct create
defaults to --unprivileged 1, where the id-map already yields the right owner
and 100000 is outside the guest's mapped range — and the actual fix in a
privileged agent guest (deliberately created with --unprivileged 0), where the
agent is host root and mkdir would otherwise create a root-owned directory. Per-volume
results are reported back keyed by volume id via the volumes field above; the
manager writes them into Volume.status (ready / failed). The container
create job blocks on Volume.status = 'ready' before attaching the Proxmox
mpN bind mount. The agent only ever creates directories — it never removes
them, so volume data is retained across container
delete + recreate. See Volumes and
Deploying Agents → Volume storage.
Environment Variables¶
Read from the process environment (systemd loads /etc/environment). Set via container runtime (Docker ENV, Proxmox LXC config) — the base image's environment.sh service propagates them on boot.
| Variable | Required | Description |
|---|---|---|
SITE_ID |
Yes | Numeric site ID from the manager |
MANAGER_URL |
Yes | Base URL of the manager (e.g., http://192.168.1.10:3000) |
API_KEY |
No | Admin API key for remote agents. Not needed on the manager (localhost is trusted). |
AGENT_SERVICES |
No | Comma-separated service groups: nginx, dnsmasq, mail. Default nginx,dnsmasq. |
MAIL_POSTFIX_DB_PASSWORD |
No | DB password for the mail_postfix role (mail hosts without local socket auth) |
MAIL_DOVECOT_DB_PASSWORD |
No | DB password for the mail_dovecot role (mail hosts without local socket auth) |
MAIL_HELPER_DIR, MAIL_HELPER_SOCKET_DIR, OPENDKIM_SOCKET_DIR |
No | Mail helper install dir and milter socket dirs (defaults match the packages) |
VMAIL_DIR, GC_STATE_FILE, GC_RETENTION_MS |
No | Maildir root (default /var/vmail), mailbox GC state file, and GC retention (default 30 days) |
STATE_DIRECTORY |
No | State directory, set by systemd via StateDirectory= (default /var/lib/opensource-agent) |
On mail hosts MANAGER_URL must be the manager's public URL — one-click unsubscribe links use it as their base.
The agent Dockerfile defaults to SITE_ID=1 and MANAGER_URL=http://localhost:3000 so the manager container works without configuration.
Managed Services¶
Services are organized in groups enabled via AGENT_SERVICES. The check-in reports the enabled groups plus any missing binaries; the manager refuses the mail-host claim while anything is missing.
| Group | Service | Files | Test | Reload |
|---|---|---|---|---|
| nginx | nginx | /etc/nginx/nginx.conf |
nginx -t |
systemctl reload-or-restart nginx |
| dnsmasq | dnsmasq | /etc/dnsmasq.conf, /var/lib/dnsmasq/{dhcp-hosts,hosts,dhcp-opts,servers} |
dnsmasq --test |
restart when /etc/dnsmasq.conf changed, otherwise SIGHUP |
| opendkim | /etc/opendkim.conf, $STATE_DIRECTORY/mail/{opendkim-keytable,opendkim-signingtable,dkim/*.private} |
opendkim -n |
restart | |
| dovecot | /etc/dovecot/dovecot.conf |
doveconf -n |
reload + doveadm auth cache flush |
|
postfix (status: postfix@-) |
/etc/postfix/{main.cf,master.cf,vdomains,sni_map,sql/senders.cf,sql/mailboxes.cf} |
postfix check (after postmap -F postWrite) |
reload-or-restart | |
| opensource-mail-helper | /opt/opensource-server/mail-helper/config.local.json |
— | restart |
The mail group renders only on the agent holding the mail-host claim (the mail section of the snapshot). The helper is its own component/package (mail-helper/) — a Postfix milter on a unix socket inside the Postfix chroot, ordered before OpenDKIM so the DKIM signature covers the List-Unsubscribe headers it injects. Taking the claim provisions directories (system users come from the helper package) and enables + starts the units; losing it — or removing mail from AGENT_SERVICES — stops and disables them, leaving configs and /var/vmail in place. Maildirs of deleted accounts are garbage-collected after 30 days.
Apply flow per service: render templates, reconcile file mode/owner even when contents are unchanged, stage new files, run post-write commands, run the test command, roll back on failure (re-running post-write commands against the restored sources), then reload. Failures are reported as lastApply: "failure" at the next check-in.
To add a managed service, add a ManagedService entry in agent/src/apply.ts (or agent/src/mail.ts for the mail group) plus its template(s) under agent/templates/, and extend the config snapshot in create-a-container/utils/agent-config.js.
Running Manually¶
systemctl status opensource-agent.timer # timer state
journalctl -u opensource-agent.service # past runs
node /opt/opensource-server/agent/dist/index.js # single run by hand