Configuration
The daemon reads /etc/jaco/jacod.yaml at startup. The schema is
closed: any unknown key fails the parse with an error pointing at the
offending field. A missing file is equivalent to "all defaults" — the
daemon does not refuse to start when the config is absent.
The path can be overridden with the JACO_CONFIG environment variable,
honored by cmd/jacod.
Defaults
data_dir: /var/lib/jaco
listen_addr: 0.0.0.0:7000
cluster_addr: 0.0.0.0:7001
unix_socket: /var/run/jaco/jaco.sock
wg_port: 51820
log_level: info
ipam_pool: 10.244.0.0/16
node_status_interval: 30s
acme_email: ""
acme_ca: "https://acme-v02.api.letsencrypt.org/directory"
acme_enabled: true
acme_skip_staging: false
dns:
forwarders: [] # default: parse /etc/resolv.conf at startup
forwarder_timeout: 2sAll defaults live in internal/daemon/config/config.go. The same
constants seed the jacod.yaml template shipped in the packages, so a
freshly-installed cluster is functional with zero edits provided the
host has a private-LAN interface JACO can auto-detect.
Keys
data_dir (string, required)
Filesystem path that holds the raft store, snapshots, the node TLS cert
- key, and the WireGuard private key. Default
/var/lib/jaco. The directory and everything under it MUST be readable + writable by the user the daemon runs as (the package installer creates thejacosystem user). A missing directory is created on first boot.
listen_addr (string, required, host:port)
Cross-host gRPC listener — peers and remote CLI dial this address. The
default 0.0.0.0:7000 is not a literal bind on every interface:
the daemon resolves an unspecified host against
internal/daemon/netdetect, which picks a private-LAN IPv4
candidate (RFC 1918, CGNAT, link-local) and binds the listener to
exactly that face. A host whose only routable interface is public
fails fast with guidance — JACO never auto-exposes the control plane
to the public Internet. See cmd/jacod/main.go::resolveAdvertise for
the resolution code.
Pin an explicit host:port (e.g. 10.0.0.5:7000) to bypass detection
on multi-NIC hosts, on overlay-only clusters where the daemon should
listen on the overlay interface, or whenever you want bind == advertise
to be an exact value. A pinned value is honored verbatim.
cluster_addr (string, required, host:port)
Raft TCP transport. Same resolution semantics as listen_addr. MUST
differ from listen_addr. Default 0.0.0.0:7001.
unix_socket (string, required)
Path the daemon binds locally for CLI-to-daemon control. Mode 0660,
group jaco. The socket's filesystem permissions ARE the auth
boundary: any process whose user is in the jaco group can drive the
local daemon without presenting a bearer token. See
Auth and tokens. Default
/var/run/jaco/jaco.sock.
wg_port (int, required, 1–65535)
UDP port for the per-node WireGuard interface (wg-jaco). All peers
must agree; mismatches present as silent traffic loss. Default 51820.
acme_email (string, optional)
Cluster-wide default ACME contact address. Used by every
deployment whose jaco.yaml does not declare its own top-level
acme_email:; deployments that do set the field get their own ACME
account and own automation policy (see
Ingress → Per-stack ACME contact email
and jaco.yaml schema).
Empty here AND empty per-stack is permitted but recommended against — ACME providers may not deliver expiry warnings without an address. No default.
acme_ca (string, optional, https URL)
ACME directory URL the cert issuer targets. Empty (the default) means
Let's Encrypt production
(https://acme-v02.api.letsencrypt.org/directory). Pin
https://acme-staging-v02.api.letsencrypt.org/directory (or the
ACMEStagingCA constant) for a dev/test cluster.
acme_enabled (bool, optional, default true)
Cluster-wide ACME kill switch. Set to false to opt out entirely: the
daemon does not register the ACME issuer and the rendered Caddy config
carries no tls.automation block, which is operator-verifiable without
any outbound ACME call. Useful for clusters fronted by a separate cert
pipeline.
acme_skip_staging (bool, optional, default false)
Skip the stage-first dry run for new domains. By default, new domains
issue against Let's Encrypt staging before flipping to the production
URL — staging's much looser rate limits absorb DNS/firewall
misconfigurations cheaply. Already-non-prod acme_ca values skip
staging automatically regardless of this setting.
dns (object, optional)
Configures the per-bridge DNS responder's external-name forwarding
chain. The whole block is optional — when absent, the daemon
parses /etc/resolv.conf at startup and forwards external names
through every nameserver entry it finds (with 127.0.0.11 and
10.244.*.1 filtered to avoid forwarding loops). See
Networking → Forwarder.
-
dns.forwarders(list of strings, optional)Ordered upstream resolver chain used for every external name. Each entry is
host[:port]; the daemon appends:53when no port is given, brackets bare IPv6 literals. Empty list (or key absent) → fall back to/etc/resolv.conf. Validator rejects127.0.0.11and10.244.*.1because either as an upstream creates a forwarding loop (Docker's embedded resolver, or a JACO bridge gateway). -
dns.forwarder_timeout(Go duration, optional, default2s)Per-upstream query deadline. Two seconds is short enough that even a full chain of two failed upstreams completes well inside libc's 5-second nameserver timeout — downstream resolvers retry instead of timing out and silently breaking container outbound DNS (issue #165). Set higher only if a single legitimate upstream consistently exceeds 2 s.
Example:
dns: forwarders: - 1.1.1.1 - 9.9.9.9 forwarder_timeout: 3s
log_level (string, optional)
One of debug | info | warn | error. Default info. Logs go to the
systemd journal under SYSLOG_IDENTIFIER=jacod when the daemon
detects systemd, JSON-on-stderr when the journal socket is unreachable,
and human-readable text otherwise (see
Observability). The JACO_LOG env
variable overrides this at the process level.
ipam_pool (string, required, IPv4 /16 CIDR)
Cluster-wide IP pool the leader carves into /24s, one per
(deployment, network) pair. Default 10.244.0.0/16 gives 256
allocations before exhaustion. MUST be exactly a /16 — any other
prefix length is rejected.
node_status_interval (Go duration, optional)
How often each daemon samples its local cgroup v2 CPU + memory
utilisation and gossips a NodeStatusUpdate heartbeat through raft.
The leader-side rebalancer
(Scheduling)
folds those samples into a per-node EWMA and uses them to decide
whether to move a replica off a hot host. Default 30s; valid range
5s..5m. A value outside that window is rejected at parse time
with node_status_interval … must be between 5s and 5m — there is no
silent clamping. Set to the default by omitting the key entirely; an
explicit 0 also means "use the default".
The collector reads /sys/fs/cgroup + /proc/meminfo and is
Linux-only. On non-Linux dev hosts, or when cgroup v2 is missing /
unreadable, the heartbeat skips that tick and the leader's freshness
gate (3× this interval) drops the node from rebalance scoring. The
rebalancer is therefore safely dormant when no signal is available.
Validation rules (enforced at parse time)
data_dir,listen_addr,cluster_addr,unix_socket,ipam_poolare required.listen_addrandcluster_addrparse ashost:portand MUST differ.wg_portis in1..65535.log_levelis one ofdebug | info | warn | error.acme_ca, when set, MUST be anhttps://…URL.ipam_poolparses as a CIDR with a/16mask exactly.node_status_interval, when set, parses as a Go duration string and MUST fall in[5s, 5m]. Zero / omitted maps to the default30s.- Any unknown top-level key fails the parse with the offending field name in the error message.
dns.forwardersentries each parse ashost[:port]; literal IPs MUST NOT be127.0.0.11(Docker's embedded resolver) nor any10.244.*.1(JACO bridge gateway) — both cases would create a forwarding loop. Empty list / key absent is fine (host/etc/resolv.conffallback). Hostname entries are allowed.dns.forwarder_timeout, when set, MUST be>= 0. Zero / omitted maps to the default2s.
See internal/daemon/config/config.go::Validate for the canonical
rule set.
Reloading
There is no hot reload in v1. Change jacod.yaml and
sudo systemctl restart jacod (or your service manager equivalent).
A restart is safe: orphaned containers are reclaimed on next boot via
JACO's label-based reconcile, and raft replays cleanly from disk.