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