Browse documentation
DURPDEPLOY DOCUMENTATION

Agent container setup

Configure the standalone agent runtime and container execution.

On this page

This runbook configures a DurpDeploy server and one outbound-only remote agent. It covers the server listener, admin pairing, agent installation, routing, maintenance, and recovery.

Execution location in step logs

Each attempt logs its physical hostname, execution mode, interpreter, and attempt number before script output. Container attempts also log the runtime, container name, and resolved image ID. The server records the selected agent name and ID. These entries use the existing redacted log upload path.

Container execution uses the physical hostname reported by Docker or Podman. Host execution uses this runtime information when available. Native host agents otherwise use the operating system hostname. Unavailable physical hostnames appear as unknown.

For a containerized host agent without runtime information, set DURPDEPLOY_AGENT_HOSTNAME to the physical machine hostname. This setting overrides automatic hostname detection. The setting does not grant runtime socket access.

DURPDEPLOY_AGENT_HOSTNAME=app-server-02

Read execution locations in deployment logs on the server. Retries and fan-out retain separate entries.

Container step execution (agent/3)

Upgrade the DurpDeploy server for the v3 contract before enabling this mode. The default agent continues to report v2 host interpreter capabilities. Pairing identity and state persist across upgrades; do not re-pair just to enable v3.

Explicit opt-in requires these service environment entries:

DURPDEPLOY_AGENT_CONTAINER_ENABLED=true
DURPDEPLOY_AGENT_CONTAINER_RUNTIME=docker
DURPDEPLOY_AGENT_CONTAINER_SOCKET=unix:///var/run/docker.sock

Select podman and its existing local Unix socket for Podman. TCP and SSH endpoints are rejected. Install the selected client for a native agent. The agent image contains both clients, but mounts no socket by default.

Runtime preflight checks socket connectivity and cleanup recovery. It does not gate startup on the reported operating system, seccomp profile, or CPU/memory/PID capabilities. Runtime access or cleanup failures are logged and block polling until recovery. PID-controller support is checked before each container attempt so a runtime cannot silently ignore the required process limit. This does not block startup or check-ins. Other unsupported workload options fail at launch.

Native systemd

For Docker, enable the host’s Docker socket and deliberately grant the dedicated agent service account membership in its existing runtime socket group. Configure SupplementaryGroups=docker in a service drop-in when that is the socket group. For rootless Podman, run the socket under the same dedicated execution account as the agent and configure, for example, unix:///run/user/10001/podman/podman.sock with that account’s actual UID. Configure user-service persistence and cgroup delegation through the host’s normal service administration. Keep the supplied systemd hardening and DURPDEPLOY_AGENT_EXECUTION_BOUNDARY=service in place.

The service account must already have permission to connect to the socket. The agent does not change socket owner, group, permissions, or SELinux labels. Use a host policy that permits the intended socket connection; do not disable the workload’s security restrictions to work around access errors.

Agent container with a Docker socket

Use the explicit override, leaving the default Compose definition socket-free:

export AGENT_RUNTIME_GID=$(stat -c %g /var/run/docker.sock)
docker compose -f compose.example.yml -f compose.agent-docker.yml up -d

The agent remains non-root and receives only the existing socket group. Its state remains in the private named volume. A read-only socket mount prevents filesystem changes through that mount; it does not limit commands sent through the runtime API.

Agent container with a rootless Podman socket

Start the socket under the dedicated execution account, then use its real path:

systemctl --user enable --now podman.socket
export AGENT_PODMAN_SOCKET="$XDG_RUNTIME_DIR/podman/podman.sock"
podman compose -f compose.example.yml -f compose.agent-podman.yml up -d

The explicit override maps the operator UID/GID to service UID/GID 10001 through keep-id:uid=10001,gid=10001, so it connects without modifying socket permissions. Use a fresh private named state volume for that identity, or deliberately migrate existing state ownership during a stopped upgrade. Do not delete pairing state to solve an ownership mismatch.

Workload boundary and recovery

Each step container uses a fixed Bash, PowerShell, or Python entrypoint as UID 65534, a read-only root, writable 64 MiB /tmp, no network, no capabilities, no new privileges, and at most 128 processes. The agent imposes no per-step RAM ceiling or CPU quota, including on retries. Applicable host, runtime, and parent cgroup limits still govern execution. A step can consume more host RAM and CPU; the operator is responsible for external resource policy. Step containers created through the runtime socket may run outside the agent service’s own cgroup, so the service’s CPU and memory limits do not necessarily constrain these workloads. The service limits and the step’s PID limit and tmpfs capacity remain in place. Each step mounts no agent state, host directory, or runtime socket. The runner pulls missing images, rejects declared image volumes on both engines, and executes the inspected image ID for every retry of that step. The image must contain its selected interpreter. Every workload explicitly selects Docker’s seccomp=builtin profile or Podman’s /usr/share/containers/seccomp.json profile on the runtime host. Capability metadata does not block check-ins; the runtime must support those launch options to execute container steps. Older Docker versions that lack seccomp=builtin can check in but reject container step launches. Script exit 127 follows configured retries; it cannot prove a missing entrypoint. PowerShell scripts are staged inside the private /tmp and run with -File so multiline blocks and terminating errors retain script execution semantics.

An empty variable_names passes compatible resolved variables. A non-empty list passes only its names. Runtime-client variable prefixes (DOCKER_, PODMAN_, CONTAINER_, CONTAINERS_, SSH_, XDG_, LD_) and HOME, PATH, TERM, TMPDIR, REGISTRY_AUTH_FILE, GODEBUG, GOTRACEBACK, GOMEMLIMIT, GOMAXPROCS, BASH_ENV, ENV are excluded from default container injection and rejected in explicit selections. Secret values never enter client arguments. Workload output goes through the existing scrubber. An output line over 1 MiB stops the step without retry; container cleanup still runs. Redacted output is framed into UTF-8 log events and batches within protocol limits. Log uploads use the execution cancellation context and a ten-second deadline; an unavailable log endpoint cannot hold execution cleanup indefinitely. Log delivery failures stop execution without retrying a step’s side effects.

Containers are labelled with a namespace derived from the agent ID, paired server URL and stable agent certificate fingerprint. Independent pairings do not share ownership labels, including when their agent IDs match. After every attempt, including cancellation, the agent removes the container and confirms absence. Cleanup uncertainty produces cleanup_unconfirmed and stops new claims and retries. Restore runtime access; reconciliation removes only this agent’s attempts before polling resumes. Started container claims retain a terminal report encrypted to the agent identity in private state. After a restart or reporting outage, reconciliation and acknowledged report delivery must both finish before polling resumes. An unreadable or rejected pending report blocks polling. Unrelated containers are never selected. The server must retain cleanup uncertainty until authenticated ready polling confirms reconciliation. An acknowledged cleanup report retains its original runtime and socket until reconciliation confirms cleanup. Recovery skips reports already acknowledged. Before ordinary results or cancellation acknowledgements are sent, the agent replaces the conservative cleanup report with the actual terminal request, encrypted to its identity. It retains that request until the server acknowledges delivery, and replays the original protocol and pairing after a restart. Failed delivery blocks polling without repeating execution. If recovery state cannot be written after start is acknowledged, the agent reports a terminal failure without starting a workload. Recovery also requires the original runtime and socket recorded with the claim; changing the endpoint cannot confirm cleanup on the previous daemon. One agent process holds a kernel lease on its state directory across reconnects. A second process exits before runtime reconciliation; replacement must wait for the first process to stop. The persistent empty agent.lock file must not be removed. The server must acknowledge repeated cleanup reports for the same authenticated claim without changing the recorded result. A generic HTTP 409 cannot confirm whether a prior report was accepted, so the agent retains the claim and blocks polling until the server can acknowledge delivery.

Runtime socket access gives the agent service host-equivalent authority. Host steps share that service UID and can also use its socket access. Use a dedicated execution host for untrusted scripts. Workload isolation does not restrict the agent service itself, and the operator remains responsible for images, scripts, supplied secrets, and their effects inside the chosen boundary.

Two storage boundaries

The server owns the DurpDeploy database. SQLite, its WAL and SHM files, projects, releases, deployments, variables, audit records, and agent records stay on the server. The agent never receives the SQLite database, DURPDEPLOY_SECRET_KEY, or the server’s control-plane state directory.

The agent has no database. Its private state directory contains only the agent identity certificate and key, paired server identity state, and a current-claim marker (a token hash plus encrypted recovery and terminal reports), and the empty process-lease file. Keep that directory private and back it up only if preserving the enrolled identity is intentional.

Execution boundary and script responsibility

DurpDeploy does not inspect or guarantee the safety of deployment scripts. The operator is responsible for each script, the secrets and files intentionally made available to it, its network access, and every effect it can cause within the service or container boundary. Read-only storage does not prevent a script from reading visible files or exfiltrating supplied secrets.

Supported execution does not use chroot. Child interpreters and the agent share the preselected unprivileged durpdeploy-agent identity with all capability sets empty and NoNewPrivs enabled. The surrounding service supplies the filesystem and cgroup boundary:

  • Containers use a read-only image root, a private state volume, a private /tmp, default seccomp and AppArmor confinement, and CPU, memory, and process limits. They mount no host filesystem, control-plane state, container socket, or host cgroup tree.
  • systemd uses ProtectSystem=strict, ProtectHome=true, PrivateTmp=true, private mounts, a single writable state path, and CPU, memory, and task limits.

The agent fails execution unless the service boundary marker is available. A direct foreground run is for the initial pairing ceremony only; restart it through the supplied systemd or container definition before assigning deployments. Server and agent containers may share a host or dedicated bridge network when identities, process namespaces, and volumes remain separate. A remote agent host remains the preferred production placement.

An unprivileged agent cannot switch to a separate runner UID without SETUID/SETGID, and those capabilities are intentionally absent. Bash can therefore read or change the private agent state volume, including the paired identity. Use one agent boundary per trusted script domain and re-pair after any suspected state change. The separate host or container still keeps control-plane state and arbitrary host data out of reach.

The agent supports only the fixed interpreter names bash, pwsh, and python3. It reports only executables installed inside the active service or container boundary. The supplied container image includes Bash; build an operator-managed image containing pwsh or python3 to advertise those capabilities. Interpreter discovery never accepts a server-supplied path or arguments.

Agents initiate most runtime connections, but pairing still needs a temporary unpaired agent callback listener: after code and fingerprint confirmation, the server performs a one-shot server-init callback to that temporary listener. After completion acknowledgment, the temporary listener is closed and the agent has no persistent inbound listener.

Transport and ports

There are two separate HTTPS paths:

  • Caddy serves the browser and JSON API HTTPS origin on port 443. Caddy obtains the public certificate from Let’s Encrypt and reverse proxies to the normal application listener on port 8080.
  • The dedicated agent listener uses port 23187, or another operator-selected address, directly on the DurpDeploy process. It uses TLS 1.3, self-signed Ed25519 identities, and direct SHA-256 certificate fingerprint pinning. It is not routed through Caddy and must not be terminated by Caddy.

Allow inbound TCP 23187 for the server’s durable direct listener. Keep application port 8080 private. During pairing, each unpaired agent briefly opens a temporary local callback listener for one-shot server-init; once paired, that listener is closed. Agents need outbound TCP access to the server’s control listener for polling and heartbeats; no persistent inbound agent listener is required.

Configure the server listener

Configure the direct listener through the server environment file. make dev remains the ordinary browser/API development path and does not enable the agent listener. For a local foreground run, set the server listener variables before starting go run ./cmd/server; the public origin hostname becomes the self-signed certificate SAN:

DURPDEPLOY_AGENT_LISTEN_ADDR=:23187 \
  DURPDEPLOY_AGENT_PUBLIC_URL=https://localhost:23187 \
  DURPDEPLOY_AGENT_IDENTITY_DIR=.agent-identity \
  go run ./cmd/server

The public URL is the direct server mTLS endpoint, not the Caddy/Let’s Encrypt browser or API endpoint. The agent is outbound-only and has no database; its state directory is not server storage.

The server listener defaults to 0.0.0.0:23187 and advertises https://localhost:23187. For remote agents, configure a reachable public URL and a persistent identity directory:

DURPDEPLOY_AGENT_LISTEN_ADDR=0.0.0.0:23187
DURPDEPLOY_AGENT_PUBLIC_URL=https://<agent-control-host>:23187
DURPDEPLOY_AGENT_IDENTITY_DIR=/var/lib/durpdeploy/agent-identity

DURPDEPLOY_AGENT_PUBLIC_URL must be an HTTPS origin with no path, query, or fragment. Its hostname is placed in the self-signed certificate SAN. Pairing persists the direct listener URL for the agent; operators do not enter it on the agent host.

On a systemd server, put those variables in the root-owned server environment file referenced by the unit, for example:

sudo install -d -o durpdeploy -g durpdeploy -m 0750 /var/lib/durpdeploy/agent-identity
sudo install -m 0600 /tmp/durpdeploy.env /etc/durpdeploy/durpdeploy.env
sudo systemctl daemon-reload
sudo systemctl restart durpdeploy
sudo systemctl status durpdeploy --no-pager

The server unit already permits writes below /var/lib/durpdeploy. Keep the identity directory owned by durpdeploy, mode 0700 or stricter, and do not copy its private key to an agent. Check the listener and server logs before pairing:

sudo journalctl -u durpdeploy -n 100 --no-pager
sudo ss -ltn '( sport = :23187 )'

For Compose, add the same three variables to compose.app.env, mount a private server identity directory at the configured path, and publish only the direct listener port to the required agent network. The stock Compose files publish only Caddy’s 80 and 443, so port 23187 is intentionally not exposed by default. Do not put the server identity directory in the agent service.

Admin UI pairing workflow

Use an administrator browser session at the Caddy URL. The agent pages are admin-only.

  1. Open Admin, Agents, choose New agent, and enter the stable Agent ID, display name, and optional agent version. The ID must be unique.
  2. Start the unpaired local agent listener, then open the agent’s pairing page only when the operator can complete the ceremony. Enter the short-lived, one-time pairing code and compare the displayed fingerprint through a trusted channel, then confirm. The operator re-types only the displayed agent fingerprint in a dedicated second confirmation step; server-init (/agent/v1/pairings/server-init) uses that value plus the server-held code and pinned endpoint to finalize pairing. The values are console-only and cannot be retrieved later. Never put them in source control, tickets, chat, shell history, or logs.
  3. Assign an environment to the paired active agent from its details page, then verify its heartbeat before creating a deployment.

Each remote deployment is assigned to exactly one paired agent. It never falls back to local execution.

Agent start and pairing

Start an unpaired agent with only local inputs:

DURPDEPLOY_AGENT_STATE_DIR=/var/lib/durpdeploy-agent
DURPDEPLOY_AGENT_LISTEN_ADDR=0.0.0.0:23187
DURPDEPLOY_AGENT_VERSION=<agent-version>

DURPDEPLOY_AGENT_STATE_DIR defaults to the platform configuration directory, but production services set it explicitly. DURPDEPLOY_AGENT_LISTEN_ADDR is needed only while the local pairing listener is open. DURPDEPLOY_AGENT_VERSION is sent after pairing. Pairing uses agent/1; v0.2.0 paired traffic uses agent/2. There is no protocol variable. Upgrade the server to accept v2 before restarting upgraded agents. During rollout, the server continues to accept v1 agents as Bash-only workers.

The first run prints a short-lived pairing code and agent fingerprint. Enter those values in the authenticated admin pairing flow, compare the displayed fingerprint, and confirm before the 10-minute code expiry. Do not put the pairing code, fingerprint, endpoint, or private key in documentation, tickets, shell history, or logs. Pairing persists the server endpoint, pinned server identity, agent identity, and agent ID in the private state directory.

After pairing, restart with DURPDEPLOY_AGENT_STATE_DIR; the embedded build identity is reported by default. DURPDEPLOY_AGENT_VERSION remains an optional reported override. Do not supply a server URL, certificate, fingerprint, token, or agent ID manually. Normal work is outbound polling, heartbeats, log uploads, and result or cancellation acknowledgements. The agent stores no server secret or deployment payload at rest. A current claim marker contains the deployment ID and a SHA-256 hash of the claim token. Started container claims also retain an encrypted cleanup report, its acknowledgement state, and the original runtime endpoint until cleanup finishes. Completed claims retain their encrypted result or cancellation request until delivery is acknowledged.

Binary installation

Build the agent binary from the repository. This builds only cmd/agent and does not create or open a database:

make build
./durpdeploy-agent version
sudo install -o root -g root -m 0755 ./durpdeploy-agent /usr/local/bin/durpdeploy-agent
/usr/local/bin/durpdeploy-agent --version

Create the dedicated account and private state directory:

sudo useradd --system --home-dir /var/lib/durpdeploy-agent \
  --shell /usr/sbin/nologin durpdeploy-agent
sudo install -d -o durpdeploy-agent -g durpdeploy-agent -m 0700 \
  /var/lib/durpdeploy-agent
sudo install -m 0600 /tmp/durpdeploy-agent.env /etc/durpdeploy-agent.env

For first pairing only, a foreground run can use a protected environment file owned by the agent user:

sudo install -o durpdeploy-agent -g durpdeploy-agent -m 0600 \
  /tmp/durpdeploy-agent.env /var/lib/durpdeploy-agent/agent.env
sudo -u durpdeploy-agent sh -c \
  'set -a; . /var/lib/durpdeploy-agent/agent.env; exec /usr/local/bin/durpdeploy-agent'

The systemd procedure below is preferred for production. Its EnvironmentFile=/etc/durpdeploy-agent.env is read by systemd, while the service process receives the values without putting them in shell history.

Docker or Podman Compose

The Compose service is a co-located compatibility and validation path. It is not a server sidecar or a production placement recommendation. Production agents should run remotely on the host where the deployment commands belong.

Create compose.agent.env with the same local variables, use mode 0600, and do not include a credential-shaped literal:

DURPDEPLOY_AGENT_LISTEN_ADDR=0.0.0.0:23187

Leave DURPDEPLOY_AGENT_VERSION unset to report the executable’s build identity. For a locally stamped image, follow the Docker build arguments in software versions and build numbers.

Docker Compose:

chmod 0600 compose.agent.env
docker compose --profile agent up -d --build agent
docker compose --profile agent ps agent
docker compose --profile agent logs -f agent

Podman Compose:

chmod 0600 compose.agent.env
podman compose --profile agent up -d --build agent
podman compose --profile agent ps agent
podman compose --profile agent logs -f agent

The service mounts one private volume at /var/lib/durpdeploy-agent, has a private /tmp, and has no /data mount, server secret, Docker socket, host network, host cgroup mount, or persistent inbound listener. That volume is agent identity state, not SQLite and not a server backup. Its root filesystem is read-only, and Compose applies CPU, memory, and process limits.

systemd installation and operations

Install the unit shipped in the repository after installing the binary and environment file:

sudo install -m 0644 systemd/durpdeploy-agent.service \
  /etc/systemd/system/durpdeploy-agent.service
sudo systemctl daemon-reload
sudo systemctl enable --now durpdeploy-agent
sudo systemctl status durpdeploy-agent --no-pager

The unit runs the agent and child interpreters as durpdeploy-agent, sets the state directory, uses /etc/durpdeploy-agent.env, applies a private UMask=0077, and permits writes only to the agent state directory. It also applies NoNewPrivileges, private mounts and /tmp, and CPU, memory, and task limits. Keep both /etc/durpdeploy-agent.env and the state directory inaccessible to other users:

sudo chown root:root /etc/durpdeploy-agent.env
sudo chmod 0600 /etc/durpdeploy-agent.env
sudo chown -R durpdeploy-agent:durpdeploy-agent /var/lib/durpdeploy-agent
sudo chmod 0700 /var/lib/durpdeploy-agent

Operations:

sudo systemctl start durpdeploy-agent
sudo systemctl status durpdeploy-agent --no-pager
sudo journalctl -u durpdeploy-agent -f
sudo systemctl restart durpdeploy-agent
sudo systemctl stop durpdeploy-agent

After a successful pairing, keep the state directory across an ordinary restart. The agent resumes its pinned outbound connection without manual server settings.

Verify before deploying

Package jobs require a server and agent build that supports artifact/1 and staging/1. Preserve the paired state directory when upgrading the agent. Read the attached release package through ARTIFACT_PATH. Copy files into DURPDEPLOY_STAGE_DIR before modifying them for later jobs. Each job downloads and verifies the same pinned package.

Host jobs use read-only file permissions within the trusted operator boundary. Container jobs use a separate read-only, noexec package mount. Managed package files follow the staging cleanup and recovery rules. This feature adds no per-job cgroup or delegation requirement.

sudo systemctl is-active durpdeploy-agent
sudo journalctl -u durpdeploy-agent -n 50 --no-pager

In the UI, confirm the agent is active and its heartbeat is current. Start a small non-production deployment first. A remote deployment should show its assigned agent, not local execution.

Troubleshooting

404 or a stale binary

The dedicated listener has its own routes under /agent/v1; the browser and API routes are not valid substitutes. A 404 often means the agent URL points at Caddy or port 443 instead of the direct listener, or the server binary was built before the listener code was installed. Check the server’s listener settings and use ss -ltn to check port 23187. Verify the installed binary and restart the server. Check /usr/local/bin/durpdeploy-agent version and compare its full source commit with the expected artifact. Version commands work without runtime configuration and while the service holds its state lease. An explicit DURPDEPLOY_AGENT_VERSION override is labeled separately in the output; remove it if it masks the version expected in fleet diagnostics. Rebuild the agent with make build and reinstall it when its behavior does not match the checkout.

Wrong fingerprint

The agent rejects an unpinned or changed certificate. For an unexpected change, stop the agent and investigate the server identity through a trusted channel; then follow the controlled re-pair or planned rotation procedure below. Do not use trust-all TLS or accept a fingerprint copied from an untrusted connection.

Expired or reused pairing code

Pairing codes expire after 10 minutes and are consumed once. Restart the unpaired local listener to obtain a fresh code. For an already active agent, revoke and re-pair it first.

No match

Check that the environment is assigned to this paired active agent. The deployment remains waiting until that agent polls.

Revoked agent

Revocation blocks the old identity. Stop the old process, confirm the agent is revoked in Admin, Agents, use Re-pair, and pair the replacement using a controlled state directory. Do not restore the old private key onto an unrelated host.

Lost agent

Stop the service and inspect the agent and server journals. Started work that misses heartbeats becomes lost and is not automatically replayed. Review the original deployment, fix the host, and create an explicit new deployment.

Cancel unconfirmed

The server keeps the deployment running until the agent acknowledges a cancel. If the 30-second acknowledgement deadline expires, routing becomes cancel_unconfirmed. Treat the outcome as uncertain, inspect the host and logs, and create a new deployment only after confirming what happened. Do not retry the original deployment or assume cancellation succeeded.

Revocation, re-pairing, and fingerprint rotation

For a lost or compromised host:

  1. Stop the agent service and isolate the host.
  2. In Admin, Agents, revoke the agent.
  3. Review redacted agent events, deployment history, and the audit log.
  4. Prepare a replacement host with a new private state directory.
  5. Use Re-pair on the revoked agent and pair the replacement. Update only the replacement local listener configuration.

For planned server certificate rotation, keep the current server identity and fingerprint working first. The server can advertise the old and pending pins through an authenticated heartbeat. Agents stage [old, pending], connect to the pending certificate, and remove the old pin only after that verified connection succeeds. Do not delete the old server identity or pin until all agents have promoted the pending pin. If rotation is interrupted, restore the old listener identity and complete the staged process rather than replacing the state file by guesswork.

Upgrades, rollback, and backup scope

Migrate the agent port

The default pairing listener changes from TCP port 10943 to 23187. Existing paired agents retain their stored server endpoint and do not reopen the pairing listener during a compatible upgrade.

  1. Wait for active work to finish before stopping the agent.
  2. Back up the private state directory and preserve its permissions.
  3. Install the new binary without deleting the state directory, identities, or pins.
  4. If an explicit listener override exists, retain it or change DURPDEPLOY_AGENT_LISTEN_ADDR to the intended address.
  5. For a new container pairing, map the selected host port to container port 23187.
  6. If temporary inbound firewall access is required for pairing, allow only the selected port from the server.
  7. Remove temporary inbound access after pairing completes.
  8. Permit outbound TCP access to the server’s dedicated agent endpoint.
  9. Restart the agent and verify polling, a deployment, and streamed logs.

For systemd, set listener overrides in /etc/durpdeploy-agent.env. For Compose, use compose.agent.env and an operator-selected temporary pairing port mapping. The supplied image and Compose definitions keep pairing ports unpublished by default.

The server’s new default endpoint uses port 23187. Existing agents continue to use the server URL saved during pairing. Changing the server’s advertised URL does not update existing agent state. Keep the old server endpoint reachable through TCP forwarding, or retain explicit server port overrides. Preserve TLS passthrough and the existing server identity during this transition. Do not reset identities, change pins, or edit pairing records to change a port. Coordinate server endpoint migration with server PR #208.

make dev publishes host port 23188 to container port 23187. This keeps the local pairing listener separate from the server’s port 23187. Set AGENT_PORT to retain a different host port. Explicit legacy listener and host port overrides remain supported.

Use compatible server and agent releases. Check durpdeploy-agent --version before and after installation and compare the embedded commit with the intended artifact. See software versions and build numbers. For an agent, build a new durpdeploy-agent, install it over the binary, and restart the service. The next poll reports the new build identity unless a reported override is configured; compatible upgrades do not require re-pairing. Preserve the state directory across a compatible upgrade. If rollback is required, stop the service, install the previous binary, and restore the matching known-good configuration. Do not delete pins or identity files during an ordinary binary rollback.

Back up the server SQLite database and the matching server encryption key as a pair. Litestream or SQLite backup procedures cover server database state. Back up an agent state directory only to preserve that exact paired identity. It is not a database, and restoring it to another host transfers the identity.

Safety rules

Do not route port 23187 through Caddy. Do not use a central CA, trust-on-first- use, SSH, a trust-all TLS setting, inbound agent ports, or a shared agent state directory. Do not give an agent the server database, server secret key, Docker socket, or control-plane state directory.

For protocol details and fixed timing limits, see agent-protocol.md. For server deployment and backup operations, see deploy.md and backup-restore.md.