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