Sign inSign up

ludix0/docker-update

By ludix0

‱Updated 2 months ago

Image
Developer tools
0

266

ludix0/docker-update repository overview

⁠docker-update

👉 Jump to 🇬🇧 English⁠ · Aller au đŸ‡«đŸ‡· Français⁠


⁠🇬🇧 English

A self-hosted, containerized scheduler that safely tests, pulls, rebuilds and restarts your other Docker Compose projects — with a built-in scheduler, no host cron, no direct Docker socket exposure, and multi-channel notifications (email, Discord, ntfy, Telegram, Gotify, Pushover).

Point it at a folder full of ~/projects/<name>/docker-compose.yml projects and it takes care of the rest: run each project's tests, compare image digests before/after pull, rebuild local Dockerfiles, restart only what changed, verify containers are still up 10 seconds later, clean up unused images, and notify you — only when something actually happened.

⁠Features

  • Scans a projects folder (find -L, symlink-aware) for any docker-compose.yml, at a configurable depth.
  • Runs tests first: test.sh or make test (if a Makefile defines a test: target). A failing test cancels the update for that project only.
  • Digest-based change detection: compares each image's resolved digest before and after docker compose pull — no restart if nothing changed.
  • Rebuilds local images: if a project has its own Dockerfile, it's rebuilt every run.
  • Restarts only on change, then waits and re-checks container state to catch a bad restart.
  • Prunes unused images after each run.
  • Timestamped logs per run + a latest.log symlink, with automatic retention/cleanup.
  • Multi-channel notifications, any combination at once: email, Discord, ntfy, Telegram, Gotify, Pushover. Silent when nothing changed and nothing failed (no spam).
  • Built-in scheduler: a daily trigger time (UPDATE_TIME), no dependency on the host's cron.
  • Runs as a non-root user (configurable PUID/PGID), never touches /var/run/docker.sock directly — it goes through a filtering Tecnativa docker-socket-proxy⁠.
  • Fully configurable via environment variables / .env — nothing is hardcoded.

⁠How it works

┌───────────────────────────┐    tcp://
:2375    ┌───────────────────────────┐
│       docker-update       │ ──────────────────â–ș │   docker-socket-proxy    │
│ (scheduler + update logic)│                      │ (filters the Docker API) │
│        non-root user      │                      │                          │
└───────────────────────────┘                      └────────────┬─────────────┘
                                                                  │ ro bind-mount
                                                                  ▌
                                                       /var/run/docker.sock
                                                        (host Docker daemon)

docker-update never runs its own Docker daemon. It sends docker compose commands over the network to docker-socket-proxy, which is the only container allowed to touch the real socket — and only for the operations it explicitly allows (see Security⁠).

⚠ Important — this is "Docker-outside-of-Docker": the commands run inside docker-update, but they're executed by the host's Docker daemon. When your project's docker-compose.yml resolves a relative bind mount or build context, it's the host that interprets that path — not the docker-update container. That's why PROJETS_DIR must be mounted at the exact same absolute path on the host and inside the container (docker-compose.yml does this: ${PROJETS_DIR}:${PROJETS_DIR}). If the paths differ, your other projects' volumes will resolve incorrectly.

⁠Expected project layout

/home/you/projects/
├── nextcloud/
│   ├── docker-compose.yml
│   └── test.sh              # optional — exit 0 = pass, anything else = cancel update
├── vaultwarden/
│   ├── docker-compose.yml
│   └── Dockerfile           # optional — rebuilt on every run if present
└── some-app/
    ├── docker-compose.yml
    └── Makefile              # optional — needs a "test:" target

⁠Quick start

git clone <this-repo>
cd maj-docker
cp .env.example .env
# edit .env: at minimum set PROJETS_DIR and one notification channel
docker compose build
docker compose up -d
docker compose logs -f docker-update

To trigger a run immediately instead of waiting for UPDATE_TIME, either set RUN_ON_STARTUP=true in .env, or run it manually once:

docker compose exec --user appuser docker-update /app/update-projets.sh

⁠Configuration reference

All variables live in .env (copy .env.example first). Anything left empty falls back to the default shown, or disables the related feature.

⁠Container user & timezone
VariableDefaultDescription
PUID1000UID the container runs as (never root). Get yours with id -u.
PGID1000GID the container runs as. Get yours with id -g.
TZEurope/ParisTimezone used for logs and the scheduler.
⁠Projects
VariableDefaultDescription
PROJETS_DIR/projetsAbsolute path to your projects folder — must match on host and container (see warning above).
FIND_MAXDEPTH3How deep to search for docker-compose.yml files under PROJETS_DIR.
⁠Logs
VariableDefaultDescription
LOGS_DIR./logsHost path where logs are persisted (compose bind mount only).
LOG_RETENTION_DAYS30Logs older than this are deleted automatically.
⁠Scheduler (no host cron involved)
VariableDefaultDescription
UPDATE_TIME04:00Daily trigger time, HH:MM, in the TZ timezone.
RUN_ON_STARTUPfalseAlso run once immediately when the container starts.
⁠Update behavior
VariableDefaultDescription
STABILITY_WAIT_SECONDS10Wait time after a restart before checking containers are still up.
IMAGE_PRUNE_ENABLEDtrueRun docker image prune -af after each pass.
⁠Notifications — channel selection
VariableDefaultDescription
NOTIFY_CHANNELSemailComma-separated list, any combination of email,discord,ntfy,telegram,gotify,pushover.
NOTIFY_PREFIX[docker-update]Prefix added to every notification's subject/title.

A notification only fires when something failed or something was actually updated — an all-skipped run stays silent.

⁠Notification channels

Enable any combination via NOTIFY_CHANNELS. A channel listed there but left unconfigured (empty URL/token) is skipped with a log line — it never breaks the run.

📧 Email (msmtp)
VariableDescription
EMAIL_TORecipient address. Empty = channel disabled.
SMTP_HOST / SMTP_PORTYour SMTP server, e.g. smtp.gmail.com / 587.
SMTP_USER / SMTP_PASSWORDSMTP credentials (use an app password with Gmail, not your account password).
SMTP_FROMFrom address (defaults to SMTP_USER).
SMTP_TLS / SMTP_STARTTLSon/off.

Credentials are injected at runtime only — ~/.msmtprc is generated by the entrypoint from these variables, never baked into the image.

💬 Discord
VariableDescription
DISCORD_WEBHOOK_URLEmpty = channel disabled.

Get one: Discord channel → Settings → Integrations → Webhooks → New Webhook → copy the URL.

🔔 ntfy (self-hosted or ntfy.sh)
VariableDefaultDescription
NTFY_URLhttps://ntfy.shYour ntfy server, self-hosted or public.
NTFY_TOPIC—Topic name. Empty = channel disabled.
NTFY_TOKEN—Access token, only if your server requires auth.
NTFY_PRIORITYdefaultmin/low/default/high/urgent.

On the public ntfy.sh, anyone who knows your topic name can read your messages — pick something hard to guess, or self-host.

✈ Telegram
VariableDescription
TELEGRAM_BOT_TOKENEmpty = channel disabled.
TELEGRAM_CHAT_IDThe chat/group that should receive messages.

Setup: message @BotFather on Telegram, /newbot → gives you the token. Start a chat with your new bot (or add it to a group), then fetch the chat ID from https://api.telegram.org/bot<TOKEN>/getUpdates after sending it a message — look for "chat":{"id":...} in the response.

📡 Gotify (self-hosted)
VariableDefaultDescription
GOTIFY_URL—e.g. https://gotify.example.com. Empty = channel disabled.
GOTIFY_TOKEN—Application token, created in the Gotify web UI.
GOTIFY_PRIORITY50–10.
đŸ“Č Pushover
VariableDefaultDescription
PUSHOVER_TOKEN—Application token from pushover.net. Empty = channel disabled.
PUSHOVER_USER_KEY—Your user (or group) key.
PUSHOVER_PRIORITY0-2 to 2.

⁠Security

  • Never runs as root — a dedicated non-root user with configurable PUID/PGID, switched to via su-exec after a short root-only setup step (permission fixing, .msmtprc generation).
  • Base images pinned by SHA256 digest, never a tag — immutable and reproducible builds.
  • No secrets in the image: no ENV/ARG credentials in the Dockerfile. SMTP and every notification token/URL are injected at container runtime from .env, and .msmtprc is generated fresh on each start.
  • No direct docker.sock mount in the update container. The only container touching the real socket is docker-socket-proxy, and it's configured with a minimal allow-list (containers, images, networks, volumes, build, start/stop/restart) — Swarm, secrets, exec, auth and everything else is explicitly denied.
  • The proxy is never published on a host port — only reachable from the internal docker-update-net network.

⁠Troubleshooting

SymptomLikely cause
"🚹 No project found" notification on every runPROJETS_DIR isn't mounted, or the path doesn't actually contain any docker-compose.yml within FIND_MAXDEPTH.
Other projects' volumes break after an update runPROJETS_DIR isn't mounted at the same absolute path on host and container — see the DooD warning above.
"Permission denied" on the logs folderPUID/PGID don't match the owner of LOGS_DIR on the host.
A notification channel never arrivesCheck docker compose logs docker-update — a misconfigured channel logs a clear "ignored" line rather than failing silently.
Socket proxy won't startSome hosts need privileged: true on docker-socket-proxy for AppArmor/SELinux reasons (already set) — try removing it if your host doesn't need it.

Full logs live at ${LOGS_DIR}/update-docker-<timestamp>.log on the host, with ${LOGS_DIR}/latest.log always pointing at the most recent run.





â đŸ‡«đŸ‡· Français

Un ordonnanceur conteneurisĂ© et auto-hĂ©bergĂ© qui teste, met Ă  jour, reconstruit et redĂ©marre en toute sĂ©curitĂ© vos autres projets Docker Compose — avec un ordonnanceur interne (sans cron de l'hĂŽte), aucune exposition directe du socket Docker, et des notifications multi-canaux (email, Discord, ntfy, Telegram, Gotify, Pushover).

Pointez-le vers un dossier contenant des projets ~/projets/<nom>/docker-compose.yml et il s'occupe du reste : lancer les tests de chaque projet, comparer les digests d'images avant/aprĂšs pull, reconstruire les Dockerfile locaux, ne redĂ©marrer que ce qui a changĂ©, vĂ©rifier 10 secondes plus tard que les conteneurs tournent toujours, nettoyer les images inutilisĂ©es, et vous notifier — uniquement quand quelque chose s'est rĂ©ellement passĂ©.

⁠Fonctionnalités

  • Parcourt un dossier de projets (find -L, suit les liens symboliques) Ă  la recherche de docker-compose.yml, Ă  une profondeur configurable.
  • Lance les tests en premier : test.sh ou make test (si un Makefile dĂ©finit une cible test:). Un test qui Ă©choue annule la mise Ă  jour pour ce projet uniquement.
  • DĂ©tection de changement par digest : compare le digest rĂ©solu de chaque image avant/aprĂšs docker compose pull — aucun redĂ©marrage si rien n'a changĂ©.
  • Reconstruit les images locales : si un projet a son propre Dockerfile, il est reconstruit Ă  chaque passage.
  • RedĂ©marre uniquement en cas de changement, puis attend et revĂ©rifie l'Ă©tat des conteneurs pour dĂ©tecter un mauvais redĂ©marrage.
  • Nettoie les images inutilisĂ©es aprĂšs chaque exĂ©cution.
  • Logs horodatĂ©s Ă  chaque exĂ©cution + un lien latest.log, avec purge automatique selon une durĂ©e de rĂ©tention.
  • Notifications multi-canaux, combinables librement : email, Discord, ntfy, Telegram, Gotify, Pushover. Silencieux quand rien n'a changĂ© et qu'aucune erreur n'est survenue (pas de spam).
  • Ordonnanceur interne : une heure de dĂ©clenchement quotidienne (UPDATE_TIME), sans dĂ©pendre du cron de l'hĂŽte.
  • Toujours en utilisateur non-root (PUID/PGID configurables), ne touche jamais /var/run/docker.sock directement — il passe par un proxy filtrant Tecnativa docker-socket-proxy⁠.
  • EntiĂšrement configurable via des variables d'environnement / .env — rien n'est codĂ© en dur.

⁠Fonctionnement

┌───────────────────────────┐   tcp://
:2375    ┌───────────────────────────┐
│       docker-update        │ ─────────────────â–ș │   docker-socket-proxy    │
│ (ordonnanceur + logique)  │                     │ (filtre l'API Docker)    │
│      utilisateur non-root │                     │                          │
└───────────────────────────┘                     └────────────┬─────────────┘
                                                                  │ montage ro
                                                                  ▌
                                                       /var/run/docker.sock
                                                    (démon Docker de l'hÎte)

docker-update ne fait jamais tourner son propre dĂ©mon Docker. Il envoie ses commandes docker compose par le rĂ©seau Ă  docker-socket-proxy, le seul conteneur autorisĂ© Ă  toucher le vrai socket — et uniquement pour les opĂ©rations qu'il autorise explicitement (voir SĂ©curité⁠).

⚠ Important — c'est du "Docker-outside-of-Docker" : les commandes s'exĂ©cutent depuis docker-update, mais c'est le dĂ©mon Docker de l'hĂŽte qui les exĂ©cute rĂ©ellement. Quand le docker-compose.yml d'un de vos projets rĂ©sout un montage relatif ou un contexte de build, c'est l'hĂŽte qui interprĂšte ce chemin — pas le conteneur docker-update. C'est pourquoi PROJETS_DIR doit ĂȘtre montĂ© au chemin absolu strictement identique cĂŽtĂ© hĂŽte et cĂŽtĂ© conteneur (le docker-compose.yml fait ${PROJETS_DIR}:${PROJETS_DIR}). Si les chemins diffĂšrent, les volumes de vos autres projets seront mal rĂ©solus.

⁠Arborescence de projets attendue

/home/vous/projets/
├── nextcloud/
│   ├── docker-compose.yml
│   └── test.sh              # optionnel — exit 0 = OK, sinon = annule la mise à jour
├── vaultwarden/
│   ├── docker-compose.yml
│   └── Dockerfile           # optionnel — reconstruit Ă  chaque passage si prĂ©sent
└── une-appli/
    ├── docker-compose.yml
    └── Makefile              # optionnel — nĂ©cessite une cible "test:"

⁠Démarrage rapide

git clone <ce-dépÎt>
cd maj-docker
cp .env.example .env
# éditer .env : au minimum PROJETS_DIR et un canal de notification
docker compose build
docker compose up -d
docker compose logs -f docker-update

Pour déclencher une exécution immédiate plutÎt que d'attendre UPDATE_TIME, soit mettez RUN_ON_STARTUP=true dans .env, soit lancez-la manuellement :

docker compose exec --user appuser docker-update /app/update-projets.sh

⁠Référence des variables

Toutes les variables vivent dans .env (copier .env.example d'abord). Une variable laissée vide reprend la valeur par défaut indiquée, ou désactive la fonctionnalité concernée.

⁠Utilisateur du conteneur et fuseau horaire
VariableDéfautDescription
PUID1000UID sous lequel tourne le conteneur (jamais root). À rĂ©cupĂ©rer avec id -u.
PGID1000GID sous lequel tourne le conteneur. À rĂ©cupĂ©rer avec id -g.
TZEurope/ParisFuseau horaire utilisé pour les logs et l'ordonnanceur.
⁠Projets
VariableDéfautDescription
PROJETS_DIR/projetsChemin absolu vers votre dossier de projets — doit ĂȘtre identique cĂŽtĂ© hĂŽte et conteneur (voir avertissement ci-dessus).
FIND_MAXDEPTH3Profondeur de recherche des docker-compose.yml sous PROJETS_DIR.
⁠Logs
VariableDéfautDescription
LOGS_DIR./logsChemin hĂŽte oĂč les logs sont conservĂ©s (montage compose uniquement).
LOG_RETENTION_DAYS30Les logs plus anciens sont supprimés automatiquement.
⁠Ordonnanceur (aucun cron de l'hÎte impliqué)
VariableDéfautDescription
UPDATE_TIME04:00Heure de déclenchement quotidienne, HH:MM, dans le fuseau TZ.
RUN_ON_STARTUPfalseLance aussi une exécution immédiate au démarrage du conteneur.
⁠Comportement de la mise à jour
VariableDéfautDescription
STABILITY_WAIT_SECONDS10Temps d'attente aprÚs un redémarrage avant de vérifier que les conteneurs tournent toujours.
IMAGE_PRUNE_ENABLEDtrueExécute docker image prune -af aprÚs chaque passage.
⁠Notifications — sĂ©lection des canaux
VariableDéfautDescription
NOTIFY_CHANNELSemailListe séparée par des virgules, combinaison libre parmi email,discord,ntfy,telegram,gotify,pushover.
NOTIFY_PREFIX[docker-update]Préfixe ajouté au sujet/titre de chaque notification.

Une notification n'est envoyĂ©e que si quelque chose a Ă©chouĂ© ou a effectivement Ă©tĂ© mis Ă  jour — une exĂ©cution oĂč tout est inchangĂ© reste silencieuse.

⁠Canaux de notification

Activez n'importe quelle combinaison via NOTIFY_CHANNELS. Un canal listĂ© mais non configurĂ© (URL/jeton vide) est simplement ignorĂ© avec une ligne de log — il ne bloque jamais l'exĂ©cution.

📧 Email (msmtp)
VariableDescription
EMAIL_TOAdresse destinataire. Vide = canal désactivé.
SMTP_HOST / SMTP_PORTVotre serveur SMTP, ex: smtp.gmail.com / 587.
SMTP_USER / SMTP_PASSWORDIdentifiants SMTP (utiliser un mot de passe d'application avec Gmail, pas le mot de passe du compte).
SMTP_FROMAdresse expéditrice (par défaut : SMTP_USER).
SMTP_TLS / SMTP_STARTTLSon/off.

Les identifiants ne sont injectĂ©s qu'au runtime — ~/.msmtprc est gĂ©nĂ©rĂ© par l'entrypoint Ă  partir de ces variables, jamais intĂ©grĂ© Ă  l'image.

💬 Discord
VariableDescription
DISCORD_WEBHOOK_URLVide = canal désactivé.

Pour l'obtenir : salon Discord → ParamĂštres → IntĂ©grations → Webhooks → Nouveau webhook → copier l'URL.

🔔 ntfy (auto-hĂ©bergĂ© ou ntfy.sh)
VariableDéfautDescription
NTFY_URLhttps://ntfy.shVotre serveur ntfy, auto-hébergé ou public.
NTFY_TOPIC—Nom du sujet. Vide = canal dĂ©sactivĂ©.
NTFY_TOKEN—Jeton d'accùs, uniquement si votre serveur l'exige.
NTFY_PRIORITYdefaultmin/low/default/high/urgent.

Sur le service public ntfy.sh, quiconque connaĂźt le nom de votre sujet peut lire vos messages — choisissez un nom difficile Ă  deviner, ou auto-hĂ©bergez votre propre serveur.

✈ Telegram
VariableDescription
TELEGRAM_BOT_TOKENVide = canal désactivé.
TELEGRAM_CHAT_IDLa conversation/le groupe qui doit recevoir les messages.

Mise en place : parler Ă  @BotFather sur Telegram, /newbot → donne le jeton. DĂ©marrer une conversation avec ce bot (ou l'ajouter Ă  un groupe), puis rĂ©cupĂ©rer l'identifiant de conversation via https://api.telegram.org/bot<TOKEN>/getUpdates aprĂšs lui avoir envoyĂ© un message — chercher "chat":{"id":...} dans la rĂ©ponse.

📡 Gotify (auto-hĂ©bergĂ©)
VariableDéfautDescription
GOTIFY_URL—ex: https://gotify.example.com. Vide = canal dĂ©sactivĂ©.
GOTIFY_TOKEN—Jeton d'application, créé dans l'interface web Gotify.
GOTIFY_PRIORITY50 Ă  10.
đŸ“Č Pushover
VariableDéfautDescription
PUSHOVER_TOKEN—Jeton d'application depuis pushover.net. Vide = canal dĂ©sactivĂ©.
PUSHOVER_USER_KEY—Votre clĂ© utilisateur (ou de groupe).
PUSHOVER_PRIORITY0-2 Ă  2.

⁠Sécurité

  • Jamais en root — un utilisateur dĂ©diĂ© non-root avec PUID/PGID configurables, la bascule se faisant via su-exec aprĂšs une courte Ă©tape d'initialisation en root (correction des permissions, gĂ©nĂ©ration de .msmtprc).
  • Images de base Ă©pinglĂ©es par digest SHA256, jamais par tag — builds immuables et reproductibles.
  • Aucun secret dans l'image : pas d'identifiants dans ENV/ARG du Dockerfile. SMTP et tous les jetons/URL de notification sont injectĂ©s au runtime depuis .env, et .msmtprc est rĂ©gĂ©nĂ©rĂ© Ă  chaque dĂ©marrage.
  • Aucun montage direct de docker.sock dans le conteneur de mise Ă  jour. Le seul conteneur qui touche le vrai socket est docker-socket-proxy, configurĂ© avec une liste minimale d'autorisations (containers, images, rĂ©seaux, volumes, build, start/stop/restart) — Swarm, secrets, exec, auth et tout le reste sont explicitement refusĂ©s.
  • Le proxy n'est jamais exposĂ© sur un port de l'hĂŽte — uniquement joignable depuis le rĂ©seau interne docker-update-net.

⁠Dépannage

SymptĂŽmeCause probable
Notification "🚹 Aucun projet trouvĂ©" Ă  chaque exĂ©cutionPROJETS_DIR n'est pas montĂ©, ou ne contient rĂ©ellement aucun docker-compose.yml dans la profondeur FIND_MAXDEPTH.
Les volumes d'autres projets cassent aprĂšs une mise Ă  jourPROJETS_DIR n'est pas montĂ© au mĂȘme chemin absolu cĂŽtĂ© hĂŽte et conteneur — voir l'avertissement DooD ci-dessus.
"Permission denied" sur le dossier de logsPUID/PGID ne correspondent pas au propriétaire de LOGS_DIR sur l'hÎte.
Un canal de notification n'arrive jamaisConsulter docker compose logs docker-update — un canal mal configurĂ© affiche une ligne "ignorĂ©" claire plutĂŽt que d'Ă©chouer silencieusement.
Le proxy de socket ne dĂ©marre pasCertains hĂŽtes ont besoin de privileged: true sur docker-socket-proxy pour des raisons AppArmor/SELinux (dĂ©jĂ  activĂ©) — essayez de le retirer si votre hĂŽte n'en a pas besoin.

Les logs complets se trouvent dans ${LOGS_DIR}/update-docker-<horodatage>.log sur l'hÎte, avec ${LOGS_DIR}/latest.log qui pointe toujours vers la derniÚre exécution.

Tag summary

Content type

Image

Digest

sha256:cf64fe6bd


Size

70 MB

Last updated

2 months ago

docker pull ludix0/docker-update