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:  2s

All 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 the jaco system 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 :53 when no port is given, brackets bare IPv6 literals. Empty list (or key absent) → fall back to /etc/resolv.conf. Validator rejects 127.0.0.11 and 10.244.*.1 because either as an upstream creates a forwarding loop (Docker's embedded resolver, or a JACO bridge gateway).

  • dns.forwarder_timeout (Go duration, optional, default 2s)

    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_pool are required.
  • listen_addr and cluster_addr parse as host:port and MUST differ.
  • wg_port is in 1..65535.
  • log_level is one of debug | info | warn | error.
  • acme_ca, when set, MUST be an https://… URL.
  • ipam_pool parses as a CIDR with a /16 mask exactly.
  • node_status_interval, when set, parses as a Go duration string and MUST fall in [5s, 5m]. Zero / omitted maps to the default 30s.
  • Any unknown top-level key fails the parse with the offending field name in the error message.
  • dns.forwarders entries each parse as host[:port]; literal IPs MUST NOT be 127.0.0.11 (Docker's embedded resolver) nor any 10.244.*.1 (JACO bridge gateway) — both cases would create a forwarding loop. Empty list / key absent is fine (host /etc/resolv.conf fallback). Hostname entries are allowed.
  • dns.forwarder_timeout, when set, MUST be >= 0. Zero / omitted maps to the default 2s.

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.

See also