[{"data":1,"prerenderedAt":1317},["ShallowReactive",2],{"blog-\u002Fblog\u002Fwhy-rust-docker-updater":3,"blog-all-meta":118},{"id":4,"title":5,"body":6,"date":104,"description":105,"draft":106,"extension":107,"meta":108,"navigation":109,"ogImage":110,"path":111,"seo":112,"stem":113,"tags":114,"__hash__":117},"blog\u002Fblog\u002Fwhy-rust-docker-updater.md","Why we built a Docker updater in Rust",{"type":7,"value":8,"toc":95},"minimark",[9,13,18,26,29,33,44,52,56,59,66,69,73,76],[10,11,12],"p",{},"Almost every Docker auto-updater in the wild is written in Go: Watchtower, Diun, What's Up Docker, the lot. So why write another one in Rust? Not for novelty. Three concrete reasons.",[14,15,17],"h2",{"id":16},"a-small-binary-that-doesnt-manage-your-homelab-with-a-runtime","A small binary that doesn't manage your homelab with a runtime",[10,19,20,21,25],{},"The thing that updates your containers shouldn't be the heaviest container on the box. freshdock compiles to a ",[22,23,24],"strong",{},"single static-musl binary, ≤ 10 MB",", with no runtime dependencies: no JVM, no language runtime, no 100 MB image. The multi-arch container image (amd64, arm64, armv7) is a thin wrapper around that binary.",[10,27,28],{},"This matters most on the hardware homelabbers actually run: a Raspberry Pi, an old NUC, a NAS. A tool whose footprint is rounding error is a tool you forget is even there.",[14,30,32],{"id":31},"modern-docker-via-bollard","Modern Docker, via bollard",[10,34,35,36,43],{},"Watchtower's fatal flaw wasn't the language. It was an embedded Docker SDK pinned to API 1.25 that can't talk to Docker Engine 29+. freshdock uses ",[37,38,42],"a",{"href":39,"rel":40},"https:\u002F\u002Fgithub.com\u002Ffussybeaver\u002Fbollard",[41],"nofollow","bollard",", a mature Rust Docker client that auto-negotiates the API version. It's tested against Docker 24.x through 29+, and it speaks Podman's Docker-compatible socket without changes.",[10,45,46,47,51],{},"The language helped here in a quieter way: bollard's types make the hardest part of the whole project, faithfully recreating a container with the ",[48,49,50],"em",{},"exact"," same config, something the compiler helps you get right.",[14,53,55],{"id":54},"the-recreate-problem-wants-a-state-machine","The recreate problem wants a state machine",[10,57,58],{},"\"Restart the container with the same options\" is the single most error-prone thing an updater does. Get a network alias, a mount, a capability, or a restart policy wrong and you've silently broken a service.",[10,60,61,62,65],{},"freshdock captures the running container's full config, maps it onto a fresh container with the new image, and a dedicated round-trip test asserts the recreated config comes back byte-identical except for the image and container ID. That test is the project's quality gate. Rust's enums and exhaustive matching make the update ",[48,63,64],{},"lifecycle"," (inspect, pull, stop, rename, create, start, health-gate, then either succeed or roll back) a state machine where the \"what if this step fails?\" branch is impossible to forget, because the compiler won't let you.",[10,67,68],{},"That rollback path is the whole point. An update either proves healthy and stays, or it's reverted automatically. Async is handled with Tokio; the cron parser and scheduler are hand-rolled to keep the dependency surface small (chrono is pulled in only for DST-correct local-time math).",[14,70,72],{"id":71},"being-honest-about-it","Being honest about it",[10,74,75],{},"Rust didn't make freshdock automatically better than the Go tools. Go is a perfectly good choice and those tools are mature. What Rust bought us, specifically, is a tiny static binary and a type system that makes the safety-critical recreate logic hard to get subtly wrong. For a tool whose failure mode is \"your service is down and you're asleep,\" that trade was worth making.",[10,77,78,79,83,84,88,89,94],{},"If that resonates, the ",[37,80,82],{"href":81},"\u002Ffeatures","features page"," walks through the health-gated lifecycle in detail, and the ",[37,85,87],{"href":86},"\u002Finstall","install guide"," gets you running in a minute. The original design rationale (goals, non-goals, and the phased roadmap) lives in the ",[37,90,93],{"href":91,"rel":92},"https:\u002F\u002Fturbootzz.github.io\u002Ffreshdock\u002FPLAN.html",[41],"architecture doc",".",{"title":96,"searchDepth":97,"depth":97,"links":98},"",3,[99,101,102,103],{"id":16,"depth":100,"text":17},2,{"id":31,"depth":100,"text":32},{"id":54,"depth":100,"text":55},{"id":71,"depth":100,"text":72},"2026-06-17","Most Docker update tools are Go; freshdock is Rust. The reasoning: a tiny static binary, modern Docker via bollard, and a safety-first update state machine.",false,"md",{},true,null,"\u002Fblog\u002Fwhy-rust-docker-updater",{"title":5,"description":105},"blog\u002Fwhy-rust-docker-updater",[115,116],"rust","design","r0Bxi1r0OosOugubS1bM5HK836aeuW53uKz2FUpW13w",[119,329,789,1019,1261],{"id":120,"title":121,"body":122,"date":319,"description":320,"draft":106,"extension":107,"meta":321,"navigation":109,"ogImage":110,"path":322,"seo":323,"stem":324,"tags":325,"__hash__":328},"blog\u002Fblog\u002Fwatchtower-archived-what-to-do.md","Watchtower is archived: here's what to do now",{"type":7,"value":123,"toc":313},[124,133,145,149,156,159,163,166,196,200,207,236,248,252,255,286,293,309],[10,125,126,127,132],{},"If you run a homelab, you have probably leaned on ",[37,128,131],{"href":129,"rel":130},"https:\u002F\u002Fgithub.com\u002Fcontainrrr\u002Fwatchtower",[41],"Watchtower"," at some point. For the better part of a decade it was the default answer to \"how do I keep my containers up to date?\" That era is over.",[10,134,135,136,139,140,144],{},"On ",[22,137,138],{},"17 December 2025",", the maintainers archived ",[141,142,143],"code",{},"containrrr\u002Fwatchtower",". Archived means read-only: no more releases, no more fixes, no more security patches. And there is a second, more urgent problem.",[14,146,148],{"id":147},"why-it-doesnt-just-keep-working","Why it doesn't just keep working",[10,150,151,152,155],{},"Watchtower ships an ",[48,153,154],{},"embedded"," Docker SDK pinned to API version 1.25. Docker Engine 29 and later require API 1.44 or newer. The two can no longer negotiate a common protocol, so on a current Docker host Watchtower simply fails to talk to the daemon. This isn't a slow deprecation you can ignore. Upgrade your Docker Engine and Watchtower stops working.",[10,157,158],{},"So \"do nothing\" has an expiry date attached to your next Docker upgrade.",[14,160,162],{"id":161},"your-options","Your options",[10,164,165],{},"There are a few honest paths forward:",[167,168,169,176,190],"ol",{},[170,171,172,175],"li",{},[22,173,174],{},"Pin Docker and freeze."," You can hold Docker Engine below 29 and keep the archived Watchtower limping along. This trades your container security posture for your update tool's, not a good trade for long.",[170,177,178,181,182,189],{},[22,179,180],{},"Use the community fork."," ",[37,183,186],{"href":184,"rel":185},"https:\u002F\u002Fgithub.com\u002Fnicholas-fedor\u002Fwatchtower",[41],[141,187,188],{},"nicholas-fedor\u002Fwatchtower"," is an active fork that keeps the original alive on modern Docker. If you want Watchtower's exact labels and behaviour with the least disruption, this is the lift-and-shift option. It's still the same Go codebase and the same safety model, though: a stop-gap, not a rethink.",[170,191,192,195],{},[22,193,194],{},"Move to a maintained successor."," Switch to a tool that's built for current Docker and adds the safety net Watchtower never had.",[14,197,199],{"id":198},"what-freshdock-changes","What freshdock changes",[10,201,202,206],{},[37,203,205],{"href":204},"\u002F","freshdock"," is a from-scratch successor written in Rust. It targets modern Docker (tested 24.x through 29+, auto-negotiated) and adds the thing that makes unattended updates actually safe:",[208,209,210,216,230],"ul",{},[170,211,212,215],{},[22,213,214],{},"Health-gated rollback."," A container counts as updated only after the new one passes its healthcheck, or stays up for a grace period if it has none. If the new image fails to come up, freshdock restores the previous container automatically and notifies you. No more waking up to a dead service.",[170,217,218,221,222,225,226,229],{},[22,219,220],{},"Opt-in by design."," Watchtower updates everything unless you exclude it. freshdock ignores every container until you set ",[141,223,224],{},"freshdock.enable=true",", and an enabled container with no mode defaults to ",[141,227,228],{},"watch"," (detect and notify, never restart).",[170,231,232,235],{},[22,233,234],{},"One small binary."," A single static Rust binary, ≤ 10 MB, instead of a runtime managing your other containers.",[10,237,238,239,242,243,247],{},"It's not a drop-in for ",[48,240,241],{},"every"," Watchtower setup. There's no dependency ordering, no \"update without pulling\", and Kubernetes and Swarm are deliberately out of scope. The ",[37,244,246],{"href":245},"\u002Fwatchtower-alternative","full comparison"," is honest about where each tool wins.",[14,249,251],{"id":250},"the-five-minute-version","The five-minute version",[10,253,254],{},"If you want to try it without risk, install it and run the read-only check first, since it never touches a container:",[256,257,261],"pre",{"className":258,"code":259,"language":260,"meta":96,"style":96},"language-bash shiki shiki-themes github-dark-high-contrast","cargo install freshdock\nfreshdock check\n","bash",[141,262,263,279],{"__ignoreMap":96},[264,265,268,272,276],"span",{"class":266,"line":267},"line",1,[264,269,271],{"class":270},"s_sBn","cargo",[264,273,275],{"class":274},"sTRMh"," install",[264,277,278],{"class":274}," freshdock\n",[264,280,281,283],{"class":266,"line":100},[264,282,205],{"class":270},[264,284,285],{"class":274}," check\n",[10,287,288,289,292],{},"That prints a table of which containers have updates available. When you trust it, graduate one container to ",[141,290,291],{},"freshdock.mode=nightly"," and let the daemon take over.",[10,294,295,296,299,300,303,304,94],{},"Ready to switch? Start with the ",[37,297,298],{"href":86},"installation guide"," or read the ",[37,301,302],{"href":245},"step-by-step migration",". The full label-and-flag translation lives in the ",[37,305,308],{"href":306,"rel":307},"https:\u002F\u002Fturbootzz.github.io\u002Ffreshdock\u002Fmigrating-from-watchtower.html",[41],"migration guide on the docs site",[310,311,312],"style",{},"html pre.shiki code .s_sBn, html code.shiki .s_sBn{--shiki-default:#FFB757}html pre.shiki code .sTRMh, html code.shiki .sTRMh{--shiki-default:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":96,"searchDepth":97,"depth":97,"links":314},[315,316,317,318],{"id":147,"depth":100,"text":148},{"id":161,"depth":100,"text":162},{"id":198,"depth":100,"text":199},{"id":250,"depth":100,"text":251},"2026-06-23","Watchtower was archived in December 2025 and no longer works with Docker Engine 29+. Here are your real options, and how to move to a maintained replacement.",{},"\u002Fblog\u002Fwatchtower-archived-what-to-do",{"title":121,"description":320},"blog\u002Fwatchtower-archived-what-to-do",[326,327],"watchtower","migration","vCJQefGYLgXpFkt2uyjTO2T1TRAg9RDg-FY-Z3401S8",{"id":330,"title":331,"body":332,"date":781,"description":782,"draft":106,"extension":107,"meta":783,"navigation":109,"ogImage":110,"path":784,"seo":785,"stem":786,"tags":787,"__hash__":788},"blog\u002Fblog\u002Fmigrating-from-watchtower-in-5-minutes.md","Migrating from Watchtower to freshdock in 5 minutes",{"type":7,"value":333,"toc":774},[334,343,347,350,380,386,390,401,473,488,492,495,603,606,711,724,728,731,742,749,753,764,771],[10,335,336,339,340,342],{},[37,337,338],{"href":322},"Watchtower is archived"," and breaks on Docker Engine 29+. Moving to ",[37,341,205],{"href":204}," is mostly a relabel plus a service swap. Here's the whole thing, start to finish.",[14,344,346],{"id":345},"_1-install-freshdock","1. Install freshdock",[10,348,349],{},"Pick whichever fits. The result is the same single binary:",[256,351,353],{"className":258,"code":352,"language":260,"meta":96,"style":96},"cargo install freshdock\n# or pull the multi-arch image\ndocker pull ghcr.io\u002Fturbootzz\u002Ffreshdock:latest\n",[141,354,355,363,369],{"__ignoreMap":96},[264,356,357,359,361],{"class":266,"line":267},[264,358,271],{"class":270},[264,360,275],{"class":274},[264,362,278],{"class":274},[264,364,365],{"class":266,"line":100},[264,366,368],{"class":367},"sQrFR","# or pull the multi-arch image\n",[264,370,371,374,377],{"class":266,"line":97},[264,372,373],{"class":270},"docker",[264,375,376],{"class":274}," pull",[264,378,379],{"class":274}," ghcr.io\u002Fturbootzz\u002Ffreshdock:latest\n",[10,381,382,383,94],{},"Full options on the ",[37,384,385],{"href":86},"install page",[14,387,389],{"id":388},"_2-translate-your-labels","2. Translate your labels",[10,391,392,393,396,397,400],{},"The concepts map closely; the spelling changes. The big one to internalise: ",[22,394,395],{},"freshdock is opt-in",", so you rarely need to ",[48,398,399],{},"disable"," anything. Unlabelled containers are simply ignored.",[402,403,404,415],"table",{},[405,406,407],"thead",{},[408,409,410,413],"tr",{},[411,412,131],"th",{},[411,414,205],{},[416,417,418,430,442,460],"tbody",{},[408,419,420,426],{},[421,422,423],"td",{},[141,424,425],{},"com.centurylinklabs.watchtower.enable=true",[421,427,428],{},[141,429,224],{},[408,431,432,437],{},[421,433,434],{},[141,435,436],{},"watchtower.monitor-only=true",[421,438,439],{},[141,440,441],{},"freshdock.mode=watch",[408,443,444,450],{},[421,445,446,449],{},[141,447,448],{},"WATCHTOWER_SCHEDULE"," (one global cron)",[421,451,452,453,456,457],{},"per-container ",[141,454,455],{},"freshdock.mode"," + ",[141,458,459],{},"freshdock.schedule",[408,461,462,468],{},[421,463,464,467],{},[141,465,466],{},"watchtower.enable=false"," (with global watch)",[421,469,470],{},[48,471,472],{},"just omit the labels",[10,474,475,476,479,480,483,484,487],{},"Two Watchtower features have no freshdock equivalent today: ",[141,477,478],{},"no-pull"," (freshdock always pulls before recreate) and ",[141,481,482],{},"depends-on"," dependency ordering. If you rely on those, check the ",[37,485,486],{"href":245},"comparison page"," before switching.",[14,489,491],{"id":490},"_3-swap-the-service-in-docker-compose","3. Swap the service in docker-compose",[10,493,494],{},"Replace the Watchtower service and relabel your apps. Before:",[256,496,500],{"className":497,"code":498,"language":499,"meta":96,"style":96},"language-yaml shiki shiki-themes github-dark-high-contrast","services:\n  app:\n    image: ghcr.io\u002Fexample\u002Fapp:latest\n    labels:\n      - \"com.centurylinklabs.watchtower.enable=true\"\n\n  watchtower:\n    image: containrrr\u002Fwatchtower\n    volumes:\n      - \u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock\n    environment:\n      - WATCHTOWER_SCHEDULE=0 0 4 * * *\n","yaml",[141,501,502,512,519,530,538,547,553,561,571,579,587,595],{"__ignoreMap":96},[264,503,504,508],{"class":266,"line":267},[264,505,507],{"class":506},"sKpQp","services",[264,509,511],{"class":510},"sMAXC",":\n",[264,513,514,517],{"class":266,"line":100},[264,515,516],{"class":506},"  app",[264,518,511],{"class":510},[264,520,521,524,527],{"class":266,"line":97},[264,522,523],{"class":506},"    image",[264,525,526],{"class":510},": ",[264,528,529],{"class":274},"ghcr.io\u002Fexample\u002Fapp:latest\n",[264,531,533,536],{"class":266,"line":532},4,[264,534,535],{"class":506},"    labels",[264,537,511],{"class":510},[264,539,541,544],{"class":266,"line":540},5,[264,542,543],{"class":510},"      - ",[264,545,546],{"class":274},"\"com.centurylinklabs.watchtower.enable=true\"\n",[264,548,550],{"class":266,"line":549},6,[264,551,552],{"emptyLinePlaceholder":109},"\n",[264,554,556,559],{"class":266,"line":555},7,[264,557,558],{"class":506},"  watchtower",[264,560,511],{"class":510},[264,562,564,566,568],{"class":266,"line":563},8,[264,565,523],{"class":506},[264,567,526],{"class":510},[264,569,570],{"class":274},"containrrr\u002Fwatchtower\n",[264,572,574,577],{"class":266,"line":573},9,[264,575,576],{"class":506},"    volumes",[264,578,511],{"class":510},[264,580,582,584],{"class":266,"line":581},10,[264,583,543],{"class":510},[264,585,586],{"class":274},"\u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock\n",[264,588,590,593],{"class":266,"line":589},11,[264,591,592],{"class":506},"    environment",[264,594,511],{"class":510},[264,596,598,600],{"class":266,"line":597},12,[264,599,543],{"class":510},[264,601,602],{"class":274},"WATCHTOWER_SCHEDULE=0 0 4 * * *\n",[10,604,605],{},"After:",[256,607,609],{"className":497,"code":608,"language":499,"meta":96,"style":96},"services:\n  app:\n    image: ghcr.io\u002Fexample\u002Fapp:latest\n    labels:\n      - \"freshdock.enable=true\"\n      - \"freshdock.mode=nightly\"   # 04:00 daily\n\n  freshdock:\n    image: ghcr.io\u002Fturbootzz\u002Ffreshdock:latest\n    command: [\"run\"]\n    volumes:\n      - \u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock\n    restart: unless-stopped\n",[141,610,611,617,623,631,637,644,654,658,665,674,688,694,700],{"__ignoreMap":96},[264,612,613,615],{"class":266,"line":267},[264,614,507],{"class":506},[264,616,511],{"class":510},[264,618,619,621],{"class":266,"line":100},[264,620,516],{"class":506},[264,622,511],{"class":510},[264,624,625,627,629],{"class":266,"line":97},[264,626,523],{"class":506},[264,628,526],{"class":510},[264,630,529],{"class":274},[264,632,633,635],{"class":266,"line":532},[264,634,535],{"class":506},[264,636,511],{"class":510},[264,638,639,641],{"class":266,"line":540},[264,640,543],{"class":510},[264,642,643],{"class":274},"\"freshdock.enable=true\"\n",[264,645,646,648,651],{"class":266,"line":549},[264,647,543],{"class":510},[264,649,650],{"class":274},"\"freshdock.mode=nightly\"",[264,652,653],{"class":367},"   # 04:00 daily\n",[264,655,656],{"class":266,"line":555},[264,657,552],{"emptyLinePlaceholder":109},[264,659,660,663],{"class":266,"line":563},[264,661,662],{"class":506},"  freshdock",[264,664,511],{"class":510},[264,666,667,669,671],{"class":266,"line":573},[264,668,523],{"class":506},[264,670,526],{"class":510},[264,672,673],{"class":274},"ghcr.io\u002Fturbootzz\u002Ffreshdock:latest\n",[264,675,676,679,682,685],{"class":266,"line":581},[264,677,678],{"class":506},"    command",[264,680,681],{"class":510},": [",[264,683,684],{"class":274},"\"run\"",[264,686,687],{"class":510},"]\n",[264,689,690,692],{"class":266,"line":589},[264,691,576],{"class":506},[264,693,511],{"class":510},[264,695,696,698],{"class":266,"line":597},[264,697,543],{"class":510},[264,699,586],{"class":274},[264,701,703,706,708],{"class":266,"line":702},13,[264,704,705],{"class":506},"    restart",[264,707,526],{"class":510},[264,709,710],{"class":274},"unless-stopped\n",[10,712,713,714,717,718,720,721,94],{},"A read-only socket (",[141,715,716],{},":ro",") is enough while everything is on ",[141,719,228],{},"; give freshdock a writable socket once a container is on an updating mode like ",[141,722,723],{},"nightly",[14,725,727],{"id":726},"_4-verify-read-only-then-commit","4. Verify read-only, then commit",[10,729,730],{},"Before you let it change anything, run the read-only check. It lists your opted-in containers and what has updates, and never pulls, stops, or recreates:",[256,732,734],{"className":258,"code":733,"language":260,"meta":96,"style":96},"freshdock check\n",[141,735,736],{"__ignoreMap":96},[264,737,738,740],{"class":266,"line":267},[264,739,205],{"class":270},[264,741,285],{"class":274},[10,743,744,745,748],{},"Happy with the table? You're done. The daemon (",[141,746,747],{},"freshdock run",") will now health-gate every update and roll back any that fail to come up.",[14,750,752],{"id":751},"notifications-and-registries","Notifications and registries",[10,754,755,756,759,760,763],{},"If you used Watchtower's shoutrrr notifications, freshdock has webhook, Discord, Telegram, and SMTP backends, declared in a small ",[141,757,758],{},"freshdock.toml",", with secrets supplied via environment variables. Private registry credentials (Docker Hub, GHCR, Quay, lscr) come from ",[141,761,762],{},"FRESHDOCK_REGISTRY_*"," env vars, no file required.",[10,765,766,767,770],{},"The complete label, flag, env, notification, and registry translation table (more than fits here) is the ",[37,768,308],{"href":306,"rel":769},[41],". That's the authoritative reference; this post is just the five-minute path.",[310,772,773],{},"html pre.shiki code .s_sBn, html code.shiki .s_sBn{--shiki-default:#FFB757}html pre.shiki code .sTRMh, html code.shiki .sTRMh{--shiki-default:#ADDCFF}html pre.shiki code .sQrFR, html code.shiki .sQrFR{--shiki-default:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sKpQp, html code.shiki .sKpQp{--shiki-default:#72F088}html pre.shiki code .sMAXC, html code.shiki .sMAXC{--shiki-default:#F0F3F6}",{"title":96,"searchDepth":97,"depth":97,"links":775},[776,777,778,779,780],{"id":345,"depth":100,"text":346},{"id":388,"depth":100,"text":389},{"id":490,"depth":100,"text":491},{"id":726,"depth":100,"text":727},{"id":751,"depth":100,"text":752},"2026-06-22","A practical, copy-paste migration from archived Watchtower to freshdock: translate labels, swap the service in docker-compose, and verify read-only first.",{},"\u002Fblog\u002Fmigrating-from-watchtower-in-5-minutes",{"title":331,"description":782},"blog\u002Fmigrating-from-watchtower-in-5-minutes",[327,326],"iDLSrpKV-rY5v5wAHFP7wq1zVKlq9SQR_94XbiPvmS8",{"id":790,"title":791,"body":792,"date":1009,"description":1010,"draft":106,"extension":107,"meta":1011,"navigation":109,"ogImage":110,"path":1012,"seo":1013,"stem":1014,"tags":1015,"__hash__":1018},"blog\u002Fblog\u002Fnotify-only-vs-auto-update.md","Notify-only vs auto-update: choosing a safe Docker update strategy",{"type":7,"value":793,"toc":1002},[794,801,805,811,817,824,828,831,851,855,861,920,934,938,945,954,958,977,999],[10,795,796,797,800],{},"The loudest debate in container maintenance is framed as a binary: auto-update everything, or update nothing and patch by hand. Both extremes are wrong for most homelabs. The right answer is ",[48,798,799],{},"per container",", and freshdock is built around that.",[14,802,804],{"id":803},"the-two-failure-modes","The two failure modes",[10,806,807,810],{},[22,808,809],{},"Auto-update everything"," is how the \"Watchtower broke my server overnight\" stories happen. A bad upstream image ships, your tool pulls it at 4 a.m., the container won't start, and you find out when something you depend on is down.",[10,812,813,816],{},[22,814,815],{},"Update nothing"," feels safe but isn't. Stale images accumulate known vulnerabilities, and \"I'll get to it\" becomes months. The friction of manual updates is exactly why tools like this exist.",[10,818,819,820,823],{},"The useful question isn't \"auto or manual?\" It's \"what's the cost if ",[48,821,822],{},"this specific container"," updates badly, and how much do I trust its upstream?\"",[14,825,827],{"id":826},"a-simple-framework","A simple framework",[10,829,830],{},"Sort each container into one of three buckets:",[208,832,833,839,845],{},[170,834,835,838],{},[22,836,837],{},"Stateless and well-behaved"," (reverse proxies, dashboards, exporters). Low blast radius, easy to roll back. Good candidates for automatic updates.",[170,840,841,844],{},[22,842,843],{},"Stateful or critical"," (databases, auth, anything with a migration step). High cost if an update goes wrong. Keep these on notify-only and update them deliberately, when you can watch.",[170,846,847,850],{},[22,848,849],{},"Pinned on purpose"," (you need a specific version). Pin the digest and let the tool report it as pinned, no checks.",[14,852,854],{"id":853},"how-freshdock-expresses-this","How freshdock expresses this",[10,856,857,858,860],{},"Each opted-in container picks a ",[141,859,455],{},":",[256,862,864],{"className":497,"code":863,"language":499,"meta":96,"style":96},"# auto-update nightly, with the safety net\nlabels:\n  - \"freshdock.enable=true\"\n  - \"freshdock.mode=nightly\"\n\n# detect updates, but only tell me, never restart\nlabels:\n  - \"freshdock.enable=true\"\n  - \"freshdock.mode=watch\"\n",[141,865,866,871,878,885,892,896,901,907,913],{"__ignoreMap":96},[264,867,868],{"class":266,"line":267},[264,869,870],{"class":367},"# auto-update nightly, with the safety net\n",[264,872,873,876],{"class":266,"line":100},[264,874,875],{"class":506},"labels",[264,877,511],{"class":510},[264,879,880,883],{"class":266,"line":97},[264,881,882],{"class":510},"  - ",[264,884,643],{"class":274},[264,886,887,889],{"class":266,"line":532},[264,888,882],{"class":510},[264,890,891],{"class":274},"\"freshdock.mode=nightly\"\n",[264,893,894],{"class":266,"line":540},[264,895,552],{"emptyLinePlaceholder":109},[264,897,898],{"class":266,"line":549},[264,899,900],{"class":367},"# detect updates, but only tell me, never restart\n",[264,902,903,905],{"class":266,"line":555},[264,904,875],{"class":506},[264,906,511],{"class":510},[264,908,909,911],{"class":266,"line":563},[264,910,882],{"class":510},[264,912,643],{"class":274},[264,914,915,917],{"class":266,"line":573},[264,916,882],{"class":510},[264,918,919],{"class":274},"\"freshdock.mode=watch\"\n",[10,921,922,924,925,930,931,933],{},[141,923,228],{}," is the default, and it's a legitimate permanent choice. It's exactly what ",[37,926,929],{"href":927,"rel":928},"https:\u002F\u002Fgithub.com\u002Fcrazy-max\u002Fdiun",[41],"Diun"," does as its entire purpose: tell you an update exists and let you decide. freshdock just lets you mix ",[141,932,228],{}," and auto-update modes on the same daemon, container by container.",[14,935,937],{"id":936},"auto-update-is-safer-here-than-youd-expect","Auto-update is safer here than you'd expect",[10,939,940,941,944],{},"The usual argument against auto-update assumes a bad update leaves you broken. freshdock's health gate changes that calculus: an updated container has to pass its healthcheck (or a grace period) before the update is kept. If it doesn't, the previous container is restored automatically and you get a ",[141,942,943],{},"failed"," notification. The downside of auto-updating a stateless service shrinks a lot when \"it broke\" becomes \"it reverted and told me.\"",[10,946,947,948,950,951,953],{},"That's why a reasonable default for many homelabs is: stateless services on ",[141,949,723],{},", stateful services on ",[141,952,228],{},", and a healthcheck declared wherever it's cheap to add one, because the gate is only as good as the signal you give it.",[14,955,957],{"id":956},"start-conservative","Start conservative",[10,959,960,961,963,964,969,970,973,974,976],{},"You don't have to decide all of this up front. Install freshdock, label everything ",[141,962,228],{},", and run ",[37,965,966],{"href":86},[141,967,968],{},"freshdock check"," for a week. Watch what ",[48,971,972],{},"would"," have updated. Then promote the containers you trust to ",[141,975,723],{}," one at a time.",[10,978,979,980,982,983,988,989,992,993,996,997,94],{},"More on the modes and the health gate is on the ",[37,981,82],{"href":81}," and in the ",[37,984,987],{"href":985,"rel":986},"https:\u002F\u002Fturbootzz.github.io\u002Ffreshdock\u002Fscheduling.html",[41],"scheduling docs",". If you're coming from Watchtower's ",[141,990,991],{},"monitor-only",", the ",[37,994,995],{"href":245},"comparison"," maps it straight onto ",[141,998,228],{},[310,1000,1001],{},"html pre.shiki code .sQrFR, html code.shiki .sQrFR{--shiki-default:#BDC4CC}html pre.shiki code .sKpQp, html code.shiki .sKpQp{--shiki-default:#72F088}html pre.shiki code .sMAXC, html code.shiki .sMAXC{--shiki-default:#F0F3F6}html pre.shiki code .sTRMh, html code.shiki .sTRMh{--shiki-default:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":96,"searchDepth":97,"depth":97,"links":1003},[1004,1005,1006,1007,1008],{"id":803,"depth":100,"text":804},{"id":826,"depth":100,"text":827},{"id":853,"depth":100,"text":854},{"id":936,"depth":100,"text":937},{"id":956,"depth":100,"text":957},"2026-06-21","Should you auto-update Docker containers or get notified? A practical framework for choosing per-container update modes, and when notify-only is the right call.",{},"\u002Fblog\u002Fnotify-only-vs-auto-update",{"title":791,"description":1010},"blog\u002Fnotify-only-vs-auto-update",[1016,1017],"strategy","homelab","I3YjomXepVY1T7MTEe5P5ERtS8JecPszmjfxrXfqonY",{"id":1020,"title":1021,"body":1022,"date":1252,"description":1253,"draft":106,"extension":107,"meta":1254,"navigation":109,"ogImage":110,"path":1255,"seo":1256,"stem":1257,"tags":1258,"__hash__":1260},"blog\u002Fblog\u002Fhow-freshdock-decides-when-to-update.md","How freshdock decides when to update a container",{"type":7,"value":1023,"toc":1245},[1024,1031,1035,1041,1062,1066,1079,1118,1133,1137,1140,1151,1161,1165,1172,1175,1195,1201,1211,1215,1218,1242],[10,1025,1026,1027,1030],{},"freshdock's job sounds simple (\"update my containers\"), but the interesting part is everything it does ",[48,1028,1029],{},"not"," do automatically. Here's the full decision path, from \"should I even look at this container?\" to \"keep the update or roll it back?\"",[14,1032,1034],{"id":1033},"step-1-is-this-container-opted-in","Step 1: is this container opted in?",[10,1036,1037,1038,1040],{},"freshdock is opt-in. A container with no ",[141,1039,224],{}," label is invisible to it: no checks, no notifications, nothing. This is the opposite of Watchtower's update-everything default, and it's deliberate: you decide what freshdock manages, one label at a time.",[256,1042,1044],{"className":497,"code":1043,"language":499,"meta":96,"style":96},"labels:\n  - \"freshdock.enable=true\"   # now freshdock can see it\n",[141,1045,1046,1052],{"__ignoreMap":96},[264,1047,1048,1050],{"class":266,"line":267},[264,1049,875],{"class":506},[264,1051,511],{"class":510},[264,1053,1054,1056,1059],{"class":266,"line":100},[264,1055,882],{"class":510},[264,1057,1058],{"class":274},"\"freshdock.enable=true\"",[264,1060,1061],{"class":367},"   # now freshdock can see it\n",[14,1063,1065],{"id":1064},"step-2-what-mode-is-it-in","Step 2: what mode is it in?",[10,1067,1068,1069,1071,1072,1075,1076,860],{},"An enabled container picks a mode with ",[141,1070,455],{},". The mode decides ",[48,1073,1074],{},"whether"," freshdock acts and ",[48,1077,1078],{},"when",[208,1080,1081,1090,1100,1112],{},[170,1082,1083,1085,1086,1089],{},[141,1084,228],{},": detect updates and notify only. Never pulls, never restarts. ",[22,1087,1088],{},"This is the default"," for an enabled container with no explicit mode.",[170,1091,1092,1095,1096,1099],{},[141,1093,1094],{},"live",": pull and recreate on every new digest, checked every ",[141,1097,1098],{},"--interval"," (default 300s).",[170,1101,1102,1104,1105,1104,1108,1111],{},[141,1103,723],{}," \u002F ",[141,1106,1107],{},"weekly",[141,1109,1110],{},"monthly",": recreate if newer, on a cron schedule (default 04:00).",[170,1113,1114,1117],{},[141,1115,1116],{},"off",": ignored entirely.",[10,1119,1120,1121,1123,1124,1126,1127,1129,1130,1132],{},"A single daemon mixes these freely. Your reverse proxy can be on ",[141,1122,1094],{},", your database on ",[141,1125,228],{},", your media server on ",[141,1128,1107],{},". Calendar modes take an optional ",[141,1131,459],{}," cron override.",[14,1134,1136],{"id":1135},"step-3-is-there-actually-a-newer-image","Step 3: is there actually a newer image?",[10,1138,1139],{},"When a container is due, freshdock resolves the latest digest for its image tag against the registry, using a rate-friendly HEAD request, not a full pull, and deduplicated so ten containers on the same image cost one lookup. It compares that digest to what the container is currently running.",[10,1141,1142,1143,1146,1147,1150],{},"If the digest is unchanged, freshdock stops here. If the image is pinned to a digest (",[141,1144,1145],{},"repo@sha256:…","), there's no moving tag to follow, so it's reported as ",[141,1148,1149],{},"pinned (no check)"," and never updated. You can see all of this without changing anything by running:",[256,1152,1153],{"className":258,"code":733,"language":260,"meta":96,"style":96},[141,1154,1155],{"__ignoreMap":96},[264,1156,1157,1159],{"class":266,"line":267},[264,1158,205],{"class":270},[264,1160,285],{"class":274},[14,1162,1164],{"id":1163},"step-4-the-health-gate","Step 4: the health gate",[10,1166,1167,1168,1171],{},"This is the part that makes unattended updates safe. For an updating mode with a newer digest, freshdock runs the recreate cycle: inspect the running container, pull the new image, stop the old one, rename it to an archive, create the new container from the ",[48,1169,1170],{},"exact same config",", and start it.",[10,1173,1174],{},"Then it waits. The container reaches one of three verdicts:",[208,1176,1177,1183,1189],{},[170,1178,1179,1182],{},[22,1180,1181],{},"Healthy",": a declared healthcheck reported healthy, or (no healthcheck) the container stayed up for the grace period. The archive is removed; the update stands.",[170,1184,1185,1188],{},[22,1186,1187],{},"Timeout",": a healthcheck was declared but never went healthy in time. Roll back.",[170,1190,1191,1194],{},[22,1192,1193],{},"Crashed",": the container exited before becoming healthy. Roll back.",[10,1196,1197,1198,1200],{},"On a rollback, freshdock removes the failed container, renames the archive back to the original name, and restarts it. You're left running exactly what you had, plus a ",[141,1199,943],{}," notification explaining why.",[1202,1203,1204],"blockquote",{},[10,1205,1206,1207,1210],{},"The health timeout (120s), the grace period for containers without a healthcheck (10s), and the poll interval (1s) are currently hardcoded. Declaring a ",[141,1208,1209],{},"HEALTHCHECK"," on your image gives the gate a much stronger signal than \"did it stay up?\".",[14,1212,1214],{"id":1213},"why-this-matters","Why this matters",[10,1216,1217],{},"The whole point is that freshdock will never knowingly leave you with a broken service. An update either passes its own health check and stays, or it's reverted automatically. That's the difference between \"set and forget\" being a feature and being a liability.",[10,1219,1220,1221,1226,1227,1231,1232,1234,1235,1237,1238,1241],{},"The exact verdict logic and timings are documented in ",[37,1222,1225],{"href":1223,"rel":1224},"https:\u002F\u002Fturbootzz.github.io\u002Ffreshdock\u002Fhealth-and-rollback.html",[41],"health & rollback","; the scheduling model is in ",[37,1228,1230],{"href":985,"rel":1229},[41],"scheduling",". When you're ready to try it, the ",[37,1233,87],{"href":86}," gets you to a read-only ",[141,1236,968],{}," in a minute, and the ",[37,1239,1240],{"href":245},"Watchtower comparison"," shows how this differs from the tool you're probably replacing.",[310,1243,1244],{},"html pre.shiki code .sKpQp, html code.shiki .sKpQp{--shiki-default:#72F088}html pre.shiki code .sMAXC, html code.shiki .sMAXC{--shiki-default:#F0F3F6}html pre.shiki code .sTRMh, html code.shiki .sTRMh{--shiki-default:#ADDCFF}html pre.shiki code .sQrFR, html code.shiki .sQrFR{--shiki-default:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s_sBn, html code.shiki .s_sBn{--shiki-default:#FFB757}",{"title":96,"searchDepth":97,"depth":97,"links":1246},[1247,1248,1249,1250,1251],{"id":1033,"depth":100,"text":1034},{"id":1064,"depth":100,"text":1065},{"id":1135,"depth":100,"text":1136},{"id":1163,"depth":100,"text":1164},{"id":1213,"depth":100,"text":1214},"2026-06-19","A walk through freshdock's update logic: opt-in labels, per-container modes, digest checks, and the health gate that decides if an update sticks or rolls back.",{},"\u002Fblog\u002Fhow-freshdock-decides-when-to-update",{"title":1021,"description":1253},"blog\u002Fhow-freshdock-decides-when-to-update",[1259,1230],"how-it-works","HOSYfxDRCF5upDGzNvwy2Z9ba-yb6C2Nry097LGJwps",{"id":4,"title":5,"body":1262,"date":104,"description":105,"draft":106,"extension":107,"meta":1314,"navigation":109,"ogImage":110,"path":111,"seo":1315,"stem":113,"tags":1316,"__hash__":117},{"type":7,"value":1263,"toc":1308},[1264,1266,1268,1272,1274,1276,1281,1285,1287,1289,1293,1295,1297,1299],[10,1265,12],{},[14,1267,17],{"id":16},[10,1269,20,1270,25],{},[22,1271,24],{},[10,1273,28],{},[14,1275,32],{"id":31},[10,1277,35,1278,43],{},[37,1279,42],{"href":39,"rel":1280},[41],[10,1282,46,1283,51],{},[48,1284,50],{},[14,1286,55],{"id":54},[10,1288,58],{},[10,1290,61,1291,65],{},[48,1292,64],{},[10,1294,68],{},[14,1296,72],{"id":71},[10,1298,75],{},[10,1300,78,1301,83,1303,88,1305,94],{},[37,1302,82],{"href":81},[37,1304,87],{"href":86},[37,1306,93],{"href":91,"rel":1307},[41],{"title":96,"searchDepth":97,"depth":97,"links":1309},[1310,1311,1312,1313],{"id":16,"depth":100,"text":17},{"id":31,"depth":100,"text":32},{"id":54,"depth":100,"text":55},{"id":71,"depth":100,"text":72},{},{"title":5,"description":105},[115,116],1782377498270]