Skip to main content

Caddy

@svendowideit/caddyv2026.10.18.3· MIT· 6d agoBEHAVIORSWORKFLOWSREPORTSACCEPTED WARNINGS
01README

Caddy reverse-proxy and service management for a Linux host with systemd. WHAT IT DOES

Manages a Caddy reverse proxy on a Linux host with systemd — install the binary, run it as a systemd user service, and drive routes, static file serving, and TLS live through Caddy's admin API. Prefer it over hand-editing a Caddyfile or a Go Caddy plugin when you want routes added and removed without a restart and managed from swamp; no Go toolchain is needed. Adding a route implies the DNS and TLS it needs: every route hostname under baseDomain automatically gains a TLS subject and an A record, so a single ensureDnsProxy makes a host reachable without hand-listing subjects or records. When a DNS provider is configured, the DNS-01 challenge covers EVERY name the config serves — route hostnames and the status page's hostnames (the baseDomain and declared record names), not just the subjects a model listed — so no served name falls back to HTTP-01 (which ACME cannot validate for a name that resolves to a private address). It can also publish a settings/document directory over HTTP(S) with file_server, issue wildcard certificates through DNS-challenge providers (including multi-field ones like Gandi, Namecheap, and DreamHost), manage static A/AAAA/CNAME records through any dns.providers module, and serve a default health contract (/, /teapot) on each configured hostname.

It runs unprivileged (plain HTTP on high ports) or as a Let's Encrypt TLS proxy on 80/443, and routes persist across restarts via autosave + --resume.

INSTALL

swamp extension pull @svendowideit/caddy

DEPENDENCIES

@svendowideit/sudo — pulled automatically. Granting CAP_NET_BIND_SERVICE so the unprivileged user service can bind 80/443 (the setcap that caddy applies after it writes a binary) is delegated to that extension's elevation ladder, so caddy never calls sudo itself. Otherwise it needs only the swamp CLI, a Linux host with systemd user services, and network access to caddyserver.com to download the binary.

RUN

# Create the model, choosing your public domain and ACME contact address.
swamp model create @svendowideit/caddy my-caddy \
  --global-arg baseDomain=example.com \
  --global-arg letsEncryptEmail=admin@example.com

# Download the Caddy binary to caddyBinPath (run once per host).
swamp model method run my-caddy installCaddy

# Write and start the systemd user service (run once, or after an upgrade).
swamp model method run my-caddy createService
swamp model method run my-caddy startService

# Do the whole setup idempotently in one command: install the binary, grant
# the capability as its own placed @svendowideit/sudo step (operation
# setcap — never runModel, which cannot run on a worker), create/start the
# service, configure TLS (provider inferred from plugins), apply DNS
# records, and install the status page. A single caddy model is set up
# automatically; with several it lists them.
swamp workflow run @svendowideit/caddy-setup

# Upgrade the Caddy this repo manages after updating the extension. With a
# single caddy model it upgrades automatically (no name to remember); with
# several it lists them and stops without changing anything, printing the
# command for each. It prints a gap report (everything missing to serve:
# capability for 80/443, baseDomain, ACME email, TLS, DNS records) with the
# exact fix command for each, then pauses for approval before
# rebuilding/restarting.
swamp workflow run @svendowideit/caddy-upgrade
swamp workflow run @svendowideit/caddy-upgrade --input model=my-caddy

# Point a hostname at a backend, creating the route if it is missing.
swamp model method run my-caddy ensureDnsProxy \
  --input hostname=foo.example.com --input upstream=127.0.0.1:8080

# Publish a directory of documents over HTTPS at a hostname.
swamp model method run my-caddy serveSettings \
  --input hostname=settings.example.com --input root=/srv/settings/current

# Grant CAP_NET_BIND_SERVICE so the unprivileged user service can bind
# 80/443. A caddy method never elevates in-process (context.runModel is
# unavailable under swamp worker remote execution), so the grant is its
# own step driving the @svendowideit/sudo model — exactly what the
# caddy-setup/caddy-upgrade workflows do. Path must be absolute.
swamp model @svendowideit/sudo method run run sudo-caddy \
  --global-arg 'allowedOperations:json=["setcap"]' \
  --input operation=setcap \
  --input 'args:json={"path":"/home/you/.local/bin/caddy","capabilities":"cap_net_bind_service=+ep"}'

# Install the default status page that answers on http://localhost and
# https://localhost (done automatically by installCaddy/startService).
swamp model method run my-caddy configureStatusPage

# Inspect EVERYTHING in one command: it finds all caddy models, groups
# them by the Caddy they manage, merges what each wants, reads each live
# config, and prints a table with drift. No model names needed; the
# trailing name is a scratch instance it auto-creates. Read-only.
swamp model method run @svendowideit/caddy audit caddy-status
swamp data get caddy-status audit --json | jq '.content.cadies[] | {target, models, desiredRoutes, onlyDesired, onlyActual, inSync}'

# To diff the whole desired config against the live config for one Caddy:
swamp model method run @svendowideit/caddy plan caddy-status
swamp data get caddy-status plan --json | jq '.content | {desiredConfig, actualConfig}'

# --- Wildcard certificate via DNS-01 (worked example: Gandi) ---
# A wildcard cert (*.example.com) cannot use the HTTP-01 challenge: Caddy
# must write a _acme-challenge TXT record through the provider's API. That
# needs TWO things set up BEFORE configureTls, or it silently fails:
#   (a) the provider driver compiled into the binary, via `plugins`; and
#   (b) its token in the service's environment, via `environmentFile`.
#
# 1. Create the model with the Gandi driver and an env file for the token.
swamp model create @svendowideit/caddy my-caddy \
  --global-arg baseDomain=example.com \
  --global-arg letsEncryptEmail=admin@example.com \
  --global-arg 'plugins:json=["github.com/caddy-dns/gandi"]' \
  --global-arg environmentFile=~/.config/caddy/dns.env
#
# 2. Install the binary, put your Gandi Personal Access Token in the env
#    file (field name for Gandi is bearer_token), then create the service
#    so the unit loads it. Gandi's old API key is NOT supported.
swamp model method run my-caddy installCaddy
printf 'GANDI_BEARER_TOKEN=YOUR_GANDI_PAT\n' > ~/.config/caddy/dns.env
chmod 600 ~/.config/caddy/dns.env
swamp model method run my-caddy createService   # renders EnvironmentFile=
swamp model method run my-caddy startService
#
# 3. Configure the ACME DNS-01 challenge: map the driver's credential
#    field to the env var name, and list the wildcard + apex subjects.
swamp model method run my-caddy configureTls \
  --input dnsProvider=gandi \
  --input 'providerConfig:json={"bearer_token":"GANDI_BEARER_TOKEN"}' \
  --input 'subjects:json=["*.example.com","example.com"]'
#
# 4. Verify: point a route under the wildcard at a backend and request it;
#    a valid cert means the DNS-01 challenge succeeded.
swamp model method run my-caddy ensureDnsProxy \
  --input hostname=app.example.com --input upstream=127.0.0.1:8080
#
# Serve an app that lives under a sub-path (e.g. /dashboard) at the
# hostname root: only "/" is rewritten, so /dashboard/assets/... still
# reaches the app.
swamp model method run my-caddy ensureDnsProxy \
  --input hostname=dashboard.example.com \
  --input upstream=127.0.0.1:9090 --input rootPath=/dashboard
#
# See the README's "Wildcard certificates with a DNS provider (step by
# step)" section for the full walkthrough and troubleshooting.

# --- Static A/AAAA/CNAME records (same provider as TLS) ---
# Caddy owns the (name, type) RRset: each reconcile replaces it with
# exactly the declared values. The provider block reuses configureTls's
# provider + credential, so one token in the env file covers both DNS-01
# and record-writing. Records are declared as a global arg.
swamp model create @svendowideit/caddy dns-caddy \
  --global-arg baseDomain=example.com \
  --global-arg 'plugins:json=["github.com/caddy-dns/gandi"]' \
  --global-arg environmentFile=~/.config/caddy/dns.env \
  --global-arg 'dnsRecords:json=[{"name":"otel.example.com","type":"A","value":["192.0.2.5"],"zone":"example.com"}]'
swamp model method run dns-caddy configureTls \
  --input dnsProvider=gandi \
  --input 'providerConfig:json={"bearer_token":"GANDI_TOKEN"}'
swamp model method run dns-caddy installCaddy   # compiles caddy-host-dns in
# Apply the records as its own step — re-run after any dnsRecords edit:
swamp model method run dns-caddy applyDnsRecords
#
# zone is required unless the provider implements libdns.ZoneLister
# (Gandi does not); set it to the registered zone, e.g. fi.gy.
# To delete a record, declare it under dnsRemovals (dropping it from
# dnsRecords leaves it in DNS), then run applyDnsRecords.

# --- Health contract (default on) ---
# On each status/default hostname Caddy answers '/' -> 200 (status page),
# '/teapot' -> 418, and any other path -> 404. Verify after a reconcile:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/           # 200
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/teapot     # 418
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/nope       # 404
# Set healthRoutes=false to omit it.

MANAGING THE MODEL

Find the model(s): swamp model list --json | jq -r '.results[] | select(.type=="@svendowideit/caddy") | .name' Inspect one: swamp model get --json | jq '.globalArguments' A model is "active" because YOU name it on the command; its serviceName (default "caddy") is the systemd unit and adminApiAddr (localhost:2019) is the running Caddy's admin API. The status page names the model that last configured it.

Change it: swamp model create fails on an existing name by design. Edit instead, non-interactively, by piping the full definition to stdin: swamp model get --json | jq '{name,version,tags,globalArguments} | .globalArguments.baseDomain="new.example.com"' | swamp model edit --json Global args only take effect on the NEXT method run: re-run what consumes the arg (plugins -> installCaddy; listenAddrs/autoHttps/environmentFile -> createService + restartService; baseDomain -> configureTls etc.).

Multiple models: fully supported — several models that manage the SAME Caddy are MERGED, not fought over. Each mutating method records what that model wants (a desired resource), then reconcile unions every model whose (target, serviceName) matches into one valid config and applies it. Routes union; listenAddrs and TLS subjects union; a real conflict (two different upstreams for the same hostname, or differing TLS email/provider) is reported naming the models rather than silently resolved. swamp-managed routes are tagged with a Caddy @id, so hand-added routes are preserved. A model only manages a SEPARATE Caddy when it has a different serviceName (e.g. a second instance on other ports) — set target= for another host. target defaults to this machine's hostname; reconcile defaults to true.

CONFIGURE

All arguments are optional and set as global args when creating/editing the model:

baseDomain         Base domain for derived hostnames
letsEncryptEmail   ACME email (enables automatic HTTPS)
autoHttps          on|off|disable_redirects|disable_certs|...
listenAddrs        HTTP listen addresses (default [":443",":80"])
plugins            Extra Caddy modules to compile in (caddy-host-dns and
                   caddy-teapot-module are always added)
dnsRecords         Static records to own, e.g.
                   [{"name":"otel.example.com","type":"A","value":["192.0.2.5"],"zone":"example.com"}]
                   (type A|AAAA|CNAME; optional ttl). zone is required
                   unless the provider implements libdns.ZoneLister
                   (Gandi does not). Needs tls's dnsProvider set; apply
                   with `applyDnsRecords`.
dnsRemovals        Records to delete each reconcile, e.g.
                   [{"name":"old.example.com","type":"A","zone":"example.com"}]
                   (empty value removes the whole name+type RRset);
                   apply with `applyDnsRecords`
dnsTtl             Default TTL for dnsRecords (default 5m) so changes
                   propagate quickly; a record's own ttl overrides it
dnsSoa             Manage the zone SOA through caddy-host-dns (the
                   dns_records app's soa block; needs a 2026-10-18+ binary),
                   e.g. {"mname":"ns1.gandi.net.","rname":"hostmaster.gandi.net.",
                   "refresh":"10800s","retry":"3600s","expire":"604800s",
                   "minimum":"300s"}. `minimum` is the RFC 2308 negative-
                   caching TTL: lower it so a new/changed name is usable
                   sooner. Gandi does not return the SOA, so set mname/rname.
                   Empty leaves the zone SOA untouched
healthRoutes       true (default): on status/default hostnames serve
                   '/' -> 200, '/teapot' -> 418, other -> 404
adminApiAddr       Admin API address (or unix//path)
vaultName          Vault used by storeConfig
environmentFile    systemd EnvironmentFile for DNS-provider tokens
                   (e.g. ~/.config/caddy/dns.env); createService renders
                   EnvironmentFile=-<path> so wildcard/DNS-01 works without
                   hand-editing the unit
target             Host this model's Caddy runs on (default: this machine).
                   With serviceName it identifies the Caddy; models sharing
                   (target, serviceName) merge. Set it for a remote/worker
                   host or to keep a second instance separate.
reconcile          true (default) merges all models for this target; false
                   writes only this model's config

Ports 80/443 need CAP_NET_BIND_SERVICE on the binary. No caddy method elevates in-process: context.runModel is unavailable under swamp worker remote execution, so the grant is its own placed workflow step driving the @svendowideit/sudo model — the caddy-setup and caddy-upgrade workflows do this (installCaddy takes setCapabilities=false; setCapabilities only checks and reports). A config is never rejected for a privileged-port bind failure: until the capability is granted, reconcile serves what is wanted on unprivileged ports (:80 -> :8080, :443 -> :8443, plain HTTP) so Caddy always loads it, and reapplies the desired settings once the capability arrives.

A status page is wanted by default (opt out with configureStatusPage disabled=true): it answers on localhost and, once the port/TLS layer is up, on each configured hostname, listing the install (version, domain, ACME email, privileged-port capability, and the compiled-in plugins) and linking to the admin API and routes; reconfigure it with configureStatusPage.

Override per call with --input, e.g. addProxyService --input serviceName=app --input upstream=127.0.0.1:8080, or configureTls --input dnsProvider=cloudflare --input 'subjects:json=["*.example.com"]'. For multi-field DNS providers pass a providerConfig map of credential field to environment variable, e.g. Gandi '{"bearer_token":"GANDI_TOKEN"}', Namecheap '{"api_key":"NC_KEY","user":"NC_USER"}', DreamHost '{"api_key":"DH_KEY"}' — but a DNS provider only works if its driver is in plugins AND its token is in the service environment (environmentFile); see the RUN wildcard example and the README's step-by-step section.

UPGRADE

Update the extension (swamp extension update), then run the upgrade workflow. It reads the caddy model definitions on disk — you never need to know a name: a single model is upgraded automatically, and when there are several it lists them, changes nothing, and prints the command for each:

  swamp workflow run @svendowideit/caddy-upgrade

Before changing anything it prints a gap report: every setting that stops Caddy serving (capability to bind 80/443, baseDomain, ACME email, TLS, DNS provider/records, a stale binary) with the exact command to fix each. It pauses at one approval gate, then rebuilds the binary if stale, re-grants the capability, regenerates the unit, and restarts; and re-runs the gap report so you see what is left. Idempotent:

  swamp workflow run @svendowideit/caddy-upgrade --input model=my-caddy

Approve the gate with swamp workflow approve @svendowideit/caddy-upgrade confirm then swamp workflow resume @svendowideit/caddy-upgrade. Secrets (a DNS token) are never collected over a signal — the gap report names the model's environmentFile to write instead.

WHAT IT INSTALLS

A Caddy binary at caddyBinPath (default ~/.local/bin/caddy) — optionally with CAP_NET_BIND_SERVICE set so it can bind 80/443 unprivileged, and always including github.com/SvenDowideit/caddy-host-dns and github.com/hairyhenderson/caddy-teapot-module — a systemd user service (default caddy.service), a Caddyfile under ~/.config/caddy, and network-facing reverse-proxy and file_server routes — plus a default status page and health contract (/, /teapot) — via Caddy's admin API. It installs no cron triggers or webhooks.

02Behaviors1
@svendowideit/caddyv2026.10.18.3caddy.ts
fn installCaddy(plugins?: array, force?: boolean, setCapabilities?: boolean, configureStatusPage?: boolean)
Download the current Caddy binary (with any requested module packages) and verify it runs
ArgumentTypeDescription
plugins?arrayOverride the module packages to include (defaults to global plugins)
force?booleanReinstall even if the binary already exists
setCapabilities?booleanAfter install, check whether the binary has CAP_NET_BIND_SERVICE and, if not, report the placed step that grants it (the @svendowideit/sudo model's `run` method, operation setcap). It never elevates in-process: context.runModel is unavailable under swamp worker remote execution, so the grant must be its own workflow step — see the @svendowideit/caddy-setup and @svendowideit/caddy-upgrade workflows. Defaults to false.
configureStatusPage?booleanAfter install, install a default status page answering on localhost, summarising the install and linking to the admin API. Requires the admin API to be reachable (a running service); skips with a log otherwise.
fn setCapabilities(capabilities?: array, binPath?: string, quiet?: boolean)
Grant CAP_NET_BIND_SERVICE to the Caddy binary by delegating setcap to the @svendowideit/sudo extension, so the unprivileged user service can bind 80/443
ArgumentTypeDescription
capabilities?arrayLinux capabilities to grant (default ['cap_net_bind_service=+ep'])
binPath?stringOverride the Caddy binary path (defaults to global caddyBinPath)
quiet?booleanSuppress the log message describing the command
fn createService(serviceName?: string)
Create the systemd user service unit for Caddy
ArgumentTypeDescription
serviceName?stringOverride the systemd service name (defaults to global serviceName)
fn startService(serviceName?: string)
Start and enable the Caddy systemd user service and verify the admin API
ArgumentTypeDescription
serviceName?stringOverride the systemd service name (defaults to global serviceName)
fn settingsGuidance()
Print the minimal settings needed for a useful Let's Encrypt TLS-configured Caddy
fn addProxyService(serviceName: string, upstream: string, baseDomain?: string)
Add a reverse-proxy route for a service via the Caddy admin API (live, no restart)
ArgumentTypeDescription
serviceNamestringService name; hostname is derived as <service-name>.<base-domain>
upstreamstringBackend host:port to proxy to (e.g. 127.0.0.1:8080 or https://127.0.0.1:8443)
baseDomain?stringOverride the base domain (defaults to global baseDomain)
fn removeProxyService(serviceName: string, baseDomain?: string)
Stop the backend systemd service and remove its Caddy route via the admin API
ArgumentTypeDescription
serviceNamestringService name whose route and systemd service to remove
baseDomain?stringOverride the base domain (defaults to global baseDomain)
fn startBackendService(serviceName: string)
Start a backend systemd user service by name
ArgumentTypeDescription
serviceNamestringsystemd user service name to act on
fn stopBackendService(serviceName: string)
Stop a backend systemd user service by name
ArgumentTypeDescription
serviceNamestringsystemd user service name to act on
fn restartBackendService(serviceName: string)
Restart a backend systemd user service by name
ArgumentTypeDescription
serviceNamestringsystemd user service name to act on
fn storeConfig(baseDomain?: string, letsEncryptEmail?: string, adminApiToken?: string, vaultName?: string)
Validate and write base domain, ACME email, and admin API token to the Vault
ArgumentTypeDescription
baseDomain?stringBase domain to store in the Vault (e.g. example.com)
letsEncryptEmail?stringLet's Encrypt / ACME email to store in the Vault
adminApiToken?stringAdmin API token to store in the Vault
vaultName?stringOverride the Vault name (defaults to global vaultName)
fn syncConfig()
Snapshot the effective config into a swamp resource
fn getConfig()
Read the stored config back from the swamp resource
fn configureTls(email?: string, dnsProvider?: string, dnsEnvVar?: string, providerConfig?: record, subjects?: array, issuer?: string, keyType?: string, resolvers?: array, skipPropagationCheck?: boolean, propagationDelay?: string)
Configure the Caddy TLS app (ACME email + optional DNS provider for DNS-challenge issuance)
ArgumentTypeDescription
email?stringACME email (defaults to global letsEncryptEmail)
dnsProvider?stringDNS provider name for DNS-challenge issuance (e.g. cloudflare)
dnsEnvVar?stringEnvironment variable holding a single-field DNS provider credential (default CADDY_DNS_API_TOKEN). Ignored when providerConfig is set.
providerConfig?recordProvider credential field -> environment variable, for multi-field providers. e.g. \
subjects?arraySubjects for the TLS policy (e.g. *.example.com, example.com)
issuer?stringIssuer module: "acme" (default, Let\
keyType?stringPrivate-key type for issued certificates: "rsa2048", "p256" (default), "p384", "p521" or "ed25519". Set "rsa2048" for a device that only accepts RSA keys.
resolvers?arrayRecursive DNS servers for the ACME DNS-01 propagation check (e.g. ["1.1.1.1:53"]). Empty uses the model\
skipPropagationCheck?booleanSkip certmagic's DNS-01 propagation self-check (propagation_timeout=-1) and rely on propagationDelay plus ACME's own validation. Defaults to the model's dnsSkipPropagationCheck (true).
propagationDelay?stringHow long to wait after writing the challenge record before use (e.g. 30s). Defaults to the model's dnsPropagationDelay.
fn applyDnsRecords()
Reconcile the desired static DNS records (dnsRecords/dnsRemovals globals) into Caddy, then report what was written
fn autoProxySwampServe(baseDomain?: string, prefix?: string, port?: number)
Detect running swamp serve systemd services and reconcile their proxy routes
ArgumentTypeDescription
baseDomain?stringOverride the base domain (defaults to global baseDomain)
prefix?stringsystemd unit name prefix for swamp serve instances
port?numberPort swamp serve instances listen on
fn upgradeCaddy(plugins?: array, confirm?: string)
Replace the Caddy binary (current release, with module packages) with confirmation, then restart the service
ArgumentTypeDescription
plugins?arrayModule packages to include (defaults to global plugins)
confirm?stringSet to 'upgrade' to confirm replacing the binary and restarting the service
fn checkHealth(serviceName?: string)
Report Caddy service + admin API health
ArgumentTypeDescription
serviceName?stringOverride the systemd service name (defaults to global serviceName)
fn upgradeTargets()
Discover every @svendowideit/caddy model this repo defines, with the settings an upgrade needs — no model name required
fn upgradeCheck()
Report everything blocking this Caddy from serving — the capability to bind 80/443, baseDomain/ACME email, TLS, DNS provider/records, a stale binary, service health — each with the exact command to fix it
fn normalizeServingSettings()
Make this model's automatic-HTTPS and listen-address settings match what it wants (public HTTPS) and what this host can do (bind 80/443) — correcting and persisting them. E.g. once the binary can bind privileged ports, switch off the unprivileged high-port preset the extension originally used because it could not.
fn stopService(serviceName?: string)
Stop the Caddy systemd user service
ArgumentTypeDescription
serviceName?stringOverride the systemd service name (defaults to global serviceName)
fn restartService(serviceName?: string)
Restart the Caddy systemd user service
ArgumentTypeDescription
serviceName?stringOverride the systemd service name (defaults to global serviceName)
fn ensureDnsProxy(hostname: string, upstream: string, rootPath?: string)
Idempotently ensure a full hostname proxies to a backend host:port
ArgumentTypeDescription
hostnamestringFull hostname to proxy (e.g. foo.example.com)
upstreamstringBackend host:port to proxy to (e.g. 127.0.0.1:8080 or https://192.0.2.10:8443)
rootPath?stringServe an app that lives under this sub-path (e.g. '/dashboard') at the
fn serveSettings(hostname: string, root: string, browse?: boolean)
Idempotently serve a static settings document directory over HTTP(S) at a hostname via Caddy file_server
ArgumentTypeDescription
hostnamestringFull hostname to serve the settings documents on (e.g. settings.otel.fi.gy)
rootstringDirectory holding the rendered settings documents (e.g. ~/.local/share/otel-settings/current)
browse?booleanEnable directory browsing (off by default; individual documents remain fetchable)
fn configureStatusPage(title?: string, extraLinks?: array, disabled?: boolean)
Install (or remove) a default status page answering on localhost, summarising the install and linking to the admin API and configured routes
ArgumentTypeDescription
title?stringHeading shown on the status page
extraLinks?arrayAdditional links to show on the status page
disabled?booleanSet true to remove the status page route instead of adding it
fn plan()
Show ALL models for this Caddy merged, beside the live config, with drift and both configs to diff
fn audit()
One command: find every caddy model, group them by the Caddy they manage, and report each Caddy's merged desired-vs-actual config and drift

Resources

install(infinite)— Caddy binary install status
dnsConfig(infinite)— Applied static DNS records (dns_records) for this model
service(infinite)— Caddy systemd user service status
guidance(infinite)— Minimal Let's Encrypt settings guidance
proxyServices(infinite)— Current reverse-proxy services managed by Caddy
config(infinite)— Effective Caddy configuration snapshot
tlsConfig(infinite)— Caddy TLS automation configuration
autoProxy(infinite)— swamp serve auto-proxy reconciliation result
upgrade(infinite)— Caddy binary upgrade result
upgradeTargets(infinite)— Every @svendowideit/caddy model this repo defines
upgradeCheck(infinite)— Pre/post-upgrade readiness check for one Caddy model
serving(infinite)— Normalised automatic-HTTPS / listen-address settings
health(infinite)— Caddy service + admin API health check
ensureProxy(infinite)— Desired-state proxy ensure result
serveSettings(infinite)— Settings file_server route status
capabilities(infinite)— Linux capabilities on the Caddy binary
statusPage(infinite)— Default status page configuration
desired(infinite)— What this model wants from its Caddy (merged across peers)
reconcile(infinite)— Result of merging and applying peer models' desired state
plan(infinite)— Desired (merged) vs actual (live) config, with drift and the full configs to diff
audit(infinite)— Every caddy model grouped by the Caddy it manages, each with desired-vs-actual drift
03Workflows2
@svendowideit/caddy-setup6f3a1c2e-9b47-4d2e-8a51-0c9d2f4e7a10

Set up the Caddy this repo manages, idempotently, in one command. Run it after `swamp extension pull @svendowideit/caddy` and `swamp model create @svendowideit/caddy <name> --global-arg baseDomain=… --global-arg letsEncryptEmail=…`. It reads the @svendowideit/caddy model this repo defines (so a never-run model is found and the auto-created scratch instance never is); if exactly one exists it is set up automatically, otherwise pass --input model=<name>. In order it: installs the binary (grantin

discoverFind every @svendowideit/caddy model this repo defines, so a single model is set up automatically and several are listed (changing nothing).
1.scan— Discover and print the caddy model(s) in this repo.
setupInstall, service, TLS, DNS and status page for one model, in the order that works. Every step is idempotent and each depends on the previous, so nothing contends on the model lock.
1.install— Download the Caddy binary (with the model's module packages). The capability is granted by the following step, never by the method.
2.grant-capability— Grant CAP_NET_BIND_SERVICE to the Caddy binary so the unprivileged systemd user service can bind 80/443. This is its own placed step that drives the @svendowideit/sudo model's `run` method (operation `setcap`), NOT a context.runModel call inside installCaddy: runModel is unavailable under swamp worker remote execution, whereas a placed model step dispatches to the worker — so when this workflow runs on a worker the elevation happens on that same worker.
3.create-service— Write the systemd user unit from the model's current globals.
4.start-service— Start and enable the service, waiting for the admin API so the status page can be installed.
5.restart— Restart so the running process loads the freshly written unit (its EnvironmentFile) and, above all, the CAP_NET_BIND_SERVICE capability that installCaddy just set on the binary — a file capability only takes effect at exec() time, so without this Caddy keeps failing to bind 80/443 with "permission denied".
6.tls— Configure the TLS app. The DNS provider is inferred from the model's plugins (and the credential mapping from environmentFile) unless passed explicitly.
7.dns— Apply the model's declared static DNS records, through the provider configured above.
8.status-page— Install the default status page (answers on localhost).
9.check— Print the gap report so the run ends by telling you exactly what is still missing (if anything) and how to fix it.
@svendowideit/caddy-upgrade5add36ac-50d1-4042-b5c5-5af5e4820b8b

Upgrade the Caddy managed by @svendowideit/caddy — with no model name to remember — and tell you exactly what is still missing. Run it after updating the extension (`swamp extension update`). Caddy global arguments only take effect on the next method run, and a replaced binary drops its capability, so a version bump alone is not enough. It first discovers every @svendowideit/caddy model this repo defines (read from the model definitions on disk, so a never-run model is found and the auto-creat

discoverList every @svendowideit/caddy model this repo defines, with the settings and staleness that matter for an upgrade, and the exact command to upgrade each one. Reads model definitions on disk, so it finds a model that has never been run and never mistakes the auto-created scratch instance for a real one.
1.scan— Discover and print the caddy model(s) in this repo.
upgradeReport the configuration and gaps, confirm, then rebuild/re-unit/restart and re-report. Every step targets one model and runs strictly in order (each depends on the previous), so nothing contends on the model lock.
1.check-before— Print the configuration found for this model and every gap that stops it serving — capability to bind 80/443, baseDomain, ACME email, TLS, DNS provider/records, stale binary, service health — each with the exact command to fix it.
2.confirm— Interactive gate: the configuration and gap report are printed above. If they are correct, approve; if not, run the correction command shown in the report first, then approve. Skipped entirely when the single target model is already up to date (nothing to do).
3.install— Migrate the model definition and (re)install the binary. Downloads a new build only when a wanted module is missing, and restarts a running service so it picks up the new build. The capability is granted by the following step, never by the method.
4.grant-capability— Re-grant CAP_NET_BIND_SERVICE (a replaced binary drops it). Its own placed step driving the @svendowideit/sudo model's `run` method (operation `setcap`), never a context.runModel call inside installCaddy: runModel is unavailable under swamp worker remote execution, whereas a placed model step dispatches to the worker, so the elevation happens on the same worker the workflow runs on.
5.normalize-serving— Make the model's automatic-HTTPS / listen-address settings match what it wants (public HTTPS) and what this host can do (bind 80/443), correcting and persisting them — e.g. leaving the unprivileged high-port preset now that the binary can bind privileged ports. Skipped when there is nothing to correct. (grant-capability above already restored CAP_NET_BIND_SERVICE, so there is no separate setcap here.)
6.recreate-unit— Regenerate the systemd unit from the model's current global args and reload the daemon.
7.restart— Restart the Caddy service so it runs the upgraded unit.
8.status-page— Re-install the default status page. Does not fail the workflow if the admin API is not yet reachable.
9.tls— (Re)configure the TLS app. The DNS provider is inferred from the model's plugins (and the credential mapping from its environmentFile), so passing --input dnsProvider is optional. Runs before the dns step so records are applied through a provider that is already configured. Tolerated when the model has no ACME email yet.
10.dns— Reconcile desired static DNS records, if the model declares any. Runs after tls, so the provider is configured first. A model with no dnsRecords/dnsRemovals is a no-op, which the workflow tolerates.
11.check-after— Re-run the gap report so the run ends by telling you exactly what is still missing (if anything) and the command to fix each gap. A report, so it never fails the run.
04Reports1
@svendowideit/caddy-statusmethod
caddy_report.ts

Human-readable summary of which models manage each Caddy and whether the running config matches

caddysummary
05Previous Versions2
2026.10.17.1MIT(was none)
2026.10.11.1
06Stats
A
100 / 100
Downloads
26
Archive size
204.2 KB
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License2/2earned
  • Verified public repository2/2earned

08Platforms