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