Sign inSign up

onesystems/onekeeper

By onesystems

Updated 27 days ago

Hardened, non-root Docker image update controller (Compose & Swarm) with NC Talk/E-Mail alerts.

Image
Security
Integration & delivery
Monitoring & observability
0

785

onesystems/onekeeper repository overview

OneKeeper

Alpine-based, non-root container that watches your running Docker containers (or Swarm services) for newer images, optionally applies updates, and reports the result via Nextcloud Talk Bot and/or e-mail.

Source / full documentation: git.onesystems.ch/Docker/OneKeeper · Changelog: CHANGELOG.md · Security: see the Security section in the full README


Highlights

  • Update detection via Docker Engine API DistributionInspect (multi-arch aware, handles OCI indexes and manifest lists); legacy docker manifest inspect fallback available.
  • Private registries (Docker Hub, GHCR, Gitea/Forgejo, Harbor, …) — credentials from config.yaml registries[], idempotent login at start and per run, passed as X-Registry-Auth.
  • Compose stacks — auto docker compose pull && up -d --remove-orphans for containers with com.docker.compose.project.working_dir, and explicit projects[] list in config.yaml.
  • Docker Swarm — compares the manager’s spec-pinned digest, docker service update --with-registry-auth, waits for UpdateStatus=completed, optional replica-count verification.
  • Post-update validation — waits for running, health check healthy, and stable RestartCount on the same container ID.
  • Non-root, hardened — runs as user keeper (UID 1000) under supercronic; compatible with read_only: true, cap_drop: [ALL], no-new-privileges, and the tecnativa/docker-socket-proxy pattern.
  • Notifications — Nextcloud Talk (HMAC-signed) and multipart e-mail (msmtp), with independent NOTIFY_STARTUP / NOTIFY_COMPLETE / NOTIFY_ERROR toggles.
  • Rate-limit aware — fail-fast on toomanyrequests and per-registry short-circuit within one run.
  • Audit trail/logs/onekeeper_YYYY-MM-DD.log and /logs/last_run.json (with optional rotation).

Image tags

TagMeaning
1.0.0Fixed release (recommended for production).
1Latest 1.x — receives feature and fix updates.
latestNewest stable release.

Multi-arch: linux/amd64, linux/arm64.


Quick Start (hardened production Compose)

Uses tecnativa/docker-socket-proxy as a sidecar so the OneKeeper container does not receive a Docker socket bind — mounting /var/run/docker.sock gives any process inside root-equivalence on the host, which non-root inside the container does not mitigate.

mkdir -p ~/onekeeper/{config,logs}
cd ~/onekeeper

config/config.yaml (chmod 600 — contains credentials):

registries:
  # Only needed for private registries. Public Docker Hub images work anonymously.
  # - name: dockerhub
  #   url: https://index.docker.io/v1/
  #   username: "your-user"
  #   password: "your-token"
  # - name: gitea
  #   url: https://git.example.com
  #   username: "keeper-reader"
  #   password: "gitea-pat-with-read:package"

policies:
  exclude: []
  # Example:
  # exclude:
  #   - container: "traefik"
  #   - image: "vendor/frozen-app:1.0"

docker-compose.yml:

services:
  docker-socket-proxy:
    image: tecnativa/docker-socket-proxy:0.3.0
    container_name: onekeeper-socket-proxy
    restart: unless-stopped
    environment:
      TZ: "Europe/Zurich"
      INFO: 1
      CONTAINERS: 1
      IMAGES: 1
      NETWORKS: 1
      VOLUMES: 1
      POST: 1              # for AUTO_UPDATE=true (pull + recreate)
      AUTH: 1
      SERVICES: 1          # for SWARM_MODE=true (manager only)
      TASKS: 1
      NODES: 1
      SWARM: 1
      LOG_LEVEL: warning
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks: [ keeper-net ]
    read_only: true
    tmpfs: [ /run ]
    security_opt: [ "no-new-privileges:true" ]
    cap_drop: [ ALL ]
    cap_add: [ CHOWN, SETGID, SETUID ]
    pids_limit: 64
    mem_limit: 64m
    cpus: "0.25"
    logging: { driver: json-file, options: { max-size: "5m", max-file: "3" } }
    healthcheck:
      test: [ "CMD", "wget", "-qO-", "http://localhost:2375/_ping" ]
      interval: 30s
      timeout: 5s
      retries: 3

  onekeeper:
    image: onesystems/onekeeper:latest        # or pin a version tag
    container_name: onekeeper
    restart: unless-stopped
    depends_on:
      docker-socket-proxy:
        condition: service_healthy
    networks: [ keeper-net ]
    environment:
      TZ: "Europe/Zurich"
      LOG_ICONS: "off"
      DOCKER_HOST: "tcp://docker-socket-proxy:2375"
      KEEPER_NAME: "prod-docker-host"
      CRON_SCHEDULE: "0 3 * * *"
      AUTO_UPDATE: "false"                    # set to "true" once you've reviewed a dry run
      SWARM_MODE: "false"                     # "true" on a Swarm manager
      NOTIFY_STARTUP: "off"
      NOTIFY_COMPLETE: "off"
      NOTIFY_ERROR: "on"
      # --- Nextcloud Talk (optional) ---
      NC_TALK_NOTIFY: "off"
      NC_TALK_URL: "https://cloud.example.com"
      NC_TALK_BOT_CHANNEL: "abc123"
      NC_TALK_BOT_SECRET: "your_shared_hmac_secret"
      # --- E-mail (optional) ---
      EMAIL_NOTIFY: "off"
      EMAIL_TO: "[email protected]"
      EMAIL_FROM: "[email protected]"
      SMTP_SERVER: "smtp.example.com"
      SMTP_PORT: "587"
      SMTP_USER: "smtp-user"
      SMTP_PASS: "smtp-pass"
      SMTP_TLS: "on"
    volumes:
      - ./config:/config:ro
      - ./logs:/logs
    read_only: true
    tmpfs:
      - /tmp:size=32m,mode=1777
      - /home/keeper:uid=1000,gid=1000,mode=0700,size=16m
    security_opt: [ "no-new-privileges:true" ]
    cap_drop: [ ALL ]
    pids_limit: 256
    mem_limit: 256m
    cpus: "0.5"
    logging: { driver: json-file, options: { max-size: "10m", max-file: "5" } }

networks:
  keeper-net:
    driver: bridge

Start and run one dry cycle:

sudo chown -R 1000:1000 logs       # keeper writes as UID 1000
chmod 600 config/config.yaml

docker compose up -d
docker compose logs -f onekeeper

# Force a single cycle now (does NOT apply updates when AUTO_UPDATE=false):
docker compose exec onekeeper /scripts/onekeeper.sh --once

Once satisfied, flip AUTO_UPDATE: "true" and docker compose up -d.


Environment variables (essentials)

Full reference: README on Forgejo.

Core
VariableDescriptionDefault
CONFIG_PATHPath to config.yaml/config/config.yaml
LOG_DIRLog directory/logs
CRON_SCHEDULECron expression (overrides schedules.default in YAML)0 2 * * *
AUTO_UPDATEtrue = pull/apply updates; false = detect onlyfalse
SHOW_ACTIONSon = with AUTO_UPDATE=false, log the commands that would runoff
DETECTION_MODEdocker (Engine API via onekeeper-go) or digest (legacy shell compare)docker
SWARM_MODEtrue = Swarm services instead of local containers (manager only)false
KEEPER_NAMEFriendly host label in notificationshostname
TZIANA timezone (tzdata is included)UTC
LOG_ICONSoff = [LEVEL] tags (grep-friendly); on = emoji glyphsoff
LOG_JSONon = extra JSON lines on stdout / stderr (Go CLI)off
Rate limits / retries
VariableDescriptionDefault
RATE_LIMIT_FAIL_FASTSkip fallbacks when the registry answers toomanyrequestson
RATE_LIMIT_SHORT_CIRCUITAfter N hits on a registry in one run, skip further checks for iton
RATE_LIMIT_SHORT_CIRCUIT_THRESHOLDHits per registry before short-circuit3
MANIFEST_RETRY_COUNTRetries when docker manifest inspect fails3
MANIFEST_RETRY_DELAY_SECInitial delay between retries (seconds)2
MANIFEST_RETRY_BACKOFFon = exponential backoff between retriesoff
Swarm / validation
VariableDescriptionDefault
SWARM_ROLLOUT_WAIT_SECONDSMax wait for UpdateStatus=completed after docker service update300
SWARM_VERIFY_REPLICASon = verify running vs desired replicas after rollout (skipped for global mode)off
RUNNING_WAIT_SECONDSMax wait for State.Status=running after an update120
OBSERVATION_SECONDSHealth-check wait after update180
Docker engine (remote / TLS)
VariableDescriptionDefault
DOCKER_HOSTe.g. tcp://docker-engine:2376; unset = default context (usually socket)unset
DOCKER_TLS_VERIFY1 / true = verify TLS on TCPunset
DOCKER_CERT_PATHDirectory with ca.pem, cert.pem, key.pemunset
DOCKER_CONFIGDocker client config directory (used for registry logins)/home/keeper/.docker
Notifications
VariableDescriptionDefault
NOTIFY_STARTUP / NOTIFY_COMPLETE / NOTIFY_ERRORIndependent per-phase togglesoff / off / on
NC_TALK_NOTIFY / NC_TALK_URL / NC_TALK_BOT_CHANNEL / NC_TALK_BOT_SECRETNextcloud Talk Bot (HMAC-SHA256)
EMAIL_NOTIFY / EMAIL_TO / EMAIL_FROM / SMTP_SERVER / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_TLS / EMAIL_HTMLSMTP via msmtp, optionally multipart HTML

Any env variable set on the container overrides the value from config.yaml. Unset/empty env → YAML wins, if present. This applies both at container start and for manual onekeeper.sh --once runs.


Volumes & files

Path in containerPurposeRecommended host permission
/configconfig.yaml (registries, policies, mail, schedules, projects, …) — mount read-onlychmod 600 config.yaml; owner readable by UID 1000
/logsDaily log files + last_run.json audit outputchown 1000:1000 logs/
/home/keeperWritable HOME (msmtprc, supercronic crontab, optional .docker/config.json). Use tmpfs with uid=1000,gid=1000,mode=0700
/tmpSmall tmpfs required for temporary manifest error filestmpfs, mode=1777

The image does not declare VOLUME directives; Compose/docker run controls mounts explicitly.


Nextcloud Talk bot setup

Requires Nextcloud ≥ 27.1 with Talk ≥ 17.1 and occ access on the Nextcloud host. Bots can only be installed via the CLI.

  1. Generate a 40–128-character shared secret (e.g. openssl rand -hex 32).
  2. Install the bot on the Nextcloud host — --feature response is required for outgoing messages; the webhook URL is a placeholder as OneKeeper only sends:
    sudo -u www-data php occ talk:bot:install \
      "OneKeeper" "<shared-secret>" "https://onekeeper.invalid/webhook" \
      --feature response
    
  3. Find the bot ID and attach it to the target conversation:
    sudo -u www-data php occ talk:bot:list
    sudo -u www-data php occ talk:bot:setup <bot-id> <conversation-token>
    
    <conversation-token> is the segment after /call/ in the room URL.
  4. Configure OneKeeper (NC_TALK_BOT_CHANNEL = the same conversation token):
    environment:
      NC_TALK_NOTIFY: "on"
      NC_TALK_URL: "https://cloud.example.com"
      NC_TALK_BOT_CHANNEL: "<conversation-token>"
      NC_TALK_BOT_SECRET: "<shared-secret>"
    
  5. Verify: docker compose exec onekeeper /scripts/onekeeper.sh --once — the log should end with 📬 Talk notification sent. HTTP 401 usually means the response feature is missing; fix in place with occ talk:bot:state --feature response <bot-id>.

Security notes

  • PID 1 is supercronic running as user keeper (UID 1000). The entrypoint, healthcheck, cron jobs, and onekeeper.sh all run non-root.
  • Prefer the socket-proxy sidecar over binding /var/run/docker.sock. If you must bind the socket, set DOCKER_GID at build time to stat -c '%g' /var/run/docker.sock on the deploy host, and add the same group_add: [ "${DOCKER_GID}" ] in Compose. Recreate (docker compose up -d --force-recreate) after changing it.
  • config.yaml contains credentials — keep it chmod 600 and mounted read-only.
  • The image runs fine with read_only: true + tmpfs for /tmp and /home/keeper + cap_drop: [ALL] + no-new-privileges: true (see the Compose example above).

Full guidance: see the Security section in the full README.


Reporting & manual runs

  • Cron log: /logs/onekeeper_YYYY-MM-DD.log (one file per day).
  • Audit file: /logs/last_run.json (schema 1, counters + lists: detected, skipped, updated, errors). Optional rotation via LAST_RUN_JSON_KEEP / audit.last_run_keep.
  • Manual run (same code path as cron): docker compose exec onekeeper /scripts/onekeeper.sh --once.
  • Diagnostics: docker compose exec onekeeper /usr/local/bin/onekeeper-go -version (build info), -print-config (parsed YAML), -print-effective (merged env + YAML), -check-image <ref> [-json].

Tag summary

Content type

Image

Digest

sha256:2e0092f26

Size

50.4 MB

Last updated

27 days ago

docker pull onesystems/onekeeper