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.
Under the shipped systemd unit the socket is socket-activated:
jaco.socket (PID 1) creates and binds it in the host mount namespace
and passes the listening file descriptor to jacod, which serves the
inherited socket instead of binding the path itself. Keep this key in
sync with jaco.socket's ListenStream= — they must name the same
path. Without socket activation (e.g. running jacod by hand) the
daemon falls back to binding unix_socket directly. See
Troubleshooting for the issue #167
background.
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.