-
Notifications
You must be signed in to change notification settings - Fork 0
Production Deployment
This page covers running yacron2 in hardened Linux container and Kubernetes
environments (the published Docker image is Linux-only): the published image, the
security context it is built to satisfy, the Kubernetes Deployment manifest, the
FROM-the-published-image build pattern, and the few cases that require a writable
path. See Installation for the package/binary install methods and
HTTP Control API for the optional web interface. For native Windows
deployment with the yacron2-windows-amd64.exe / yacron2-windows-arm64.exe
binaries, see Running on Windows.
At runtime the daemon only reads its configuration and secrets and writes its output to stdout/stderr. It needs no writable working directory, no temp files, and no log files. It runs in the foreground, makes no exotic syscalls, and requires no special privileges. The published image runs under a restricted Kubernetes/container security context with no writable paths or elevated privileges required.
The single exception is the optional per-job user/group switching feature,
which calls os.initgroups/os.setgid/os.setuid (yacron2/job.py) and so
requires the daemon to run as root. It is unavailable in the non-root
published image. If you do not use that feature, drop root entirely. See
Commands and Environment for the feature itself.
Prebuilt, multi-architecture (linux/amd64, linux/arm64, linux/386,
linux/arm/v7, linux/ppc64le and linux/s390x) images are published to the
GitHub Container Registry on every release.
| Property | Value |
|---|---|
| Registry/image | ghcr.io/ptweezy/yacron2 |
| Tags | the release version (e.g. 1.0.4), plus latest
|
| Base |
python:3.14-slim (multi-stage; runtime stage copies a self-contained venv) |
| User |
65534:65534 (nobody), set via USER in the Dockerfile
|
| Entrypoint | yacron2 |
| Default command | -c /etc/yacron2.d |
| Config path |
/etc/yacron2.d (a file or a directory of *.yaml/*.yml files) |
| Env |
PYTHONUNBUFFERED=1, PYTHONDONTWRITEBYTECODE=1, PATH=/opt/venv/bin:$PATH
|
PYTHONUNBUFFERED flushes stdout/stderr immediately (where yacron2 logs) and
PYTHONDONTWRITEBYTECODE prevents .pyc writes; both matter under a read-only
root filesystem.
A second
python:3.14-slim-based Dockerfile underexample/docker/exists for demonstration; itpip installs yacron2 and is not non-root. For production use the published GHCR image (or base your own image on it, below), not the example.
Mount a crontab read-only and run:
docker run --rm \
-v "$PWD/yacron2tab.yaml:/etc/yacron2.d/yacron2tab.yaml:ro" \
ghcr.io/ptweezy/yacron2:latestFor production, pin a specific version instead of latest
(e.g. ghcr.io/ptweezy/yacron2:1.0.4).
If you would rather bake the configuration into an image, base it on the published image. The non-root user, entrypoint, and config path are inherited:
FROM ghcr.io/ptweezy/yacron2:latest
# The base image already runs as the non-root user 65534.
COPY yacron2tab.yaml /etc/yacron2.d/yacron2tab.yamlThe published image satisfies a fully restricted pod security context out of the box:
-
runAsNonRoot/ non-root UID: the daemon needs no privileges, so it runs as an unprivileged UID (65534). Only per-jobuser/groupswitching needs root, and that feature is not available here. -
RuntimeDefaultseccomp: yacron2 makes no exotic syscalls, so the default seccomp profile (or an equivalently strict custom one) works. -
readOnlyRootFilesystem: no runtime writes are required by the image (or apip/pipxinstall). Mount the crontab read-only. See Writable-path exceptions for the two features that need a small writable volume: a Unix-socket web listener (with the image) and the standalone binary's self-extraction temp directory (only if you deploy the binary instead of the image). -
fsGroup-mounted config/secrets: mount config and secret volumes with anfsGroup(e.g.65534) so the non-root process can read them. -
drop: [ALL]capabilities andallowPrivilegeEscalation: false: yacron2 needs no Linux capabilities and never escalates privileges.
A Deployment mounting the crontab from a ConfigMap, read-only, under a fully
restricted security context:
apiVersion: apps/v1
kind: Deployment
metadata:
name: yacron2
spec:
replicas: 1
selector:
matchLabels:
app: yacron2
template:
metadata:
labels:
app: yacron2
spec:
securityContext: # pod-level
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534 # lets the non-root process read mounted volumes
seccompProfile:
type: RuntimeDefault
containers:
- name: yacron2
image: ghcr.io/ptweezy/yacron2:latest
args: ["-c", "/etc/yacron2.d"]
securityContext: # container-level
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
volumeMounts:
- name: crontab
mountPath: /etc/yacron2.d
readOnly: true
volumes:
- name: crontab
configMap:
name: yacron2tabreplicas: 1 is the safe default: yacron2 holds the schedule in-process, so
two replicas with no coordination each run every job independently.
To run more than one replica without double-running jobs, enable
leader election with a cluster section.
Pick a backend by whether you already run a coordination store:
-
backend: kubernetes(recommended on Kubernetes). Acoordination.k8s.io/v1Leasegives a fenced, exactly-once election while the apiserver is reachable. No mTLS, no peer list, no odd-replica rule: a plainDeploymentwith any replica count works; you only grant theLeaseRBAC (get/create/update). Likewisebackend: etcdif you run etcd. See Operating the lease backends andexample/kubernetes/. -
backend: gossip(the default, no coordination store). A quorum-gated, mutual-TLS election that keeps no shared state, so:-
Use an odd replica count. 3 replicas tolerate one failure, 5 tolerate
two; even counts buy no extra fault tolerance, and
replicas: 2is worse than 1 (a majority of 2 is 2, so both must be up to run anything); yacron2 refuses to start withelectLeaderand a 2-node cluster, and warns on even sizes. Spread replicas across nodes/zones withtopologySpreadConstraints; correlated failures defeat quorum regardless of count. -
A minority partition stands down (runs nothing) to guarantee at most one
leader, and the view is only as fresh as the poll
interval, so this is best-effort, not fenced exactly-once. If a job must never be skipped or doubled, use a lease backend above or keepreplicas: 1. - Provision the per-pod certificates from your own PKI (e.g. cert-manager) and
give each pod a stable
nodeName; a StatefulSet's ordinal hostnames make both the cert SANs and the peer list straightforward.
-
Use an odd replica count. 3 replicas tolerate one failure, 5 tolerate
two; even counts buy no extra fault tolerance, and
See Clustering and Leader Election for the full
trust model, quorum math, per-job clusterPolicy, the lease backends, and a
runnable local example.
The gossip backend adds a second listener, the cluster.listen mTLS port
(0.0.0.0:8443 in the examples), that peers poll over
mutual TLS. It is
separate from the optional web API port, so plan the pod network for it:
-
Open the port for pod-to-pod traffic. Every node both serves
/peeroncluster.listenand connects out to each peer'scluster.listen, so the port must be reachable between the replicas. Expose it on the container (containerPort: 8443). -
Add a
NetworkPolicyin a default-deny cluster. If you run default-deny ingress, allow thecluster.listenport from the pods carrying the sameapplabel (the peers), so the mTLS gossip is not silently dropped; a blocked port shows up asunreachablepeers and a lost quorum, not a clear error. -
Use a headless
Servicefor peer DNS. Pair a gossip StatefulSet with a headlessService(clusterIP: None) so each pod is addressable at a stable per-ordinal DNS name (yacron2-0.<svc>.<ns>.svc,yacron2-1.<svc>…). Those names are what you list undercluster.peersand what the certificate SANs must match. A lease backend needs none of this: it has no peer listener.
yacron2 serves a GET /version on the web API that returns 200
once the daemon is up, which makes a cheap liveness probe. Enable an
http:// web listener (for example web.listen: ["http://0.0.0.0:8080"])
and point the probe at it:
ports:
- name: web
containerPort: 8080
livenessProbe:
httpGet:
path: /version
port: web
initialDelaySeconds: 5
periodSeconds: 10If you protect the web API with a web.authToken, only the dashboard page (/)
stays unauthenticated, so /version then returns 401 without the bearer
token; either add the Authorization header to the probe (httpGet.httpHeaders)
or point it at /.
For clustered deployments, do not turn quorum loss into a pod restart: a
livenessProbe that fails on lost quorum would restart exactly the nodes you
need to reform it. Keep the probe scoped to daemon liveness (/version) and
alert on cluster health separately. quorate on GET /cluster is the field to
alert on (a node that cannot see a majority, or, on a lease backend, cannot
reach the store, stands its Leader jobs down); see the
Monitoring and alerting
table for the full signal set.
This applies to the gossip backend only (the lease backends use no per-node
mTLS certs). The day-2 story is mostly hands-off, because yacron2 reloads its
mTLS contexts in place: on each config-reload pass it compares the on-disk
ca/cert/key against what it loaded and, if any changed, rebuilds the TLS
contexts (dry-running the new material first so a half-written file is retried,
not fatal) within ~1 minute, with no restart. So the in-place renewals
cert-manager, Vault, and mounted-secret refreshes perform (same paths, new
bytes) are picked up automatically. The full mechanism is on the Clustering page
under Certificate rotation;
the operational cases:
-
The cluster
cais the membership allowlist. yacron2 trusts any cert the configuredcasigns to assert a node identity and gossip state, so thecamust be a dedicated, single-purpose CA issued only to yacron2 nodes, not a shared service-mesh or organisation-wide CA. Any cert the CA admits could otherwise fabricate the/peerpayload (fake agreement, trip the conflict gate, or suppress an@rebootjob). Provision leaf certs from that dedicated PKI (a private cert-manager issuer, an internal CA); yacron2 only consumes them. -
Rotating a single node's cert. A leaf renewed by your PKI needs no coordination as long as it still chains to the same
ca: write the newcert/keyin place, the node reloads within ~1 minute, and its peers keep trusting it. Issue the replacement with a comfortable overlap (well before the old cert expires) so a slow refresh never leaves the node serving an expired cert. -
Rolling the cluster CA. Changing the CA itself needs care, because a node only trusts peers whose certs chain to the CA bundle it currently holds. Roll it so trust always overlaps:
- Distribute a bundle file containing both the old and new CA as the
tls.caon every node first (an additional trust anchor). Each node reloads within ~1 minute and now trusts either CA. - Confirm every node still shows its peers
agreedonGET /cluster(nountrusted). - Re-issue each node's leaf cert from the new CA, one node at a time,
watching
GET /clusterafter each until the rotated node and its peers return toagreed. - Once every node presents a new-CA cert, narrow the bundle back to the new CA alone and distribute it everywhere.
Never cut the CA over in a single step: if some nodes trust only the new CA while others still present old-CA certs, they reject each other as
untrustedand the cluster loses quorum until trust overlaps again. - Distribute a bundle file containing both the old and new CA as the
-
Recovering from an
untrustedcascade. If peers start showinguntrustedonGET /cluster(or thepeer … is untrustedWARNINGin the logs) after a rotation, certs and CA trust have diverged (typically a CA roll that skipped the overlap step, or a node whose refresh lagged). Recovery needs no restart: restore the trust overlap (push a CA bundle that includes whichever CA the still-untrustedpeers were issued from), or finish rolling the lagging nodes onto the new CA. Each node reloads within ~1 minute and peers return toagreedautomatically. BecauseLeaderjobs stand down (fail closed) while quorum is lost, the cascade skips firings rather than double-running them: no split-brain during recovery, only missed firings until trust reconverges.
Before scaling past replicas: 1, walk this per your backend. Each row links to
the authoritative section on the Clustering page.
| Concern |
gossip (default) |
Lease (kubernetes / etcd) |
|---|---|---|
| Trust / auth (gossip mTLS / lease store) | per-node certs from a dedicated cluster CA; open the cluster.listen mTLS port |
apiserver Lease RBAC (get/create/update) or reachable etcd endpoints; no mTLS |
| Node identity (gossip / lease) | stable, unique nodeName (distinct cert SANs / stable hostnames) |
stable name too, but the lease is the fence, so a duplicate name cannot double-lead |
| Listener / port (attestation) |
cluster.listen reachable pod-to-pod; headless Service for peer DNS |
none (no peer listener) |
| Replica count (sizing) |
odd (3/5/7); yacron2 rejects an electLeader 2-node cluster and warns on even sizes |
any count; the store fences, not a quorum |
| Rotation (cert rotation) | in-place cert/CA reload (above); roll the CA with an overlap | n/a (no per-node mTLS certs) |
| Preflight |
yacron2 -c <path> --validate-config (catches lease ordering, CA/cert paths, an electLeader 2-node cluster) |
yacron2 -c <path> --validate-config (same) |
yacron2 -c <path> --validate-config parses the config and exits non-zero on any
error, so it belongs in CI before a cluster deploy: it enforces the lease timing
ordering (leaseDurationSeconds > renewDeadlineSeconds, retryPeriodSeconds < renewDeadlineSeconds, and the like), the RFC1123 leaseName/leaseNamespace
shapes, the etcd endpoint/TLS rules, and the electLeader 2-node rejection. See
Command-Line Reference.
A read-only root filesystem is sufficient for the published image in the normal case. Two features need a small writable mount.
If you enable the optional HTTP Control API on a Unix socket
(web.listen: [unix:///path/yacron2.sock]), yacron2 binds a UnixSite and
creates the socket file at that path, a write. Point the socket at a small
writable volume (a Kubernetes emptyDir) rather than the root filesystem:
volumeMounts:
- name: crontab
mountPath: /etc/yacron2.d
readOnly: true
- name: run
mountPath: /run/yacron2
volumes:
- name: crontab
configMap:
name: yacron2tab
- name: run
emptyDir: {}with web.listen: [unix:///run/yacron2/yacron2.sock] in your config. (TCP
listeners such as http://0.0.0.0:8080 need no writable path.) The optional
web.socketMode config key, if set, chmods the socket after bind.
unix://web listeners are not supported on Windows (the Proactor event loop lackscreate_unix_server); such a listen URL is skipped with the warningIgnoring web listen url <url>: unix-socket listeners are not supported on this platform. Use anhttp://listener instead.web.socketModeonly applies to unix sockets, so it is irrelevant on Windows. See Running on Windows.
This applies only if you deploy the standalone binary (from the GitHub
releases) instead of the published image. The binary is a self-extracting
executable: on each start it unpacks its embedded Python runtime into a temporary
directory and loads shared libraries from there, so it needs a temp directory
that is both writable and executable. On an ordinary system /tmp already
satisfies this and no setup is needed.
Under a read-only root filesystem, /tmp is read-only too and the binary aborts
at startup with Could not create temporary directory or
Error loading shared library …: Operation not permitted. Provide a writable,
executable temp mount:
-
Docker:
--tmpfs /tmp:rw,exec,nosuid,nodev,size=64m. Theexecis required: Docker's--tmpfsdefaults tonoexec, but the binary must execute the libraries it unpacks.docker run --rm --read-only \ --tmpfs /tmp:rw,exec,nosuid,nodev,size=64m \ -v "$PWD/yacron2tab.yaml:/etc/yacron2.d/yacron2tab.yaml:ro" \ your-image-with-the-binary -c /etc/yacron2.d -
Kubernetes: mount an
emptyDirat/tmp(writable and executable by default; usemedium: Memoryfor a tmpfs). -
Either: point the binary at another writable, executable directory with
TMPDIR=/path.
This requirement is unique to the standalone binary. The published image and
pip/pipx installs run yacron2 as a normal Python package with the interpreter
on disk; they never self-extract and need no writable temp directory. See
Installation for the binary download.
-
Logging: yacron2 logs to stdout/stderr; collect logs at the platform
level (
kubectl logs, the container runtime's log driver). Adjust verbosity with-l/--log-level(defaultINFO) or alogging:config section. -
Shutdown: on POSIX, yacron2 installs handlers for
SIGINTandSIGTERMand shuts down gracefully, so the default pod termination path works without extra configuration. On Windows (where the Proactor loop has noadd_signal_handler) it instead handlesSIGINT/SIGBREAKviasignal.signalplus a heartbeat timer, so pressing Ctrl-C or Ctrl-Break stops it; either way it finishes the currently-running jobs first. See Running on Windows. -
Validate before deploy:
yacron2 -c <path> --validate-configparses the config and exits, useful as a CI/pre-deploy gate. See Command-Line Reference. -
Config not found: the default config path is platform-specific:
/etc/yacron2.don POSIX,%APPDATA%\yacron2on Windows (falling back to the user profile~ifAPPDATAis unset). When-cis left at whichever is the platform default and that path does not exist, yacron2 prints an error and exits non-zero. In the container, ensure the config volume is mounted at/etc/yacron2.d.
This wiki documents yacron2. See the README and the changelog.
yacron2 is a fork of gjcarneiro/yacron.
- Getting Started
- Configuration
- Job Behavior
- Integrations
- Reference and Development