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