Hardened, non-root Docker image update controller (Compose & Swarm) with NC Talk/E-Mail alerts.
785
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
DistributionInspect (multi-arch aware, handles OCI indexes and manifest lists); legacy docker manifest inspect fallback available.config.yaml registries[], idempotent login at start and per run, passed as X-Registry-Auth.docker compose pull && up -d --remove-orphans for containers with com.docker.compose.project.working_dir, and explicit projects[] list in config.yaml.docker service update --with-registry-auth, waits for UpdateStatus=completed, optional replica-count verification.running, health check healthy, and stable RestartCount on the same container ID.keeper (UID 1000) under supercronic; compatible with read_only: true, cap_drop: [ALL], no-new-privileges, and the tecnativa/docker-socket-proxy pattern.msmtp), with independent NOTIFY_STARTUP / NOTIFY_COMPLETE / NOTIFY_ERROR toggles.toomanyrequests and per-registry short-circuit within one run./logs/onekeeper_YYYY-MM-DD.log and /logs/last_run.json (with optional rotation).| Tag | Meaning |
|---|---|
1.0.0 | Fixed release (recommended for production). |
1 | Latest 1.x — receives feature and fix updates. |
latest | Newest stable release. |
Multi-arch: linux/amd64, linux/arm64.
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.
Full reference: README on Forgejo.
| Variable | Description | Default |
|---|---|---|
CONFIG_PATH | Path to config.yaml | /config/config.yaml |
LOG_DIR | Log directory | /logs |
CRON_SCHEDULE | Cron expression (overrides schedules.default in YAML) | 0 2 * * * |
AUTO_UPDATE | true = pull/apply updates; false = detect only | false |
SHOW_ACTIONS | on = with AUTO_UPDATE=false, log the commands that would run | off |
DETECTION_MODE | docker (Engine API via onekeeper-go) or digest (legacy shell compare) | docker |
SWARM_MODE | true = Swarm services instead of local containers (manager only) | false |
KEEPER_NAME | Friendly host label in notifications | hostname |
TZ | IANA timezone (tzdata is included) | UTC |
LOG_ICONS | off = [LEVEL] tags (grep-friendly); on = emoji glyphs | off |
LOG_JSON | on = extra JSON lines on stdout / stderr (Go CLI) | off |
| Variable | Description | Default |
|---|---|---|
RATE_LIMIT_FAIL_FAST | Skip fallbacks when the registry answers toomanyrequests | on |
RATE_LIMIT_SHORT_CIRCUIT | After N hits on a registry in one run, skip further checks for it | on |
RATE_LIMIT_SHORT_CIRCUIT_THRESHOLD | Hits per registry before short-circuit | 3 |
MANIFEST_RETRY_COUNT | Retries when docker manifest inspect fails | 3 |
MANIFEST_RETRY_DELAY_SEC | Initial delay between retries (seconds) | 2 |
MANIFEST_RETRY_BACKOFF | on = exponential backoff between retries | off |
| Variable | Description | Default |
|---|---|---|
SWARM_ROLLOUT_WAIT_SECONDS | Max wait for UpdateStatus=completed after docker service update | 300 |
SWARM_VERIFY_REPLICAS | on = verify running vs desired replicas after rollout (skipped for global mode) | off |
RUNNING_WAIT_SECONDS | Max wait for State.Status=running after an update | 120 |
OBSERVATION_SECONDS | Health-check wait after update | 180 |
| Variable | Description | Default |
|---|---|---|
DOCKER_HOST | e.g. tcp://docker-engine:2376; unset = default context (usually socket) | unset |
DOCKER_TLS_VERIFY | 1 / true = verify TLS on TCP | unset |
DOCKER_CERT_PATH | Directory with ca.pem, cert.pem, key.pem | unset |
DOCKER_CONFIG | Docker client config directory (used for registry logins) | /home/keeper/.docker |
| Variable | Description | Default |
|---|---|---|
NOTIFY_STARTUP / NOTIFY_COMPLETE / NOTIFY_ERROR | Independent per-phase toggles | off / off / on |
NC_TALK_NOTIFY / NC_TALK_URL / NC_TALK_BOT_CHANNEL / NC_TALK_BOT_SECRET | Nextcloud Talk Bot (HMAC-SHA256) | — |
EMAIL_NOTIFY / EMAIL_TO / EMAIL_FROM / SMTP_SERVER / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_TLS / EMAIL_HTML | SMTP 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 manualonekeeper.sh --onceruns.
| Path in container | Purpose | Recommended host permission |
|---|---|---|
/config | config.yaml (registries, policies, mail, schedules, projects, …) — mount read-only | chmod 600 config.yaml; owner readable by UID 1000 |
/logs | Daily log files + last_run.json audit output | chown 1000:1000 logs/ |
/home/keeper | Writable HOME (msmtprc, supercronic crontab, optional .docker/config.json). Use tmpfs with uid=1000,gid=1000,mode=0700 | — |
/tmp | Small tmpfs required for temporary manifest error files | tmpfs, mode=1777 |
The image does not declare VOLUME directives; Compose/docker run controls mounts explicitly.
Requires Nextcloud ≥ 27.1 with Talk ≥ 17.1 and occ access on the Nextcloud host. Bots can only be installed via the CLI.
openssl rand -hex 32).--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
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.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>"
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>.supercronic running as user keeper (UID 1000). The entrypoint, healthcheck, cron jobs, and onekeeper.sh all run non-root./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.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.
/logs/onekeeper_YYYY-MM-DD.log (one file per day)./logs/last_run.json (schema 1, counters + lists: detected, skipped, updated, errors). Optional rotation via LAST_RUN_JSON_KEEP / audit.last_run_keep.docker compose exec onekeeper /scripts/onekeeper.sh --once.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].git.onesystems.ch/Docker/OneKeeperContent type
Image
Digest
sha256:2e0092f26…
Size
50.4 MB
Last updated
27 days ago
docker pull onesystems/onekeeper