Skip to content

// docs · containerisation · scheduled

Scheduled pattern

The simplest pattern. syscert runs once, exits, and an external timer brings it back. Mirrors the systemd-timer model and maps directly to a Kubernetes CronJob.

For most setups, the scheduled pattern is recommended. syscert runs without --interval, does one ensure cycle (issue if missing, renew if due, distribute), and exits. Something external re-runs it on a schedule: cron, a systemd timer on the host, or a Kubernetes CronJob.

It mirrors the host systemd-timer model: a one-shot binary on a schedule. Easy to inspect, safe to restart, and every run gives you a clear success or failure signal.

Docker Compose + cron

# compose.scheduled.yml
services:
  syscert:
    image: alpine:latest          # replace with your own image containing syscert
    user: "1000:1000"
    command:
      - syscert
      - --config
      - /etc/syscert/syscert.toml
      - --env-file
      - /run/secrets/syscert_secrets
    volumes:
      - ./syscert.toml:/etc/syscert/syscert.toml:ro
      - certs:/var/lib/syscert
    env_file:
      - .env
    restart: "no"                 # one-shot — the external timer re-runs it

  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - certs:/etc/nginx/tls:ro
    restart: unless-stopped

volumes:
  certs:
    driver: local

Full annotated file: examples/container/compose.scheduled.yml

Run it on a schedule from the host’s crontab:

# crontab -e — daily at 03:17
17 3 * * * docker compose -f /opt/myapp/compose.scheduled.yml run syscert && \
           docker exec myapp-nginx-1 nginx -s reload

Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: syscert
  namespace: default
spec:
  schedule: "17 3 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            runAsGroup: 1000
            fsGroup: 1000        # ensures the PVC is writable by uid 1000
          containers:
            - name: syscert
              image: your-registry/syscert:latest
              args: [syscert, --config, /etc/syscert/syscert.toml]
              env:
                - name: CLOUDFLARE_DNS_API_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: syscert-dns-creds
                      key: CLOUDFLARE_DNS_API_TOKEN
              volumeMounts:
                - name: config
                  mountPath: /etc/syscert
                  readOnly: true
                - name: cert-store
                  mountPath: /var/lib/syscert
          volumes:
            - name: config
              configMap:
                name: syscert-config
            - name: cert-store
              persistentVolumeClaim:
                claimName: syscert-cert-store

Full annotated file (with PVC definition): examples/container/k8s-cronjob.yaml

The app Deployment mounts the same PVC read-only:

volumes:
  - name: cert-store
    persistentVolumeClaim:
      claimName: syscert-cert-store
      readOnly: true

Volume ownership

In Compose, fix the volume owner before you use it:

docker run --rm -v <project>_certs:/vol alpine chown 1000:1000 /vol

In Kubernetes, securityContext.fsGroup sets the group owner of a mounted PVC for you, so there’s no manual step.

Secrets

In Compose, put credentials in a .env file (0600). In Kubernetes, keep them in a Secret and project them as environment variables, like the example above. Never bake secrets into the image.

Why one-shot?

A few things make one-shot nice. A failed run doesn’t spin and hammer the CA; the next scheduled run just retries, so you stay rate-limit safe. Job history hands you a success or failure record per run. And re-running the job is safe, because syscert is idempotent.

The one thing --interval buys you over this pattern is that the sidecar schedules itself without an external timer. If you’ve already got cron or a Kubernetes CronJob, this pattern is simpler.


Next: Sidecar pattern · Embedded pattern · Containerisation overview