[{"data":1,"prerenderedAt":5424},["ShallowReactive",2],{"nav-stories":3,"footer-stories":61,"home-projects":74,"home-blog":2473},[4,16,25,34,43,52],{"id":5,"color":6,"extension":7,"image":8,"label":9,"link":10,"meta":11,"order":12,"stem":13,"text":14,"__hash__":15},"stories\u002Fstories\u002F01-data-center.yml",null,"yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=1080","DATA_CENTER","https:\u002F\u002Fx.com\u002Fabbeytetteh_",{},1,"stories\u002F01-data-center","Racking new servers. 40gbit backbone online.","0QUZQbaANhdO8WemZxkDdO7vbVopfnynHtH9FxBZb_w",{"id":17,"color":6,"extension":7,"image":18,"label":19,"link":6,"meta":20,"order":21,"stem":22,"text":23,"__hash__":24},"stories\u002Fstories\u002F02-thoughts.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1498050108023-c5249f4df085?w=1080","THOUGHTS",{},2,"stories\u002F02-thoughts","Late night bug hunting. Found the memory leak.","Gd1am954aasY6HRHD7hCtOuessXb6zYZ8iizS501ICg",{"id":26,"color":27,"extension":7,"image":6,"label":28,"link":6,"meta":29,"order":30,"stem":31,"text":32,"__hash__":33},"stories\u002Fstories\u002F03-coding.yml","#3b82f6","CODING",{},3,"stories\u002F03-coding","Just thinking about how much easier life is with Swarm.","vLAyiGUPtlXB2SHa5KM_U2AaK4QkG3Og85UEUE7qzgM",{"id":35,"color":6,"extension":7,"image":36,"label":37,"link":6,"meta":38,"order":39,"stem":40,"text":41,"__hash__":42},"stories\u002Fstories\u002F04-update.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1591799264318-7e6ef8ddb7ea?w=1080","UPDATE",{},4,"stories\u002F04-update","New cluster nodes arrived. Prepping for installation.","kyT60N5C6Re_jMonZbgNy0PbQhzXmUWxDbD0D_v43ts",{"id":44,"color":45,"extension":7,"image":6,"label":46,"link":6,"meta":47,"order":48,"stem":49,"text":50,"__hash__":51},"stories\u002Fstories\u002F05-setup.yml","#86868b","SETUP",{},5,"stories\u002F05-setup","Optimizing the telemetry pipeline for 1M req\u002Fs.","cPOBkzoyXsCmPgRO2d80Hj3vm4MP-6nAejtlQ5iuSzw",{"id":53,"color":6,"extension":7,"image":54,"label":55,"link":6,"meta":56,"order":57,"stem":58,"text":59,"__hash__":60},"stories\u002Fstories\u002F06-travel.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1560969184-10fe8719e047?w=1080","TRAVEL",{},6,"stories\u002F06-travel","Travel log — system architecture workshop in Berlin.","jnOxerdF6usAIHdR35Z-opx0LJAy9kZluXnZhtz62Z0",[62,64,66,68,70,72],{"id":5,"color":6,"extension":7,"image":8,"label":9,"link":10,"meta":63,"order":12,"stem":13,"text":14,"__hash__":15},{},{"id":17,"color":6,"extension":7,"image":18,"label":19,"link":6,"meta":65,"order":21,"stem":22,"text":23,"__hash__":24},{},{"id":26,"color":27,"extension":7,"image":6,"label":28,"link":6,"meta":67,"order":30,"stem":31,"text":32,"__hash__":33},{},{"id":35,"color":6,"extension":7,"image":36,"label":37,"link":6,"meta":69,"order":39,"stem":40,"text":41,"__hash__":42},{},{"id":44,"color":45,"extension":7,"image":6,"label":46,"link":6,"meta":71,"order":48,"stem":49,"text":50,"__hash__":51},{},{"id":53,"color":6,"extension":7,"image":54,"label":55,"link":6,"meta":73,"order":57,"stem":58,"text":59,"__hash__":60},{},[75,427,670,717,1061,1287,1575,1950,2074],{"id":76,"title":77,"body":78,"description":417,"extension":418,"hash":419,"liveUrl":6,"meta":420,"navigation":215,"order":48,"path":421,"rackBay":6,"rackStatus":6,"region":422,"seo":423,"stem":424,"thumbnail":425,"vault":215,"__hash__":426},"projects\u002Fprojects\u002Fcloudform-ui.md","Cloudform UI",{"type":79,"value":80,"toc":412},"minimark",[81,85,90,93,381,385,401,405,408],[82,83,84],"p",{},"Cloudform UI is the control plane interface for the private cloud. It provides operators with a live view of cluster state, service health, and resource utilisation without requiring SSH access to manager nodes.",[86,87,89],"h2",{"id":88},"stack","Stack",[82,91,92],{},"Built on Nuxt 4 with TypeScript throughout. The real-time data layer uses Server-Sent Events — a simpler and more reliable transport than WebSockets for unidirectional state streaming.",[94,95,100],"pre",{"className":96,"code":97,"language":98,"meta":99,"style":99},"language-typescript shiki shiki-themes vitesse-light","\u002F\u002F server\u002Froutes\u002Fstream\u002Fcluster.ts\nexport default defineEventHandler(async (event) => {\n  setHeader(event, 'Content-Type', 'text\u002Fevent-stream')\n  setHeader(event, 'Cache-Control', 'no-cache')\n\n  const send = (data: ClusterState) => {\n    event.node.res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n  }\n\n  const unsubscribe = clusterBus.subscribe(send)\n  event.node.req.on('close', unsubscribe)\n})\n","typescript","",[101,102,103,111,148,183,211,217,246,304,310,315,339,375],"code",{"__ignoreMap":99},[104,105,107],"span",{"class":106,"line":12},"line",[104,108,110],{"class":109},"s8zF2","\u002F\u002F server\u002Froutes\u002Fstream\u002Fcluster.ts\n",[104,112,113,117,120,124,128,132,135,139,142,145],{"class":106,"line":21},[104,114,116],{"class":115},"sbBg2","export",[104,118,119],{"class":115}," default",[104,121,123],{"class":122},"sySUi"," defineEventHandler",[104,125,127],{"class":126},"sYZai","(",[104,129,131],{"class":130},"si04Y","async",[104,133,134],{"class":126}," (",[104,136,138],{"class":137},"svycV","event",[104,140,141],{"class":126},")",[104,143,144],{"class":126}," =>",[104,146,147],{"class":126}," {\n",[104,149,150,153,155,157,160,164,168,171,173,175,178,180],{"class":106,"line":30},[104,151,152],{"class":122},"  setHeader",[104,154,127],{"class":126},[104,156,138],{"class":137},[104,158,159],{"class":126},",",[104,161,163],{"class":162},"sSP4y"," '",[104,165,167],{"class":166},"spphp","Content-Type",[104,169,170],{"class":162},"'",[104,172,159],{"class":126},[104,174,163],{"class":162},[104,176,177],{"class":166},"text\u002Fevent-stream",[104,179,170],{"class":162},[104,181,182],{"class":126},")\n",[104,184,185,187,189,191,193,195,198,200,202,204,207,209],{"class":106,"line":39},[104,186,152],{"class":122},[104,188,127],{"class":126},[104,190,138],{"class":137},[104,192,159],{"class":126},[104,194,163],{"class":162},[104,196,197],{"class":166},"Cache-Control",[104,199,170],{"class":162},[104,201,159],{"class":126},[104,203,163],{"class":162},[104,205,206],{"class":166},"no-cache",[104,208,170],{"class":162},[104,210,182],{"class":126},[104,212,213],{"class":106,"line":48},[104,214,216],{"emptyLinePlaceholder":215},true,"\n",[104,218,219,222,225,228,230,233,236,240,242,244],{"class":106,"line":57},[104,220,221],{"class":130},"  const ",[104,223,224],{"class":122},"send",[104,226,227],{"class":126}," =",[104,229,134],{"class":126},[104,231,232],{"class":137},"data",[104,234,235],{"class":126},": ",[104,237,239],{"class":238},"sUxyF","ClusterState",[104,241,141],{"class":126},[104,243,144],{"class":126},[104,245,147],{"class":126},[104,247,249,252,255,258,260,263,265,268,270,273,276,279,282,284,287,289,291,293,296,300,302],{"class":106,"line":248},7,[104,250,251],{"class":137},"    event",[104,253,254],{"class":126},".",[104,256,257],{"class":137},"node",[104,259,254],{"class":126},[104,261,262],{"class":137},"res",[104,264,254],{"class":126},[104,266,267],{"class":122},"write",[104,269,127],{"class":126},[104,271,272],{"class":162},"`",[104,274,275],{"class":166},"data: ",[104,277,278],{"class":115},"${",[104,280,281],{"class":166},"JSON",[104,283,254],{"class":126},[104,285,286],{"class":122},"stringify",[104,288,127],{"class":126},[104,290,232],{"class":166},[104,292,141],{"class":126},[104,294,295],{"class":115},"}",[104,297,299],{"class":298},"sEi1f","\\n\\n",[104,301,272],{"class":162},[104,303,182],{"class":126},[104,305,307],{"class":106,"line":306},8,[104,308,309],{"class":126},"  }\n",[104,311,313],{"class":106,"line":312},9,[104,314,216],{"emptyLinePlaceholder":215},[104,316,318,320,323,325,328,330,333,335,337],{"class":106,"line":317},10,[104,319,221],{"class":130},[104,321,322],{"class":137},"unsubscribe",[104,324,227],{"class":126},[104,326,327],{"class":137}," clusterBus",[104,329,254],{"class":126},[104,331,332],{"class":122},"subscribe",[104,334,127],{"class":126},[104,336,224],{"class":137},[104,338,182],{"class":126},[104,340,342,345,347,349,351,354,356,359,361,363,366,368,370,373],{"class":106,"line":341},11,[104,343,344],{"class":137},"  event",[104,346,254],{"class":126},[104,348,257],{"class":137},[104,350,254],{"class":126},[104,352,353],{"class":137},"req",[104,355,254],{"class":126},[104,357,358],{"class":122},"on",[104,360,127],{"class":126},[104,362,170],{"class":162},[104,364,365],{"class":166},"close",[104,367,170],{"class":162},[104,369,159],{"class":126},[104,371,372],{"class":137}," unsubscribe",[104,374,182],{"class":126},[104,376,378],{"class":106,"line":377},12,[104,379,380],{"class":126},"})\n",[86,382,384],{"id":383},"key-features","Key Features",[386,387,388,392,395,398],"ul",{},[389,390,391],"li",{},"Live node map with per-hypervisor CPU, memory, and network I\u002FO",[389,393,394],{},"Service deployment UI with rollback-to-previous-version in two clicks",[389,396,397],{},"Audit log viewer showing all API calls with the operator identity and timestamp",[389,399,400],{},"Alert acknowledgement workflow integrated with PagerDuty",[86,402,404],{"id":403},"status","Status",[82,406,407],{},"Internal tool, not publicly accessible. Actively developed — next milestone is adding a resource quota management view for multi-tenant environments.",[409,410,411],"style",{},"html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .sEi1f, html code.shiki .sEi1f{--shiki-default:#A65E2B}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":99,"searchDepth":21,"depth":21,"links":413},[414,415,416],{"id":88,"depth":21,"text":89},{"id":383,"depth":21,"text":384},{"id":403,"depth":21,"text":404},"A reactive management dashboard for OpenStack clusters built with Nuxt 3 and TypeScript, providing real-time visibility into cluster state.","md","M3N4O5",{},"\u002Fprojects\u002Fcloudform-ui","EU-WEST-2",{"title":77,"description":417},"projects\u002Fcloudform-ui","\u002Fimages\u002Fthumbnails\u002Fcloudform-ui.png","Iz2PSS-_TOi34gAgOQDGl_IgzGUXCxWrs4PMu5k7mWs",{"id":428,"title":429,"body":430,"description":661,"extension":418,"hash":662,"liveUrl":6,"meta":663,"navigation":215,"order":57,"path":664,"rackBay":6,"rackStatus":6,"region":665,"seo":666,"stem":667,"thumbnail":668,"vault":215,"__hash__":669},"projects\u002Fprojects\u002Fhyper-replica.md","Hyper Replica",{"type":79,"value":431,"toc":655},[432,435,439,442,446,449,640,644,647,649,652],[82,433,434],{},"Hyper Replica solves a specific problem: keeping dataset copies in sync across geographically distributed edge nodes where the WAN link is unreliable and latency-sensitive.",[86,436,438],{"id":437},"the-core-problem","The Core Problem",[82,440,441],{},"Traditional replication approaches assume a stable, high-bandwidth connection. Edge environments don't have that. A replication protocol that works perfectly in a data centre will stall, buffer, and eventually corrupt when the underlying transport is a flaky 4G link with 200ms RTT spikes.",[86,443,445],{"id":444},"the-protocol","The Protocol",[82,447,448],{},"Hyper Replica uses a delta-based sync protocol over a persistent WebSocket connection. Each edge node maintains a vector clock. On reconnect after a partition, the nodes exchange vector clocks and compute the minimal changeset required to reach convergence — no full resync needed.",[94,450,454],{"className":451,"code":452,"language":453,"meta":99,"style":99},"language-go shiki shiki-themes vitesse-light","type VectorClock map[NodeID]uint64\n\nfunc (local VectorClock) Delta(remote VectorClock) []Event {\n    var missing []Event\n    for nodeID, remoteSeq := range remote {\n        if local[nodeID] \u003C remoteSeq {\n            missing = append(missing, eventLog.Since(nodeID, local[nodeID]))\n        }\n    }\n    return missing\n}\n","go",[101,455,456,479,483,518,531,555,577,617,622,627,635],{"__ignoreMap":99},[104,457,458,461,464,467,470,473,476],{"class":106,"line":12},[104,459,460],{"class":115},"type",[104,462,463],{"class":238}," VectorClock",[104,465,466],{"class":115}," map",[104,468,469],{"class":126},"[",[104,471,472],{"class":238},"NodeID",[104,474,475],{"class":126},"]",[104,477,478],{"class":130},"uint64\n",[104,480,481],{"class":106,"line":21},[104,482,216],{"emptyLinePlaceholder":215},[104,484,485,488,490,493,496,498,501,503,506,508,510,513,516],{"class":106,"line":30},[104,486,487],{"class":115},"func",[104,489,134],{"class":126},[104,491,492],{"class":137},"local ",[104,494,495],{"class":238},"VectorClock",[104,497,141],{"class":126},[104,499,500],{"class":122}," Delta",[104,502,127],{"class":126},[104,504,505],{"class":137},"remote",[104,507,463],{"class":238},[104,509,141],{"class":126},[104,511,512],{"class":126}," []",[104,514,515],{"class":238},"Event",[104,517,147],{"class":126},[104,519,520,523,526,528],{"class":106,"line":39},[104,521,522],{"class":115},"    var",[104,524,525],{"class":137}," missing",[104,527,512],{"class":126},[104,529,530],{"class":238},"Event\n",[104,532,533,536,539,541,544,547,550,553],{"class":106,"line":48},[104,534,535],{"class":115},"    for",[104,537,538],{"class":137}," nodeID",[104,540,159],{"class":126},[104,542,543],{"class":137}," remoteSeq",[104,545,546],{"class":126}," :=",[104,548,549],{"class":115}," range",[104,551,552],{"class":137}," remote",[104,554,147],{"class":126},[104,556,557,560,563,565,568,570,573,575],{"class":106,"line":57},[104,558,559],{"class":115},"        if",[104,561,562],{"class":137}," local",[104,564,469],{"class":126},[104,566,567],{"class":137},"nodeID",[104,569,475],{"class":126},[104,571,572],{"class":130}," \u003C",[104,574,543],{"class":137},[104,576,147],{"class":126},[104,578,579,582,584,587,589,592,594,597,599,602,604,606,608,610,612,614],{"class":106,"line":248},[104,580,581],{"class":137},"            missing",[104,583,227],{"class":126},[104,585,586],{"class":122}," append",[104,588,127],{"class":126},[104,590,591],{"class":137},"missing",[104,593,159],{"class":126},[104,595,596],{"class":137}," eventLog",[104,598,254],{"class":126},[104,600,601],{"class":122},"Since",[104,603,127],{"class":126},[104,605,567],{"class":137},[104,607,159],{"class":126},[104,609,562],{"class":137},[104,611,469],{"class":126},[104,613,567],{"class":137},[104,615,616],{"class":126},"]))\n",[104,618,619],{"class":106,"line":306},[104,620,621],{"class":126},"        }\n",[104,623,624],{"class":106,"line":312},[104,625,626],{"class":126},"    }\n",[104,628,629,632],{"class":106,"line":317},[104,630,631],{"class":115},"    return",[104,633,634],{"class":137}," missing\n",[104,636,637],{"class":106,"line":341},[104,638,639],{"class":126},"}\n",[86,641,643],{"id":642},"results","Results",[82,645,646],{},"In a 3-node test across regions (London, São Paulo, Singapore), 95th-percentile replication latency was 38ms under normal conditions. After a simulated 10-minute partition, full convergence was achieved in under 2 seconds.",[86,648,404],{"id":403},[82,650,651],{},"In active development. The core protocol is stable; the backplane management UI and operational tooling are ongoing work.",[409,653,654],{},"html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}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":99,"searchDepth":21,"depth":21,"links":656},[657,658,659,660],{"id":437,"depth":21,"text":438},{"id":444,"depth":21,"text":445},{"id":642,"depth":21,"text":643},{"id":403,"depth":21,"text":404},"Low-latency data replication across edge nodes using WebSockets and dedicated backplanes, targeting sub-50ms propagation.","P6Q7R8",{},"\u002Fprojects\u002Fhyper-replica","US-EAST-1",{"title":429,"description":661},"projects\u002Fhyper-replica","\u002Fimages\u002Fthumbnails\u002Fhyper-replica.png","l9Oex6_CqDWkefhwIgtTC2ZxSH5gSCnVHceG0vgeuBc",{"id":671,"title":672,"body":673,"description":705,"extension":418,"hash":706,"liveUrl":6,"meta":707,"navigation":215,"order":39,"path":708,"rackBay":709,"rackStatus":710,"region":711,"seo":712,"stem":713,"thumbnail":714,"vault":715,"__hash__":716},"projects\u002Fprojects\u002Flegacy-monitor.md","Legacy Monitor",{"type":79,"value":674,"toc":700},[675,678,682,685,688,692,695,697],[82,676,677],{},"Legacy Monitor was the first attempt at building an observability layer for the cluster. It was a Python script that ran as a cron job, queried the OpenStack API, and wrote metrics to a flat CSV file.",[86,679,681],{"id":680},"why-it-was-retired","Why It Was Retired",[82,683,684],{},"The architecture was fundamentally incompatible with scaling. Every metric query was synchronous and blocking. Adding a new hypervisor node increased the cron runtime linearly. At 40 nodes it took longer to collect metrics than the cron interval — meaning metrics were always stale.",[82,686,687],{},"The final straw was a 6-hour outage that the monitor completely failed to surface because it had silently timed out on an unresponsive node while marking it healthy.",[86,689,691],{"id":690},"what-it-taught-us","What It Taught Us",[82,693,694],{},"The failure modes of Legacy Monitor directly shaped Production Engine's design: async metric collection from the start, dead-man's-switch alerts for collector health, and immutable audit logs for every state change.",[86,696,404],{"id":403},[82,698,699],{},"Archived. The codebase is preserved for historical reference but the binary is not running. Replaced by Production Engine in Q2 2025.",{"title":99,"searchDepth":21,"depth":21,"links":701},[702,703,704],{"id":680,"depth":21,"text":681},{"id":690,"depth":21,"text":691},{"id":403,"depth":21,"text":404},"The original hand-rolled infrastructure monitoring system, now decommissioned and replaced by Production Engine.","J0K1L2",{},"\u002Fprojects\u002Flegacy-monitor","BAY 03","offline","AF-SOUTH-1",{"title":672,"description":705},"projects\u002Flegacy-monitor","\u002Fimages\u002Fthumbnails\u002Flegacy-monitor.png",false,"COeQJfFwO5TJJlgs1PCKaOyTgclEx2NhFONFnl95a8s",{"id":718,"title":719,"body":720,"description":1052,"extension":418,"hash":1053,"liveUrl":6,"meta":1054,"navigation":215,"order":248,"path":1055,"rackBay":6,"rackStatus":6,"region":1056,"seo":1057,"stem":1058,"thumbnail":1059,"vault":715,"__hash__":1060},"projects\u002Fprojects\u002Fmetric-stream.md","Metric Stream",{"type":79,"value":721,"toc":1047},[722,725,729,740,1032,1036,1039,1041,1044],[82,723,724],{},"Metric Stream is a purpose-built telemetry ingestion pipeline designed for environments where sensor data arrives faster than a traditional time-series database can absorb it.",[86,726,728],{"id":727},"architecture","Architecture",[82,730,731,732,735,736,739],{},"Producers write events to Redis Streams using ",[101,733,734],{},"XADD",". A pool of Go consumer goroutines reads with ",[101,737,738],{},"XREADGROUP",", processes and enriches the events (unit conversion, anomaly flagging, threshold evaluation), and forwards the result to the downstream store.",[94,741,743],{"className":451,"code":742,"language":453,"meta":99,"style":99},"func (w *Worker) Run(ctx context.Context) error {\n    for {\n        entries, err := w.redis.XReadGroup(ctx, &redis.XReadGroupArgs{\n            Group:    w.group,\n            Consumer: w.id,\n            Streams:  []string{w.stream, \">\"},\n            Count:    100,\n            Block:    time.Second,\n        })\n        if err != nil { continue }\n        for _, e := range entries[0].Messages {\n            w.process(ctx, e)\n        }\n    }\n}\n",[101,744,745,785,791,835,854,870,908,921,938,943,964,997,1017,1022,1027],{"__ignoreMap":99},[104,746,747,749,751,754,757,760,762,765,767,770,773,775,778,780,783],{"class":106,"line":12},[104,748,487],{"class":115},[104,750,134],{"class":126},[104,752,753],{"class":137},"w ",[104,755,756],{"class":130},"*",[104,758,759],{"class":238},"Worker",[104,761,141],{"class":126},[104,763,764],{"class":122}," Run",[104,766,127],{"class":126},[104,768,769],{"class":137},"ctx",[104,771,772],{"class":238}," context",[104,774,254],{"class":126},[104,776,777],{"class":238},"Context",[104,779,141],{"class":126},[104,781,782],{"class":130}," error",[104,784,147],{"class":126},[104,786,787,789],{"class":106,"line":21},[104,788,535],{"class":115},[104,790,147],{"class":126},[104,792,793,796,798,801,803,806,808,811,813,816,818,820,822,825,827,829,832],{"class":106,"line":30},[104,794,795],{"class":137},"        entries",[104,797,159],{"class":126},[104,799,800],{"class":137}," err",[104,802,546],{"class":126},[104,804,805],{"class":137}," w",[104,807,254],{"class":126},[104,809,810],{"class":137},"redis",[104,812,254],{"class":126},[104,814,815],{"class":122},"XReadGroup",[104,817,127],{"class":126},[104,819,769],{"class":137},[104,821,159],{"class":126},[104,823,824],{"class":130}," &",[104,826,810],{"class":238},[104,828,254],{"class":126},[104,830,831],{"class":238},"XReadGroupArgs",[104,833,834],{"class":126},"{\n",[104,836,837,840,843,846,848,851],{"class":106,"line":39},[104,838,839],{"class":137},"            Group",[104,841,842],{"class":126},":",[104,844,845],{"class":137},"    w",[104,847,254],{"class":126},[104,849,850],{"class":137},"group",[104,852,853],{"class":126},",\n",[104,855,856,859,861,863,865,868],{"class":106,"line":48},[104,857,858],{"class":137},"            Consumer",[104,860,842],{"class":126},[104,862,805],{"class":137},[104,864,254],{"class":126},[104,866,867],{"class":137},"id",[104,869,853],{"class":126},[104,871,872,875,877,880,883,886,889,891,894,896,899,902,905],{"class":106,"line":57},[104,873,874],{"class":137},"            Streams",[104,876,842],{"class":126},[104,878,879],{"class":126},"  []",[104,881,882],{"class":130},"string",[104,884,885],{"class":126},"{",[104,887,888],{"class":137},"w",[104,890,254],{"class":126},[104,892,893],{"class":137},"stream",[104,895,159],{"class":126},[104,897,898],{"class":162}," \"",[104,900,901],{"class":166},">",[104,903,904],{"class":162},"\"",[104,906,907],{"class":126},"},\n",[104,909,910,913,915,919],{"class":106,"line":248},[104,911,912],{"class":137},"            Count",[104,914,842],{"class":126},[104,916,918],{"class":917},"s-TwI","    100",[104,920,853],{"class":126},[104,922,923,926,928,931,933,936],{"class":106,"line":306},[104,924,925],{"class":137},"            Block",[104,927,842],{"class":126},[104,929,930],{"class":137},"    time",[104,932,254],{"class":126},[104,934,935],{"class":137},"Second",[104,937,853],{"class":126},[104,939,940],{"class":106,"line":312},[104,941,942],{"class":126},"        })\n",[104,944,945,947,949,952,955,958,961],{"class":106,"line":317},[104,946,559],{"class":115},[104,948,800],{"class":137},[104,950,951],{"class":130}," !=",[104,953,954],{"class":130}," nil",[104,956,957],{"class":126}," {",[104,959,960],{"class":115}," continue",[104,962,963],{"class":126}," }\n",[104,965,966,969,972,974,977,979,981,984,986,989,992,995],{"class":106,"line":341},[104,967,968],{"class":115},"        for",[104,970,971],{"class":137}," _",[104,973,159],{"class":126},[104,975,976],{"class":137}," e",[104,978,546],{"class":126},[104,980,549],{"class":115},[104,982,983],{"class":137}," entries",[104,985,469],{"class":126},[104,987,988],{"class":917},"0",[104,990,991],{"class":126},"].",[104,993,994],{"class":137},"Messages",[104,996,147],{"class":126},[104,998,999,1002,1004,1007,1009,1011,1013,1015],{"class":106,"line":377},[104,1000,1001],{"class":137},"            w",[104,1003,254],{"class":126},[104,1005,1006],{"class":122},"process",[104,1008,127],{"class":126},[104,1010,769],{"class":137},[104,1012,159],{"class":126},[104,1014,976],{"class":137},[104,1016,182],{"class":126},[104,1018,1020],{"class":106,"line":1019},13,[104,1021,621],{"class":126},[104,1023,1025],{"class":106,"line":1024},14,[104,1026,626],{"class":126},[104,1028,1030],{"class":106,"line":1029},15,[104,1031,639],{"class":126},[86,1033,1035],{"id":1034},"throughput","Throughput",[82,1037,1038],{},"In benchmarks on 4 consumer instances, the pipeline sustains 52k events\u002Fsecond with P99 end-to-end latency of 18ms. The Redis consumer group ensures exactly-once delivery semantics even if a worker crashes mid-batch.",[86,1040,404],{"id":403},[82,1042,1043],{},"Deployed in a staging environment with production rollout planned for Q3 2026. Pending load testing at 100k events\u002Fsecond target.",[409,1045,1046],{},"html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}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":99,"searchDepth":21,"depth":21,"links":1048},[1049,1050,1051],{"id":727,"depth":21,"text":728},{"id":1034,"depth":21,"text":1035},{"id":403,"depth":21,"text":404},"Real-time telemetry pipeline for high-throughput sensor data using Go and Redis Streams, processing over 50k events per second.","S9T0U1",{},"\u002Fprojects\u002Fmetric-stream","EU-WEST-1",{"title":719,"description":1052},"projects\u002Fmetric-stream","\u002Fimages\u002Fthumbnails\u002Fmetric-stream.png","MugEKAYZVkkgQBZB14V9locuNfQvVoHgaGIWQRVeyFw",{"id":1062,"title":1063,"body":1064,"description":1278,"extension":418,"hash":1279,"liveUrl":6,"meta":1280,"navigation":215,"order":306,"path":1281,"rackBay":6,"rackStatus":6,"region":1282,"seo":1283,"stem":1284,"thumbnail":1285,"vault":715,"__hash__":1286},"projects\u002Fprojects\u002Foak-search.md","Oak Search",{"type":79,"value":1065,"toc":1272},[1066,1069,1073,1076,1080,1251,1257,1261,1264,1266,1269],[82,1067,1068],{},"Oak Search is a thin orchestration layer over Elasticsearch that adds per-tenant index isolation, query rewriting for access control, and a structured query DSL consumable from TypeScript.",[86,1070,1072],{"id":1071},"the-problem-it-solves","The Problem It Solves",[82,1074,1075],{},"Multi-tenant search has a nasty default: if tenants share an index, a misconfigured query can return documents from the wrong tenant. Oak Search enforces isolation at the query layer — every search request is rewritten to include a tenant filter before hitting Elasticsearch.",[86,1077,1079],{"id":1078},"query-rewriting","Query Rewriting",[94,1081,1083],{"className":96,"code":1082,"language":98,"meta":99,"style":99},"const search = async (tenantId: string, query: SearchQuery) => {\n  const safe: ElasticQuery = {\n    bool: {\n      must: [toElasticQuery(query)],\n      filter: [{ term: { tenant_id: tenantId } }], \u002F\u002F injected, not user-controlled\n    },\n  }\n  return elastic.search({ index: 'documents', body: { query: safe } })\n}\n",[101,1084,1085,1123,1139,1148,1167,1194,1199,1203,1247],{"__ignoreMap":99},[104,1086,1087,1090,1093,1095,1098,1100,1103,1105,1107,1109,1112,1114,1117,1119,1121],{"class":106,"line":12},[104,1088,1089],{"class":130},"const ",[104,1091,1092],{"class":122},"search",[104,1094,227],{"class":126},[104,1096,1097],{"class":130}," async ",[104,1099,127],{"class":126},[104,1101,1102],{"class":137},"tenantId",[104,1104,235],{"class":126},[104,1106,882],{"class":238},[104,1108,159],{"class":126},[104,1110,1111],{"class":137}," query",[104,1113,235],{"class":126},[104,1115,1116],{"class":238},"SearchQuery",[104,1118,141],{"class":126},[104,1120,144],{"class":126},[104,1122,147],{"class":126},[104,1124,1125,1127,1130,1132,1135,1137],{"class":106,"line":21},[104,1126,221],{"class":130},[104,1128,1129],{"class":137},"safe",[104,1131,235],{"class":126},[104,1133,1134],{"class":238},"ElasticQuery",[104,1136,227],{"class":126},[104,1138,147],{"class":126},[104,1140,1141,1145],{"class":106,"line":30},[104,1142,1144],{"class":1143},"su6XF","    bool",[104,1146,1147],{"class":126},": {\n",[104,1149,1150,1153,1156,1159,1161,1164],{"class":106,"line":39},[104,1151,1152],{"class":1143},"      must",[104,1154,1155],{"class":126},": [",[104,1157,1158],{"class":122},"toElasticQuery",[104,1160,127],{"class":126},[104,1162,1163],{"class":137},"query",[104,1165,1166],{"class":126},")],\n",[104,1168,1169,1172,1175,1178,1181,1184,1186,1188,1191],{"class":106,"line":48},[104,1170,1171],{"class":1143},"      filter",[104,1173,1174],{"class":126},": [{ ",[104,1176,1177],{"class":1143},"term",[104,1179,1180],{"class":126},": { ",[104,1182,1183],{"class":1143},"tenant_id",[104,1185,235],{"class":126},[104,1187,1102],{"class":137},[104,1189,1190],{"class":126}," } }], ",[104,1192,1193],{"class":109},"\u002F\u002F injected, not user-controlled\n",[104,1195,1196],{"class":106,"line":57},[104,1197,1198],{"class":126},"    },\n",[104,1200,1201],{"class":106,"line":248},[104,1202,309],{"class":126},[104,1204,1205,1208,1211,1213,1215,1218,1221,1223,1225,1228,1230,1233,1236,1238,1240,1242,1244],{"class":106,"line":306},[104,1206,1207],{"class":115},"  return",[104,1209,1210],{"class":137}," elastic",[104,1212,254],{"class":126},[104,1214,1092],{"class":122},[104,1216,1217],{"class":126},"({ ",[104,1219,1220],{"class":1143},"index",[104,1222,235],{"class":126},[104,1224,170],{"class":162},[104,1226,1227],{"class":166},"documents",[104,1229,170],{"class":162},[104,1231,1232],{"class":126},", ",[104,1234,1235],{"class":1143},"body",[104,1237,1180],{"class":126},[104,1239,1163],{"class":1143},[104,1241,235],{"class":126},[104,1243,1129],{"class":137},[104,1245,1246],{"class":126}," } })\n",[104,1248,1249],{"class":106,"line":312},[104,1250,639],{"class":126},[82,1252,1253,1254,1256],{},"The ",[101,1255,1183],{}," filter is always injected server-side. The caller has no way to omit or override it.",[86,1258,1260],{"id":1259},"performance","Performance",[82,1262,1263],{},"With a 10M document corpus split across 8 tenants, median search latency is 42ms. The Elasticsearch cluster uses ILM policies to automatically move older indices to frozen tier storage, keeping hot-tier costs stable as the dataset grows.",[86,1265,404],{"id":403},[82,1267,1268],{},"In production for two tenants. Expanding to a third tenant in the next sprint.",[409,1270,1271],{},"html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .su6XF, html code.shiki .su6XF{--shiki-default:#998418}html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}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":99,"searchDepth":21,"depth":21,"links":1273},[1274,1275,1276,1277],{"id":1071,"depth":21,"text":1072},{"id":1078,"depth":21,"text":1079},{"id":1259,"depth":21,"text":1260},{"id":403,"depth":21,"text":404},"Distributed search engine layer built on top of Elasticsearch, providing sub-100ms full-text search across multi-tenant document stores.","V2W3X4",{},"\u002Fprojects\u002Foak-search","AP-SOUTH-1",{"title":1063,"description":1278},"projects\u002Foak-search","\u002Fimages\u002Fthumbnails\u002Foak-search.png","Xpd7AeVt0jL-b_KJf6o-RcvzbbsmSsfy-GijgtpMNDA",{"id":1288,"title":1289,"body":1290,"description":1565,"extension":418,"hash":1566,"liveUrl":6,"meta":1567,"navigation":215,"order":12,"path":1568,"rackBay":1569,"rackStatus":1570,"region":711,"seo":1571,"stem":1572,"thumbnail":1573,"vault":215,"__hash__":1574},"projects\u002Fprojects\u002Fproduction-engine.md","Production Engine",{"type":79,"value":1291,"toc":1560},[1292,1295,1297,1300,1545,1549,1552,1554,1557],[82,1293,1294],{},"Production Engine is the core operational daemon running the KNUST private cloud. It continuously monitors cluster health, collects real-time telemetry from all hypervisors, and triggers autoscaling events when load thresholds are breached.",[86,1296,728],{"id":727},[82,1298,1299],{},"The daemon runs as a privileged service on each manager node, communicating with the OpenStack API to provision or decommission compute resources. State is persisted to an embedded SQLite store with a write-ahead log for crash recovery.",[94,1301,1303],{"className":451,"code":1302,"language":453,"meta":99,"style":99},"type ClusterMonitor struct {\n    client    *openstack.Client\n    db        *sql.DB\n    threshold ScalingThreshold\n}\n\nfunc (m *ClusterMonitor) EvaluateLoad(ctx context.Context) error {\n    metrics, err := m.client.GetClusterMetrics(ctx)\n    if err != nil {\n        return fmt.Errorf(\"metrics fetch: %w\", err)\n    }\n    if metrics.CPUPercent > m.threshold.ScaleUp {\n        return m.scaleOut(ctx, 1)\n    }\n    return nil\n}\n",[101,1304,1305,1317,1333,1349,1357,1361,1365,1400,1430,1443,1474,1478,1507,1529,1533,1540],{"__ignoreMap":99},[104,1306,1307,1309,1312,1315],{"class":106,"line":12},[104,1308,460],{"class":115},[104,1310,1311],{"class":238}," ClusterMonitor",[104,1313,1314],{"class":115}," struct",[104,1316,147],{"class":126},[104,1318,1319,1322,1325,1328,1330],{"class":106,"line":21},[104,1320,1321],{"class":137},"    client",[104,1323,1324],{"class":130},"    *",[104,1326,1327],{"class":238},"openstack",[104,1329,254],{"class":126},[104,1331,1332],{"class":238},"Client\n",[104,1334,1335,1338,1341,1344,1346],{"class":106,"line":30},[104,1336,1337],{"class":137},"    db",[104,1339,1340],{"class":130},"        *",[104,1342,1343],{"class":238},"sql",[104,1345,254],{"class":126},[104,1347,1348],{"class":238},"DB\n",[104,1350,1351,1354],{"class":106,"line":39},[104,1352,1353],{"class":137},"    threshold",[104,1355,1356],{"class":238}," ScalingThreshold\n",[104,1358,1359],{"class":106,"line":48},[104,1360,639],{"class":126},[104,1362,1363],{"class":106,"line":57},[104,1364,216],{"emptyLinePlaceholder":215},[104,1366,1367,1369,1371,1374,1376,1379,1381,1384,1386,1388,1390,1392,1394,1396,1398],{"class":106,"line":248},[104,1368,487],{"class":115},[104,1370,134],{"class":126},[104,1372,1373],{"class":137},"m ",[104,1375,756],{"class":130},[104,1377,1378],{"class":238},"ClusterMonitor",[104,1380,141],{"class":126},[104,1382,1383],{"class":122}," EvaluateLoad",[104,1385,127],{"class":126},[104,1387,769],{"class":137},[104,1389,772],{"class":238},[104,1391,254],{"class":126},[104,1393,777],{"class":238},[104,1395,141],{"class":126},[104,1397,782],{"class":130},[104,1399,147],{"class":126},[104,1401,1402,1405,1407,1409,1411,1414,1416,1419,1421,1424,1426,1428],{"class":106,"line":306},[104,1403,1404],{"class":137},"    metrics",[104,1406,159],{"class":126},[104,1408,800],{"class":137},[104,1410,546],{"class":126},[104,1412,1413],{"class":137}," m",[104,1415,254],{"class":126},[104,1417,1418],{"class":137},"client",[104,1420,254],{"class":126},[104,1422,1423],{"class":122},"GetClusterMetrics",[104,1425,127],{"class":126},[104,1427,769],{"class":137},[104,1429,182],{"class":126},[104,1431,1432,1435,1437,1439,1441],{"class":106,"line":312},[104,1433,1434],{"class":115},"    if",[104,1436,800],{"class":137},[104,1438,951],{"class":130},[104,1440,954],{"class":130},[104,1442,147],{"class":126},[104,1444,1445,1448,1451,1453,1456,1458,1460,1463,1466,1468,1470,1472],{"class":106,"line":317},[104,1446,1447],{"class":115},"        return",[104,1449,1450],{"class":137}," fmt",[104,1452,254],{"class":126},[104,1454,1455],{"class":122},"Errorf",[104,1457,127],{"class":126},[104,1459,904],{"class":162},[104,1461,1462],{"class":166},"metrics fetch: ",[104,1464,1465],{"class":298},"%w",[104,1467,904],{"class":162},[104,1469,159],{"class":126},[104,1471,800],{"class":137},[104,1473,182],{"class":126},[104,1475,1476],{"class":106,"line":341},[104,1477,626],{"class":126},[104,1479,1480,1482,1485,1487,1490,1493,1495,1497,1500,1502,1505],{"class":106,"line":377},[104,1481,1434],{"class":115},[104,1483,1484],{"class":137}," metrics",[104,1486,254],{"class":126},[104,1488,1489],{"class":137},"CPUPercent",[104,1491,1492],{"class":130}," >",[104,1494,1413],{"class":137},[104,1496,254],{"class":126},[104,1498,1499],{"class":137},"threshold",[104,1501,254],{"class":126},[104,1503,1504],{"class":137},"ScaleUp",[104,1506,147],{"class":126},[104,1508,1509,1511,1513,1515,1518,1520,1522,1524,1527],{"class":106,"line":1019},[104,1510,1447],{"class":115},[104,1512,1413],{"class":137},[104,1514,254],{"class":126},[104,1516,1517],{"class":122},"scaleOut",[104,1519,127],{"class":126},[104,1521,769],{"class":137},[104,1523,159],{"class":126},[104,1525,1526],{"class":917}," 1",[104,1528,182],{"class":126},[104,1530,1531],{"class":106,"line":1024},[104,1532,626],{"class":126},[104,1534,1535,1537],{"class":106,"line":1029},[104,1536,631],{"class":115},[104,1538,1539],{"class":130}," nil\n",[104,1541,1543],{"class":106,"line":1542},16,[104,1544,639],{"class":126},[86,1546,1548],{"id":1547},"observability","Observability",[82,1550,1551],{},"All scaling events are emitted as structured logs consumed by a Loki instance. Grafana dashboards surface cluster utilisation across a 7-day rolling window with per-hypervisor breakdowns.",[86,1553,404],{"id":403},[82,1555,1556],{},"Live in production, handling approximately 200 VMs across 3 availability zones. Average response time for a scale-out event is under 90 seconds from threshold breach to node registration.",[409,1558,1559],{},"html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .sEi1f, html code.shiki .sEi1f{--shiki-default:#A65E2B}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}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":99,"searchDepth":21,"depth":21,"links":1561},[1562,1563,1564],{"id":727,"depth":21,"text":728},{"id":1547,"depth":21,"text":1548},{"id":403,"depth":21,"text":404},"A custom-built live monitoring and autoscaling daemon for the KNUST Cloud, ensuring high availability across all cluster nodes.","A1B2C3",{},"\u002Fprojects\u002Fproduction-engine","BAY 01","live",{"title":1289,"description":1565},"projects\u002Fproduction-engine","\u002Fimages\u002Fthumbnails\u002Fproduction-engine.png","KC6VHsNbYyyWtTidhS6I8zgUmAdY2fxxqSOzaRqsqsg",{"id":1576,"title":1577,"body":1578,"description":1941,"extension":418,"hash":1942,"liveUrl":6,"meta":1943,"navigation":215,"order":21,"path":1944,"rackBay":1945,"rackStatus":1570,"region":1056,"seo":1946,"stem":1947,"thumbnail":1948,"vault":215,"__hash__":1949},"projects\u002Fprojects\u002Fservice-autoscaler.md","Service Autoscaler",{"type":79,"value":1579,"toc":1936},[1580,1583,1587,1590,1917,1921,1928,1930,1933],[82,1581,1582],{},"Service Autoscaler is a lightweight Go daemon that subscribes to a Prometheus metrics stream and drives Docker Swarm service scaling decisions without any manual intervention.",[86,1584,1586],{"id":1585},"how-it-works","How It Works",[82,1588,1589],{},"The autoscaler queries a set of configurable PromQL expressions on a 15-second tick. When a metric crosses a defined threshold, it calls the Swarm API to update the target replica count. Scale-down events include a stabilisation window to prevent thrashing.",[94,1591,1593],{"className":451,"code":1592,"language":453,"meta":99,"style":99},"func (a *Autoscaler) Tick(ctx context.Context) {\n    for _, rule := range a.rules {\n        val, err := a.prom.QueryInstant(ctx, rule.Query)\n        if err != nil || val == nil {\n            continue\n        }\n        if *val > rule.ScaleUpThreshold {\n            a.swarm.Scale(ctx, rule.Service, rule.CurrentReplicas+rule.Step)\n        } else if *val \u003C rule.ScaleDownThreshold && rule.StabilisedFor(5*time.Minute) {\n            a.swarm.Scale(ctx, rule.Service, max(rule.MinReplicas, rule.CurrentReplicas-rule.Step))\n        }\n    }\n}\n",[101,1594,1595,1628,1653,1691,1714,1719,1723,1744,1794,1847,1905,1909,1913],{"__ignoreMap":99},[104,1596,1597,1599,1601,1604,1606,1609,1611,1614,1616,1618,1620,1622,1624,1626],{"class":106,"line":12},[104,1598,487],{"class":115},[104,1600,134],{"class":126},[104,1602,1603],{"class":137},"a ",[104,1605,756],{"class":130},[104,1607,1608],{"class":238},"Autoscaler",[104,1610,141],{"class":126},[104,1612,1613],{"class":122}," Tick",[104,1615,127],{"class":126},[104,1617,769],{"class":137},[104,1619,772],{"class":238},[104,1621,254],{"class":126},[104,1623,777],{"class":238},[104,1625,141],{"class":126},[104,1627,147],{"class":126},[104,1629,1630,1632,1634,1636,1639,1641,1643,1646,1648,1651],{"class":106,"line":21},[104,1631,535],{"class":115},[104,1633,971],{"class":137},[104,1635,159],{"class":126},[104,1637,1638],{"class":137}," rule",[104,1640,546],{"class":126},[104,1642,549],{"class":115},[104,1644,1645],{"class":137}," a",[104,1647,254],{"class":126},[104,1649,1650],{"class":137},"rules",[104,1652,147],{"class":126},[104,1654,1655,1658,1660,1662,1664,1666,1668,1671,1673,1676,1678,1680,1682,1684,1686,1689],{"class":106,"line":30},[104,1656,1657],{"class":137},"        val",[104,1659,159],{"class":126},[104,1661,800],{"class":137},[104,1663,546],{"class":126},[104,1665,1645],{"class":137},[104,1667,254],{"class":126},[104,1669,1670],{"class":137},"prom",[104,1672,254],{"class":126},[104,1674,1675],{"class":122},"QueryInstant",[104,1677,127],{"class":126},[104,1679,769],{"class":137},[104,1681,159],{"class":126},[104,1683,1638],{"class":137},[104,1685,254],{"class":126},[104,1687,1688],{"class":137},"Query",[104,1690,182],{"class":126},[104,1692,1693,1695,1697,1699,1701,1704,1707,1710,1712],{"class":106,"line":39},[104,1694,559],{"class":115},[104,1696,800],{"class":137},[104,1698,951],{"class":130},[104,1700,954],{"class":130},[104,1702,1703],{"class":130}," ||",[104,1705,1706],{"class":137}," val",[104,1708,1709],{"class":130}," ==",[104,1711,954],{"class":130},[104,1713,147],{"class":126},[104,1715,1716],{"class":106,"line":48},[104,1717,1718],{"class":115},"            continue\n",[104,1720,1721],{"class":106,"line":57},[104,1722,621],{"class":126},[104,1724,1725,1727,1730,1733,1735,1737,1739,1742],{"class":106,"line":248},[104,1726,559],{"class":115},[104,1728,1729],{"class":130}," *",[104,1731,1732],{"class":137},"val",[104,1734,1492],{"class":130},[104,1736,1638],{"class":137},[104,1738,254],{"class":126},[104,1740,1741],{"class":137},"ScaleUpThreshold",[104,1743,147],{"class":126},[104,1745,1746,1749,1751,1754,1756,1759,1761,1763,1765,1767,1769,1772,1774,1776,1778,1781,1784,1787,1789,1792],{"class":106,"line":306},[104,1747,1748],{"class":137},"            a",[104,1750,254],{"class":126},[104,1752,1753],{"class":137},"swarm",[104,1755,254],{"class":126},[104,1757,1758],{"class":122},"Scale",[104,1760,127],{"class":126},[104,1762,769],{"class":137},[104,1764,159],{"class":126},[104,1766,1638],{"class":137},[104,1768,254],{"class":126},[104,1770,1771],{"class":137},"Service",[104,1773,159],{"class":126},[104,1775,1638],{"class":137},[104,1777,254],{"class":126},[104,1779,1780],{"class":137},"CurrentReplicas",[104,1782,1783],{"class":130},"+",[104,1785,1786],{"class":137},"rule",[104,1788,254],{"class":126},[104,1790,1791],{"class":137},"Step",[104,1793,182],{"class":126},[104,1795,1796,1799,1802,1805,1807,1809,1811,1813,1815,1818,1821,1823,1825,1828,1830,1833,1835,1838,1840,1843,1845],{"class":106,"line":312},[104,1797,1798],{"class":126},"        }",[104,1800,1801],{"class":115}," else",[104,1803,1804],{"class":115}," if",[104,1806,1729],{"class":130},[104,1808,1732],{"class":137},[104,1810,572],{"class":130},[104,1812,1638],{"class":137},[104,1814,254],{"class":126},[104,1816,1817],{"class":137},"ScaleDownThreshold",[104,1819,1820],{"class":130}," &&",[104,1822,1638],{"class":137},[104,1824,254],{"class":126},[104,1826,1827],{"class":122},"StabilisedFor",[104,1829,127],{"class":126},[104,1831,1832],{"class":917},"5",[104,1834,756],{"class":130},[104,1836,1837],{"class":137},"time",[104,1839,254],{"class":126},[104,1841,1842],{"class":137},"Minute",[104,1844,141],{"class":126},[104,1846,147],{"class":126},[104,1848,1849,1851,1853,1855,1857,1859,1861,1863,1865,1867,1869,1871,1873,1876,1878,1880,1882,1885,1887,1889,1891,1893,1896,1898,1900,1902],{"class":106,"line":317},[104,1850,1748],{"class":137},[104,1852,254],{"class":126},[104,1854,1753],{"class":137},[104,1856,254],{"class":126},[104,1858,1758],{"class":122},[104,1860,127],{"class":126},[104,1862,769],{"class":137},[104,1864,159],{"class":126},[104,1866,1638],{"class":137},[104,1868,254],{"class":126},[104,1870,1771],{"class":137},[104,1872,159],{"class":126},[104,1874,1875],{"class":122}," max",[104,1877,127],{"class":126},[104,1879,1786],{"class":137},[104,1881,254],{"class":126},[104,1883,1884],{"class":137},"MinReplicas",[104,1886,159],{"class":126},[104,1888,1638],{"class":137},[104,1890,254],{"class":126},[104,1892,1780],{"class":137},[104,1894,1895],{"class":130},"-",[104,1897,1786],{"class":137},[104,1899,254],{"class":126},[104,1901,1791],{"class":137},[104,1903,1904],{"class":126},"))\n",[104,1906,1907],{"class":106,"line":341},[104,1908,621],{"class":126},[104,1910,1911],{"class":106,"line":377},[104,1912,626],{"class":126},[104,1914,1915],{"class":106,"line":1019},[104,1916,639],{"class":126},[86,1918,1920],{"id":1919},"configuration","Configuration",[82,1922,1923,1924,1927],{},"Rules are declared in a YAML file and hot-reloaded on change via ",[101,1925,1926],{},"inotify",", so threshold adjustments don't require a daemon restart.",[86,1929,404],{"id":403},[82,1931,1932],{},"Running in production. Has handled scale events across 12 services with zero missed scaling opportunities recorded over a 90-day observation window.",[409,1934,1935],{},"html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}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":99,"searchDepth":21,"depth":21,"links":1937},[1938,1939,1940],{"id":1585,"depth":21,"text":1586},{"id":1919,"depth":21,"text":1920},{"id":403,"depth":21,"text":404},"Go-based daemon that monitors Swarm service load and adjusts replica counts in real time based on Prometheus metrics.","D4E5F6",{},"\u002Fprojects\u002Fservice-autoscaler","BAY 02",{"title":1577,"description":1941},"projects\u002Fservice-autoscaler","\u002Fimages\u002Fthumbnails\u002Fservice-autoscaler.png","6jraPhE5BqB7rMeWOOshRgbYJmvkH4sWjcymC74tzfk",{"id":1951,"title":1952,"body":1953,"description":2064,"extension":418,"hash":2065,"liveUrl":6,"meta":2066,"navigation":215,"order":30,"path":2067,"rackBay":2068,"rackStatus":1570,"region":2069,"seo":2070,"stem":2071,"thumbnail":2072,"vault":215,"__hash__":2073},"projects\u002Fprojects\u002Ftraefik-mesh.md","Traefik Mesh",{"type":79,"value":1954,"toc":2059},[1955,1958,1962,1965,2044,2048,2051,2053,2056],[82,1956,1957],{},"Traefik Mesh is the ingress and internal routing layer for the production service cluster. It handles TLS termination, per-service rate limiting, and request authentication via a forward-auth middleware chain backed by Keycloak.",[86,1959,1961],{"id":1960},"ingress-configuration","Ingress Configuration",[82,1963,1964],{},"All services are declared via Docker labels, making the routing configuration live alongside the service definition in the compose stack:",[94,1966,1970],{"className":1967,"code":1968,"language":1969,"meta":99,"style":99},"language-yaml shiki shiki-themes vitesse-light","deploy:\n  labels:\n    - \"traefik.enable=true\"\n    - \"traefik.http.routers.api.rule=Host(`api.example.com`)\"\n    - \"traefik.http.routers.api.tls.certresolver=letsencrypt\"\n    - \"traefik.http.middlewares.auth.forwardauth.address=http:\u002F\u002Fauth-service\u002Fverify\"\n    - \"traefik.http.routers.api.middlewares=auth@docker\"\n","yaml",[101,1971,1972,1980,1987,2000,2011,2022,2033],{"__ignoreMap":99},[104,1973,1974,1977],{"class":106,"line":12},[104,1975,1976],{"class":1143},"deploy",[104,1978,1979],{"class":126},":\n",[104,1981,1982,1985],{"class":106,"line":21},[104,1983,1984],{"class":1143},"  labels",[104,1986,1979],{"class":126},[104,1988,1989,1992,1994,1997],{"class":106,"line":30},[104,1990,1991],{"class":126},"    -",[104,1993,898],{"class":162},[104,1995,1996],{"class":166},"traefik.enable=true",[104,1998,1999],{"class":162},"\"\n",[104,2001,2002,2004,2006,2009],{"class":106,"line":39},[104,2003,1991],{"class":126},[104,2005,898],{"class":162},[104,2007,2008],{"class":166},"traefik.http.routers.api.rule=Host(`api.example.com`)",[104,2010,1999],{"class":162},[104,2012,2013,2015,2017,2020],{"class":106,"line":48},[104,2014,1991],{"class":126},[104,2016,898],{"class":162},[104,2018,2019],{"class":166},"traefik.http.routers.api.tls.certresolver=letsencrypt",[104,2021,1999],{"class":162},[104,2023,2024,2026,2028,2031],{"class":106,"line":57},[104,2025,1991],{"class":126},[104,2027,898],{"class":162},[104,2029,2030],{"class":166},"traefik.http.middlewares.auth.forwardauth.address=http:\u002F\u002Fauth-service\u002Fverify",[104,2032,1999],{"class":162},[104,2034,2035,2037,2039,2042],{"class":106,"line":248},[104,2036,1991],{"class":126},[104,2038,898],{"class":162},[104,2040,2041],{"class":166},"traefik.http.routers.api.middlewares=auth@docker",[104,2043,1999],{"class":162},[86,2045,2047],{"id":2046},"zero-trust-auth-layer","Zero-Trust Auth Layer",[82,2049,2050],{},"Every service route passes through the forward-auth middleware. The auth service validates JWTs issued by Keycloak and returns a 401 for unauthenticated requests before traffic ever reaches the upstream service.",[86,2052,404],{"id":403},[82,2054,2055],{},"Handling ~15k requests per minute in steady state. Rate limiting has successfully throttled three separate credential-stuffing attempts without any manual intervention. Auto-renewing Let's Encrypt certificates across 8 domains.",[409,2057,2058],{},"html pre.shiki code .su6XF, html code.shiki .su6XF{--shiki-default:#998418}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}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":99,"searchDepth":21,"depth":21,"links":2060},[2061,2062,2063],{"id":1960,"depth":21,"text":1961},{"id":2046,"depth":21,"text":2047},{"id":403,"depth":21,"text":404},"High-performance reverse proxy configuration and zero-trust identity layer for distributed microservices running on Docker Swarm.","G7H8I9",{},"\u002Fprojects\u002Ftraefik-mesh","BAY 04","EU-CENTRAL-1",{"title":1952,"description":2064},"projects\u002Ftraefik-mesh","\u002Fimages\u002Fthumbnails\u002Ftraefik-mesh.png","OkxpNVZgJlXM3F8wDkLjH_tzGrCJXLuk21a83G5TLNM",{"id":2075,"title":2076,"body":2077,"description":2464,"extension":418,"hash":2465,"liveUrl":6,"meta":2466,"navigation":215,"order":312,"path":2467,"rackBay":6,"rackStatus":6,"region":2468,"seo":2469,"stem":2470,"thumbnail":2471,"vault":715,"__hash__":2472},"projects\u002Fprojects\u002Fveil-auth.md","Veil Auth",{"type":79,"value":2078,"toc":2458},[2079,2082,2086,2089,2437,2441,2444,2448,2451,2453,2456],[82,2080,2081],{},"Veil Auth is the identity and access management service powering the administrative interfaces across the cluster. It issues short-lived JWTs, scoped API tokens for service-to-service calls, and enforces biometric second-factor for privileged operations.",[86,2083,2085],{"id":2084},"token-architecture","Token Architecture",[82,2087,2088],{},"Access tokens are valid for 15 minutes. Refresh tokens are stored as opaque references in Redis with a 7-day TTL. Revoking a refresh token is an O(1) operation — delete the Redis key.",[94,2090,2092],{"className":451,"code":2091,"language":453,"meta":99,"style":99},"func (s *TokenService) Issue(ctx context.Context, subject string, scopes []string) (*TokenPair, error) {\n    claims := jwt.MapClaims{\n        \"sub\":    subject,\n        \"scopes\": scopes,\n        \"exp\":    time.Now().Add(15 * time.Minute).Unix(),\n        \"jti\":    uuid.New().String(),\n    }\n    access, _ := s.key.Sign(claims)\n\n    refreshID := uuid.New().String()\n    s.redis.SetEx(ctx, \"refresh:\"+refreshID, subject, 7*24*time.Hour)\n\n    return &TokenPair{Access: access, RefreshID: refreshID}, nil\n}\n",[101,2093,2094,2157,2174,2191,2206,2253,2279,2283,2314,2318,2339,2396,2400,2433],{"__ignoreMap":99},[104,2095,2096,2098,2100,2103,2105,2108,2110,2113,2115,2117,2119,2121,2123,2125,2128,2131,2133,2136,2138,2140,2142,2144,2146,2149,2151,2153,2155],{"class":106,"line":12},[104,2097,487],{"class":115},[104,2099,134],{"class":126},[104,2101,2102],{"class":137},"s ",[104,2104,756],{"class":130},[104,2106,2107],{"class":238},"TokenService",[104,2109,141],{"class":126},[104,2111,2112],{"class":122}," Issue",[104,2114,127],{"class":126},[104,2116,769],{"class":137},[104,2118,772],{"class":238},[104,2120,254],{"class":126},[104,2122,777],{"class":238},[104,2124,159],{"class":126},[104,2126,2127],{"class":137}," subject",[104,2129,2130],{"class":130}," string",[104,2132,159],{"class":126},[104,2134,2135],{"class":137}," scopes",[104,2137,512],{"class":126},[104,2139,882],{"class":130},[104,2141,141],{"class":126},[104,2143,134],{"class":126},[104,2145,756],{"class":130},[104,2147,2148],{"class":238},"TokenPair",[104,2150,159],{"class":126},[104,2152,782],{"class":130},[104,2154,141],{"class":126},[104,2156,147],{"class":126},[104,2158,2159,2162,2164,2167,2169,2172],{"class":106,"line":21},[104,2160,2161],{"class":137},"    claims",[104,2163,546],{"class":126},[104,2165,2166],{"class":238}," jwt",[104,2168,254],{"class":126},[104,2170,2171],{"class":238},"MapClaims",[104,2173,834],{"class":126},[104,2175,2176,2179,2182,2184,2186,2189],{"class":106,"line":30},[104,2177,2178],{"class":162},"        \"",[104,2180,2181],{"class":166},"sub",[104,2183,904],{"class":162},[104,2185,842],{"class":126},[104,2187,2188],{"class":137},"    subject",[104,2190,853],{"class":126},[104,2192,2193,2195,2198,2200,2202,2204],{"class":106,"line":39},[104,2194,2178],{"class":162},[104,2196,2197],{"class":166},"scopes",[104,2199,904],{"class":162},[104,2201,842],{"class":126},[104,2203,2135],{"class":137},[104,2205,853],{"class":126},[104,2207,2208,2210,2213,2215,2217,2219,2221,2224,2227,2230,2232,2235,2237,2240,2242,2244,2247,2250],{"class":106,"line":48},[104,2209,2178],{"class":162},[104,2211,2212],{"class":166},"exp",[104,2214,904],{"class":162},[104,2216,842],{"class":126},[104,2218,930],{"class":137},[104,2220,254],{"class":126},[104,2222,2223],{"class":122},"Now",[104,2225,2226],{"class":126},"().",[104,2228,2229],{"class":122},"Add",[104,2231,127],{"class":126},[104,2233,2234],{"class":917},"15",[104,2236,1729],{"class":130},[104,2238,2239],{"class":137}," time",[104,2241,254],{"class":126},[104,2243,1842],{"class":137},[104,2245,2246],{"class":126},").",[104,2248,2249],{"class":122},"Unix",[104,2251,2252],{"class":126},"(),\n",[104,2254,2255,2257,2260,2262,2264,2267,2269,2272,2274,2277],{"class":106,"line":57},[104,2256,2178],{"class":162},[104,2258,2259],{"class":166},"jti",[104,2261,904],{"class":162},[104,2263,842],{"class":126},[104,2265,2266],{"class":137},"    uuid",[104,2268,254],{"class":126},[104,2270,2271],{"class":122},"New",[104,2273,2226],{"class":126},[104,2275,2276],{"class":122},"String",[104,2278,2252],{"class":126},[104,2280,2281],{"class":106,"line":248},[104,2282,626],{"class":126},[104,2284,2285,2288,2290,2292,2294,2297,2299,2302,2304,2307,2309,2312],{"class":106,"line":306},[104,2286,2287],{"class":137},"    access",[104,2289,159],{"class":126},[104,2291,971],{"class":137},[104,2293,546],{"class":126},[104,2295,2296],{"class":137}," s",[104,2298,254],{"class":126},[104,2300,2301],{"class":137},"key",[104,2303,254],{"class":126},[104,2305,2306],{"class":122},"Sign",[104,2308,127],{"class":126},[104,2310,2311],{"class":137},"claims",[104,2313,182],{"class":126},[104,2315,2316],{"class":106,"line":312},[104,2317,216],{"emptyLinePlaceholder":215},[104,2319,2320,2323,2325,2328,2330,2332,2334,2336],{"class":106,"line":317},[104,2321,2322],{"class":137},"    refreshID",[104,2324,546],{"class":126},[104,2326,2327],{"class":137}," uuid",[104,2329,254],{"class":126},[104,2331,2271],{"class":122},[104,2333,2226],{"class":126},[104,2335,2276],{"class":122},[104,2337,2338],{"class":126},"()\n",[104,2340,2341,2344,2346,2348,2350,2353,2355,2357,2359,2361,2364,2366,2368,2371,2373,2375,2377,2380,2382,2385,2387,2389,2391,2394],{"class":106,"line":341},[104,2342,2343],{"class":137},"    s",[104,2345,254],{"class":126},[104,2347,810],{"class":137},[104,2349,254],{"class":126},[104,2351,2352],{"class":122},"SetEx",[104,2354,127],{"class":126},[104,2356,769],{"class":137},[104,2358,159],{"class":126},[104,2360,898],{"class":162},[104,2362,2363],{"class":166},"refresh:",[104,2365,904],{"class":162},[104,2367,1783],{"class":130},[104,2369,2370],{"class":137},"refreshID",[104,2372,159],{"class":126},[104,2374,2127],{"class":137},[104,2376,159],{"class":126},[104,2378,2379],{"class":917}," 7",[104,2381,756],{"class":130},[104,2383,2384],{"class":917},"24",[104,2386,756],{"class":130},[104,2388,1837],{"class":137},[104,2390,254],{"class":126},[104,2392,2393],{"class":137},"Hour",[104,2395,182],{"class":126},[104,2397,2398],{"class":106,"line":377},[104,2399,216],{"emptyLinePlaceholder":215},[104,2401,2402,2404,2406,2408,2410,2413,2415,2418,2420,2423,2425,2428,2431],{"class":106,"line":1019},[104,2403,631],{"class":115},[104,2405,824],{"class":130},[104,2407,2148],{"class":238},[104,2409,885],{"class":126},[104,2411,2412],{"class":137},"Access",[104,2414,842],{"class":126},[104,2416,2417],{"class":137}," access",[104,2419,159],{"class":126},[104,2421,2422],{"class":137}," RefreshID",[104,2424,842],{"class":126},[104,2426,2427],{"class":137}," refreshID",[104,2429,2430],{"class":126},"},",[104,2432,1539],{"class":130},[104,2434,2435],{"class":106,"line":1024},[104,2436,639],{"class":126},[86,2438,2440],{"id":2439},"biometric-second-factor","Biometric Second Factor",[82,2442,2443],{},"WebAuthn handles the biometric flow. The server stores credential public keys, never any biometric data. Browser passkeys and hardware security keys are both supported. The fallback for non-WebAuthn clients is TOTP.",[86,2445,2447],{"id":2446},"audit-log","Audit Log",[82,2449,2450],{},"Every authentication event, token issue, and access denial is written to an append-only audit log in Postgres with a cryptographic hash chain — each record includes the hash of the previous record, making retroactive tampering detectable.",[86,2452,404],{"id":403},[82,2454,2455],{},"Running in production, protecting 6 administrative interfaces. Planning a self-service credential management UI for the next release.",[409,2457,1046],{},{"title":99,"searchDepth":21,"depth":21,"links":2459},[2460,2461,2462,2463],{"id":2084,"depth":21,"text":2085},{"id":2439,"depth":21,"text":2440},{"id":2446,"depth":21,"text":2447},{"id":403,"depth":21,"text":404},"Zero-trust identity layer with JWT-based authentication, role-scoped API tokens, and biometric second factor for administrative consoles.","Y5Z6A7",{},"\u002Fprojects\u002Fveil-auth","CA-CENTRAL-1",{"title":2076,"description":2464},"projects\u002Fveil-auth","\u002Fimages\u002Fthumbnails\u002Fveil-auth.png","3RnHwxVVupReRkyUAJhnBTtaDsZ_Q9PgFtbZ1leF9nA",[2474,2803,3030,3638,5120,5265],{"id":2475,"title":2476,"body":2477,"date":2789,"description":2790,"extension":418,"meta":2791,"navigation":215,"path":2792,"readTime":2793,"seo":2794,"stem":2795,"tags":2796,"thumbnail":2801,"__hash__":2802},"blog\u002Fblog\u002Fbuilding-swarm-cluster-openstack.md","Building a Highly Available Swarm Cluster on OpenStack",{"type":79,"value":2478,"toc":2783},[2479,2482,2486,2489,2492,2496,2503,2709,2712,2716,2723,2766,2773,2777,2780],[82,2480,2481],{},"Deploying a robust, fault-tolerant cluster isn't just about spinning up instances; it's about engineering resilience at every layer. In this log, I'm documenting the architecture and configuration of a production-grade Docker Swarm cluster running on OpenStack.",[86,2483,2485],{"id":2484},"the-architecture","The Architecture",[82,2487,2488],{},"Our foundation requires at least three manager nodes to maintain quorum. Losing a single manager should not degrade the cluster state. Worker nodes scale horizontally across different availability zones to prevent a single point of failure.",[82,2490,2491],{},"The manager tier handles cluster state, scheduling, and service reconciliation. Workers are provisioned in groups per zone, so a complete AZ outage only removes a fraction of total capacity.",[86,2493,2495],{"id":2494},"network-configuration","Network Configuration",[82,2497,2498,2499,2502],{},"Proper overlay networking is critical. We utilize Traefik as the ingress controller, routing traffic efficiently to the appropriate services. The configuration is declared in a ",[101,2500,2501],{},"docker-compose.yml"," deployed as a stack.",[94,2504,2506],{"className":1967,"code":2505,"language":1969,"meta":99,"style":99},"version: \"3.8\"\n\nservices:\n  traefik:\n    image: traefik:v2.9\n    command:\n      - \"--api.insecure=true\"\n      - \"--providers.docker=true\"\n      - \"--providers.docker.swarmMode=true\"\n      - \"--entrypoints.web.address=:80\"\n    ports:\n      - \"80:80\"\n      - \"8080:8080\"\n    networks:\n      - proxy\n    deploy:\n      placement:\n        constraints: [node.role == manager]\n\nnetworks:\n  proxy:\n    external: true\n",[101,2507,2508,2522,2526,2533,2540,2550,2557,2569,2580,2591,2602,2609,2620,2631,2638,2645,2652,2660,2677,2682,2690,2698],{"__ignoreMap":99},[104,2509,2510,2513,2515,2517,2520],{"class":106,"line":12},[104,2511,2512],{"class":1143},"version",[104,2514,842],{"class":126},[104,2516,898],{"class":162},[104,2518,2519],{"class":166},"3.8",[104,2521,1999],{"class":162},[104,2523,2524],{"class":106,"line":21},[104,2525,216],{"emptyLinePlaceholder":215},[104,2527,2528,2531],{"class":106,"line":30},[104,2529,2530],{"class":1143},"services",[104,2532,1979],{"class":126},[104,2534,2535,2538],{"class":106,"line":39},[104,2536,2537],{"class":1143},"  traefik",[104,2539,1979],{"class":126},[104,2541,2542,2545,2547],{"class":106,"line":48},[104,2543,2544],{"class":1143},"    image",[104,2546,842],{"class":126},[104,2548,2549],{"class":166}," traefik:v2.9\n",[104,2551,2552,2555],{"class":106,"line":57},[104,2553,2554],{"class":1143},"    command",[104,2556,1979],{"class":126},[104,2558,2559,2562,2564,2567],{"class":106,"line":248},[104,2560,2561],{"class":126},"      -",[104,2563,898],{"class":162},[104,2565,2566],{"class":166},"--api.insecure=true",[104,2568,1999],{"class":162},[104,2570,2571,2573,2575,2578],{"class":106,"line":306},[104,2572,2561],{"class":126},[104,2574,898],{"class":162},[104,2576,2577],{"class":166},"--providers.docker=true",[104,2579,1999],{"class":162},[104,2581,2582,2584,2586,2589],{"class":106,"line":312},[104,2583,2561],{"class":126},[104,2585,898],{"class":162},[104,2587,2588],{"class":166},"--providers.docker.swarmMode=true",[104,2590,1999],{"class":162},[104,2592,2593,2595,2597,2600],{"class":106,"line":317},[104,2594,2561],{"class":126},[104,2596,898],{"class":162},[104,2598,2599],{"class":166},"--entrypoints.web.address=:80",[104,2601,1999],{"class":162},[104,2603,2604,2607],{"class":106,"line":341},[104,2605,2606],{"class":1143},"    ports",[104,2608,1979],{"class":126},[104,2610,2611,2613,2615,2618],{"class":106,"line":377},[104,2612,2561],{"class":126},[104,2614,898],{"class":162},[104,2616,2617],{"class":166},"80:80",[104,2619,1999],{"class":162},[104,2621,2622,2624,2626,2629],{"class":106,"line":1019},[104,2623,2561],{"class":126},[104,2625,898],{"class":162},[104,2627,2628],{"class":166},"8080:8080",[104,2630,1999],{"class":162},[104,2632,2633,2636],{"class":106,"line":1024},[104,2634,2635],{"class":1143},"    networks",[104,2637,1979],{"class":126},[104,2639,2640,2642],{"class":106,"line":1029},[104,2641,2561],{"class":126},[104,2643,2644],{"class":166}," proxy\n",[104,2646,2647,2650],{"class":106,"line":1542},[104,2648,2649],{"class":1143},"    deploy",[104,2651,1979],{"class":126},[104,2653,2655,2658],{"class":106,"line":2654},17,[104,2656,2657],{"class":1143},"      placement",[104,2659,1979],{"class":126},[104,2661,2663,2666,2668,2671,2674],{"class":106,"line":2662},18,[104,2664,2665],{"class":1143},"        constraints",[104,2667,842],{"class":126},[104,2669,2670],{"class":126}," [",[104,2672,2673],{"class":166},"node.role == manager",[104,2675,2676],{"class":126},"]\n",[104,2678,2680],{"class":106,"line":2679},19,[104,2681,216],{"emptyLinePlaceholder":215},[104,2683,2685,2688],{"class":106,"line":2684},20,[104,2686,2687],{"class":1143},"networks",[104,2689,1979],{"class":126},[104,2691,2693,2696],{"class":106,"line":2692},21,[104,2694,2695],{"class":1143},"  proxy",[104,2697,1979],{"class":126},[104,2699,2701,2704,2706],{"class":106,"line":2700},22,[104,2702,2703],{"class":1143},"    external",[104,2705,842],{"class":126},[104,2707,2708],{"class":115}," true\n",[82,2710,2711],{},"The overlay network ensures containers on separate physical hosts can communicate as if they are on the same LAN, while Traefik's Swarm mode provider dynamically discovers services as they are deployed.",[86,2713,2715],{"id":2714},"automated-scaling","Automated Scaling",[82,2717,2718,2719,2722],{},"Using ",[101,2720,2721],{},"swarmctl"," alongside custom Prometheus metrics allows us to dynamically provision worker nodes when CPU load exceeds our baseline threshold.",[94,2724,2728],{"className":2725,"code":2726,"language":2727,"meta":99,"style":99},"language-bash shiki shiki-themes vitesse-light","$ swarmctl cluster update \\\n    --autoscale-cpu-threshold=75 \\\n    --autoscale-max-nodes=10 \\\n    --cluster-name=prod-swarm-alpha\n","bash",[101,2729,2730,2747,2754,2761],{"__ignoreMap":99},[104,2731,2732,2735,2738,2741,2744],{"class":106,"line":12},[104,2733,2734],{"class":122},"$",[104,2736,2737],{"class":166}," swarmctl",[104,2739,2740],{"class":166}," cluster",[104,2742,2743],{"class":166}," update",[104,2745,2746],{"class":298}," \\\n",[104,2748,2749,2752],{"class":106,"line":21},[104,2750,2751],{"class":298},"    --autoscale-cpu-threshold=75",[104,2753,2746],{"class":298},[104,2755,2756,2759],{"class":106,"line":30},[104,2757,2758],{"class":298},"    --autoscale-max-nodes=10",[104,2760,2746],{"class":298},[104,2762,2763],{"class":106,"line":39},[104,2764,2765],{"class":298},"    --cluster-name=prod-swarm-alpha\n",[82,2767,2768,2769,2772],{},"When the system detects a sustained spike, the OpenStack API is triggered to boot a new instance, execute a ",[101,2770,2771],{},"cloud-init"," script containing the swarm join token, and register the node to the cluster within minutes. This ensures our services remain responsive even under unpredictable loads. We also have alerting tied into our Slack channels for real-time visibility.",[86,2774,2776],{"id":2775},"whats-next","What's Next",[82,2778,2779],{},"The next iteration involves integrating Consul for service discovery and moving secrets management to Vault. Both bring operational maturity the cluster currently lacks — expect a follow-up post once the migration settles.",[409,2781,2782],{},"html pre.shiki code .su6XF, html code.shiki .su6XF{--shiki-default:#998418}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}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 .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sEi1f, html code.shiki .sEi1f{--shiki-default:#A65E2B}",{"title":99,"searchDepth":21,"depth":21,"links":2784},[2785,2786,2787,2788],{"id":2484,"depth":21,"text":2485},{"id":2494,"depth":21,"text":2495},{"id":2714,"depth":21,"text":2715},{"id":2775,"depth":21,"text":2776},"2026-05-14","A deep dive into networking, resource limits, and automated scaling strategies for Docker Swarm on OpenStack.",{},"\u002Fblog\u002Fbuilding-swarm-cluster-openstack","5 min",{"title":2476,"description":2790},"blog\u002Fbuilding-swarm-cluster-openstack",[2797,2798,2799,2800],"Docker","OpenStack","DevOps","Infrastructure","\u002Fimages\u002Fthumbnails\u002Fbuilding-swarm-cluster-openstack.png","No_bDZVlSIYDeHZz_sIAaWIwX7hlq0yaVrJPTRnnE8s",{"id":2804,"title":2805,"body":2806,"date":3017,"description":3018,"extension":418,"meta":3019,"navigation":215,"path":3020,"readTime":3021,"seo":3022,"stem":3023,"tags":3024,"thumbnail":3028,"__hash__":3029},"blog\u002Fblog\u002Fembedding-external-content.md","Embedding External Content in Your Posts",{"type":79,"value":2807,"toc":3013},[2808,2811,2815,2821,3010],[82,2809,2810],{},"Posts support rich embeds using MDC component blocks. Drop any snippet below into your markdown. No raw HTML, no iframes to configure — just the tag and the ID.",[86,2812,2814],{"id":2813},"youtube","YouTube",[82,2816,2817,2818,2246],{},"Use the video ID from the URL (the part after ",[101,2819,2820],{},"v=",[2813,2822,2825,2835,2838,2842,2848],{"id":2823,"title":2824},"jNQXAC9IVRw","Me at the zoo — the very first YouTube upload",[94,2826,2829],{"className":2827,"code":2828,"language":418,"meta":99,"style":99},"language-md shiki shiki-themes vitesse-light","::youtube{id=\"jNQXAC9IVRw\" title=\"Video title here\"}\n",[101,2830,2831],{"__ignoreMap":99},[104,2832,2833],{"class":106,"line":12},[104,2834,2828],{},[2836,2837],"hr",{},[86,2839,2841],{"id":2840},"twitter-x","Twitter \u002F X",[82,2843,2844,2845,2246],{},"Use the numeric tweet ID from the end of the tweet URL (",[101,2846,2847],{},"twitter.com\u002Fuser\u002Fstatus\u002FID",[2849,2850,2852,2861,2863,2867,2882],"tweet",{"id":2851},"2057183802569421019",[94,2853,2855],{"className":2827,"code":2854,"language":418,"meta":99,"style":99},"::tweet{id=\"2057183802569421019\"}\n",[101,2856,2857],{"__ignoreMap":99},[104,2858,2859],{"class":106,"line":12},[104,2860,2854],{},[2836,2862],{},[86,2864,2866],{"id":2865},"instagram","Instagram",[82,2868,2869,2870,2873,2874,2877,2878,2881],{},"Use the post shortcode from the URL — ",[101,2871,2872],{},"instagram.com\u002Fp\u002FSHORTCODE\u002F",". Add ",[101,2875,2876],{},"author"," and ",[101,2879,2880],{},"caption"," for context.",[2865,2883,2887,2896,2898,2902,2922],{"author":2884,"caption":2885,"id":2886},"@nuxt.js","Nuxt 3 is here.","B9MexqUnoIM",[94,2888,2890],{"className":2827,"code":2889,"language":418,"meta":99,"style":99},"::instagram{id=\"B9MexqUnoIM\" author=\"@nuxt.js\" caption=\"Nuxt 3 is here.\"}\n",[101,2891,2892],{"__ignoreMap":99},[104,2893,2894],{"class":106,"line":12},[104,2895,2889],{},[2836,2897],{},[86,2899,2901],{"id":2900},"spotify","Spotify",[82,2903,2904,2905,1232,2908,1232,2911,2914,2915,2918,2919,2246],{},"Supports ",[101,2906,2907],{},"track",[101,2909,2910],{},"album",[101,2912,2913],{},"playlist",", and ",[101,2916,2917],{},"episode"," types. Grab the ID from the share link (",[101,2920,2921],{},"open.spotify.com\u002Ftrack\u002FID",[2900,2923,2925,2944,2946,2950,2970],{"id":2924,"type":2907},"44DsgT84HJBv8PaxeK397f",[94,2926,2928],{"className":2827,"code":2927,"language":418,"meta":99,"style":99},"::spotify{id=\"44DsgT84HJBv8PaxeK397f\" type=\"track\"}\n\n::spotify{id=\"37i9dQZF1DX4sWSpwq3LiO\" type=\"playlist\"}\n",[101,2929,2930,2935,2939],{"__ignoreMap":99},[104,2931,2932],{"class":106,"line":12},[104,2933,2934],{},"::spotify{id=\"44DsgT84HJBv8PaxeK397f\" type=\"track\"}\n",[104,2936,2937],{"class":106,"line":21},[104,2938,216],{"emptyLinePlaceholder":215},[104,2940,2941],{"class":106,"line":30},[104,2942,2943],{},"::spotify{id=\"37i9dQZF1DX4sWSpwq3LiO\" type=\"playlist\"}\n",[2836,2945],{},[86,2947,2949],{"id":2948},"github","GitHub",[82,2951,2952,2953,2956,2957,2960,2961,1232,2964,2914,2967,254],{},"A styled repo card — no external scripts needed. Pass ",[101,2954,2955],{},"repo"," as ",[101,2958,2959],{},"username\u002Frepository",", with optional ",[101,2962,2963],{},"description",[101,2965,2966],{},"language",[101,2968,2969],{},"stars",[2948,2971,2976,2985,2987,2991,2994,2997],{"description":2972,"language":2973,"repo":2974,"stars":2975},"The Intuitive Vue Framework.","TypeScript","nuxt\u002Fnuxt","54k",[94,2977,2979],{"className":2827,"code":2978,"language":418,"meta":99,"style":99},"::github{repo=\"nuxt\u002Fnuxt\" description=\"The Intuitive Vue Framework.\" language=\"TypeScript\" stars=\"54k\"}\n",[101,2980,2981],{"__ignoreMap":99},[104,2982,2983],{"class":106,"line":12},[104,2984,2978],{},[2836,2986],{},[86,2988,2990],{"id":2989},"using-embeds-in-context","Using embeds in context",[82,2992,2993],{},"Embeds can sit anywhere in a post, mixed with regular prose. Example:",[82,2995,2996],{},"I've been experimenting with this talk on microservices — worth watching if you're evaluating service mesh patterns:",[2813,2998,3001,3004],{"id":2999,"title":3000},"GBTdnfD6s5Q","Microservices at scale",[82,3002,3003],{},"The repo from the talk:",[2948,3005],{"description":3006,"language":3007,"repo":3008,"stars":3009},"Connect, secure, control, and observe services.","Go","istio\u002Fistio","35k",[409,3011,3012],{},"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":99,"searchDepth":21,"depth":21,"links":3014},[3015,3016],{"id":2813,"depth":21,"text":2814},{"id":2840,"depth":21,"text":2841},"2026-05-17","Full reference for every supported embed — YouTube, Twitter\u002FX, Instagram, Spotify, and GitHub cards — with copy-paste examples for each.",{},"\u002Fblog\u002Fembedding-external-content","3 min",{"title":2805,"description":3018},"blog\u002Fembedding-external-content",[3025,3026,3027],"Guide","MDC","Embeds","\u002Fimages\u002Fthumbnails\u002Fembedding-external-content.png","gjjAUSnqoHoIj3QhbDO1nqi-JYqUbFA9KVNWuQyqIAU",{"id":3031,"title":3032,"body":3033,"date":3624,"description":3625,"extension":418,"meta":3626,"navigation":215,"path":3627,"readTime":3628,"seo":3629,"stem":3630,"tags":3631,"thumbnail":3636,"__hash__":3637},"blog\u002Fblog\u002Fintegrating-fastapi-nuxt.md","Integrating FastAPI with Next-Gen Vue\u002FNuxt Interfaces",{"type":79,"value":3034,"toc":3618},[3035,3038,3042,3045,3221,3227,3231,3242,3266,3269,3521,3524,3528,3531,3607,3610,3612,3615],[82,3036,3037],{},"Most portfolio sites gloss over the API boundary — the messy strip of no-man's-land between a Python backend and a JavaScript frontend. In production, that seam is where bugs live. This post is about closing it properly.",[86,3039,3041],{"id":3040},"why-fastapi","Why FastAPI",[82,3043,3044],{},"FastAPI is the right tool for this job for two reasons: it generates an OpenAPI spec out of the box, and its Pydantic models give you runtime validation for free. The spec becomes your single source of truth for the interface contract.",[94,3046,3050],{"className":3047,"code":3048,"language":3049,"meta":99,"style":99},"language-python shiki shiki-themes vitesse-light","from fastapi import FastAPI\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\nclass DeploymentPayload(BaseModel):\n    service_name: str\n    image_tag: str\n    replicas: int = 1\n\n@app.post(\"\u002Fdeployments\", response_model=DeploymentPayload)\nasync def create_deployment(payload: DeploymentPayload):\n    # schedule the deployment...\n    return payload\n","python",[101,3051,3052,3067,3079,3083,3096,3100,3116,3126,3135,3150,3154,3188,3209,3214],{"__ignoreMap":99},[104,3053,3054,3057,3061,3064],{"class":106,"line":12},[104,3055,3056],{"class":115},"from",[104,3058,3060],{"class":3059},"suHK_"," fastapi ",[104,3062,3063],{"class":115},"import",[104,3065,3066],{"class":3059}," FastAPI\n",[104,3068,3069,3071,3074,3076],{"class":106,"line":21},[104,3070,3056],{"class":115},[104,3072,3073],{"class":3059}," pydantic ",[104,3075,3063],{"class":115},[104,3077,3078],{"class":3059}," BaseModel\n",[104,3080,3081],{"class":106,"line":30},[104,3082,216],{"emptyLinePlaceholder":215},[104,3084,3085,3088,3091,3094],{"class":106,"line":39},[104,3086,3087],{"class":3059},"app ",[104,3089,3090],{"class":126},"=",[104,3092,3093],{"class":3059}," FastAPI",[104,3095,2338],{"class":126},[104,3097,3098],{"class":106,"line":48},[104,3099,216],{"emptyLinePlaceholder":215},[104,3101,3102,3105,3108,3110,3113],{"class":106,"line":57},[104,3103,3104],{"class":130},"class",[104,3106,3107],{"class":238}," DeploymentPayload",[104,3109,127],{"class":126},[104,3111,3112],{"class":122},"BaseModel",[104,3114,3115],{"class":126},"):\n",[104,3117,3118,3121,3123],{"class":106,"line":248},[104,3119,3120],{"class":3059},"    service_name",[104,3122,842],{"class":126},[104,3124,3125],{"class":1143}," str\n",[104,3127,3128,3131,3133],{"class":106,"line":306},[104,3129,3130],{"class":3059},"    image_tag",[104,3132,842],{"class":126},[104,3134,3125],{"class":1143},[104,3136,3137,3140,3142,3145,3147],{"class":106,"line":312},[104,3138,3139],{"class":3059},"    replicas",[104,3141,842],{"class":126},[104,3143,3144],{"class":1143}," int",[104,3146,227],{"class":126},[104,3148,3149],{"class":917}," 1\n",[104,3151,3152],{"class":106,"line":317},[104,3153,216],{"emptyLinePlaceholder":215},[104,3155,3156,3159,3162,3164,3167,3169,3171,3174,3176,3178,3181,3183,3186],{"class":106,"line":341},[104,3157,3158],{"class":126},"@",[104,3160,3161],{"class":122},"app",[104,3163,254],{"class":126},[104,3165,3166],{"class":122},"post",[104,3168,127],{"class":126},[104,3170,904],{"class":162},[104,3172,3173],{"class":166},"\u002Fdeployments",[104,3175,904],{"class":162},[104,3177,159],{"class":126},[104,3179,3180],{"class":137}," response_model",[104,3182,3090],{"class":126},[104,3184,3185],{"class":3059},"DeploymentPayload",[104,3187,182],{"class":126},[104,3189,3190,3192,3195,3198,3200,3203,3205,3207],{"class":106,"line":377},[104,3191,131],{"class":130},[104,3193,3194],{"class":130}," def",[104,3196,3197],{"class":122}," create_deployment",[104,3199,127],{"class":126},[104,3201,3202],{"class":3059},"payload",[104,3204,842],{"class":126},[104,3206,3107],{"class":3059},[104,3208,3115],{"class":126},[104,3210,3211],{"class":106,"line":1019},[104,3212,3213],{"class":109},"    # schedule the deployment...\n",[104,3215,3216,3218],{"class":106,"line":1024},[104,3217,631],{"class":115},[104,3219,3220],{"class":3059}," payload\n",[82,3222,1253,3223,3226],{},[101,3224,3225],{},"response_model"," annotation is the key — FastAPI will serialize exactly what you declare, and nothing more. No accidental data leaks.",[86,3228,3230],{"id":3229},"type-generation-on-the-nuxt-side","Type Generation on the Nuxt Side",[82,3232,3233,3234,3237,3238,3241],{},"Once the OpenAPI spec exists at ",[101,3235,3236],{},"http:\u002F\u002Flocalhost:8000\u002Fopenapi.json",", use ",[101,3239,3240],{},"openapi-typescript"," to generate TypeScript types:",[94,3243,3245],{"className":2725,"code":3244,"language":2727,"meta":99,"style":99},"$ npx openapi-typescript http:\u002F\u002Flocalhost:8000\u002Fopenapi.json -o src\u002Ftypes\u002Fapi.ts\n",[101,3246,3247],{"__ignoreMap":99},[104,3248,3249,3251,3254,3257,3260,3263],{"class":106,"line":12},[104,3250,2734],{"class":122},[104,3252,3253],{"class":166}," npx",[104,3255,3256],{"class":166}," openapi-typescript",[104,3258,3259],{"class":166}," http:\u002F\u002Flocalhost:8000\u002Fopenapi.json",[104,3261,3262],{"class":298}," -o",[104,3264,3265],{"class":166}," src\u002Ftypes\u002Fapi.ts\n",[82,3267,3268],{},"Now your Nuxt composable can be fully typed with zero manual interface definitions:",[94,3270,3272],{"className":96,"code":3271,"language":98,"meta":99,"style":99},"import type { components } from '~\u002Ftypes\u002Fapi'\n\ntype Deployment = components['schemas']['DeploymentPayload']\n\nexport const useDeployments = () => {\n  const list = ref\u003CDeployment[]>([])\n\n  const create = async (payload: Deployment) => {\n    const data = await $fetch\u003CDeployment>('\u002Fapi\u002Fdeployments', {\n      method: 'POST',\n      body: payload,\n    })\n    list.value.push(data)\n  }\n\n  return { list, create }\n}\n",[101,3273,3274,3300,3304,3335,3339,3358,3379,3383,3408,3441,3457,3468,3473,3494,3498,3502,3517],{"__ignoreMap":99},[104,3275,3276,3278,3281,3283,3286,3289,3292,3294,3297],{"class":106,"line":12},[104,3277,3063],{"class":115},[104,3279,3280],{"class":115}," type",[104,3282,957],{"class":126},[104,3284,3285],{"class":137}," components",[104,3287,3288],{"class":126}," }",[104,3290,3291],{"class":115}," from",[104,3293,163],{"class":162},[104,3295,3296],{"class":166},"~\u002Ftypes\u002Fapi",[104,3298,3299],{"class":162},"'\n",[104,3301,3302],{"class":106,"line":21},[104,3303,216],{"emptyLinePlaceholder":215},[104,3305,3306,3308,3311,3313,3315,3317,3319,3322,3324,3327,3329,3331,3333],{"class":106,"line":30},[104,3307,460],{"class":130},[104,3309,3310],{"class":238}," Deployment",[104,3312,227],{"class":126},[104,3314,3285],{"class":238},[104,3316,469],{"class":126},[104,3318,170],{"class":162},[104,3320,3321],{"class":166},"schemas",[104,3323,170],{"class":162},[104,3325,3326],{"class":126},"][",[104,3328,170],{"class":162},[104,3330,3185],{"class":166},[104,3332,170],{"class":162},[104,3334,2676],{"class":126},[104,3336,3337],{"class":106,"line":39},[104,3338,216],{"emptyLinePlaceholder":215},[104,3340,3341,3343,3346,3349,3351,3354,3356],{"class":106,"line":48},[104,3342,116],{"class":115},[104,3344,3345],{"class":130}," const ",[104,3347,3348],{"class":122},"useDeployments",[104,3350,227],{"class":126},[104,3352,3353],{"class":126}," ()",[104,3355,144],{"class":126},[104,3357,147],{"class":126},[104,3359,3360,3362,3365,3367,3370,3373,3376],{"class":106,"line":57},[104,3361,221],{"class":130},[104,3363,3364],{"class":137},"list",[104,3366,227],{"class":126},[104,3368,3369],{"class":122}," ref",[104,3371,3372],{"class":126},"\u003C",[104,3374,3375],{"class":238},"Deployment",[104,3377,3378],{"class":126},"[]>([])\n",[104,3380,3381],{"class":106,"line":248},[104,3382,216],{"emptyLinePlaceholder":215},[104,3384,3385,3387,3390,3392,3394,3396,3398,3400,3402,3404,3406],{"class":106,"line":306},[104,3386,221],{"class":130},[104,3388,3389],{"class":122},"create",[104,3391,227],{"class":126},[104,3393,1097],{"class":130},[104,3395,127],{"class":126},[104,3397,3202],{"class":137},[104,3399,235],{"class":126},[104,3401,3375],{"class":238},[104,3403,141],{"class":126},[104,3405,144],{"class":126},[104,3407,147],{"class":126},[104,3409,3410,3413,3415,3417,3420,3423,3425,3427,3430,3432,3435,3437,3439],{"class":106,"line":312},[104,3411,3412],{"class":130},"    const ",[104,3414,232],{"class":137},[104,3416,227],{"class":126},[104,3418,3419],{"class":115}," await",[104,3421,3422],{"class":122}," $fetch",[104,3424,3372],{"class":126},[104,3426,3375],{"class":238},[104,3428,3429],{"class":126},">(",[104,3431,170],{"class":162},[104,3433,3434],{"class":166},"\u002Fapi\u002Fdeployments",[104,3436,170],{"class":162},[104,3438,159],{"class":126},[104,3440,147],{"class":126},[104,3442,3443,3446,3448,3450,3453,3455],{"class":106,"line":317},[104,3444,3445],{"class":1143},"      method",[104,3447,235],{"class":126},[104,3449,170],{"class":162},[104,3451,3452],{"class":166},"POST",[104,3454,170],{"class":162},[104,3456,853],{"class":126},[104,3458,3459,3462,3464,3466],{"class":106,"line":341},[104,3460,3461],{"class":1143},"      body",[104,3463,235],{"class":126},[104,3465,3202],{"class":137},[104,3467,853],{"class":126},[104,3469,3470],{"class":106,"line":377},[104,3471,3472],{"class":126},"    })\n",[104,3474,3475,3478,3480,3483,3485,3488,3490,3492],{"class":106,"line":1019},[104,3476,3477],{"class":137},"    list",[104,3479,254],{"class":126},[104,3481,3482],{"class":137},"value",[104,3484,254],{"class":126},[104,3486,3487],{"class":122},"push",[104,3489,127],{"class":126},[104,3491,232],{"class":137},[104,3493,182],{"class":126},[104,3495,3496],{"class":106,"line":1024},[104,3497,309],{"class":126},[104,3499,3500],{"class":106,"line":1029},[104,3501,216],{"emptyLinePlaceholder":215},[104,3503,3504,3506,3509,3511,3513,3515],{"class":106,"line":1542},[104,3505,1207],{"class":115},[104,3507,3508],{"class":126}," { ",[104,3510,3364],{"class":137},[104,3512,1232],{"class":126},[104,3514,3389],{"class":137},[104,3516,963],{"class":126},[104,3518,3519],{"class":106,"line":2654},[104,3520,639],{"class":126},[82,3522,3523],{},"If the backend changes a field name, TypeScript will scream at you at compile time rather than letting a runtime mismatch slip to production.",[86,3525,3527],{"id":3526},"cors-and-the-proxy-pattern","CORS and the Proxy Pattern",[82,3529,3530],{},"Never expose your Python backend directly to the browser in production. Run Nuxt's built-in server as a reverse proxy instead:",[94,3532,3534],{"className":96,"code":3533,"language":98,"meta":99,"style":99},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    '\u002Fapi\u002F**': {\n      proxy: { to: 'http:\u002F\u002Ffastapi-service:8000\u002F**' },\n    },\n  },\n})\n",[101,3535,3536,3541,3553,3560,3572,3594,3598,3603],{"__ignoreMap":99},[104,3537,3538],{"class":106,"line":12},[104,3539,3540],{"class":109},"\u002F\u002F nuxt.config.ts\n",[104,3542,3543,3545,3547,3550],{"class":106,"line":21},[104,3544,116],{"class":115},[104,3546,119],{"class":115},[104,3548,3549],{"class":122}," defineNuxtConfig",[104,3551,3552],{"class":126},"({\n",[104,3554,3555,3558],{"class":106,"line":30},[104,3556,3557],{"class":1143},"  routeRules",[104,3559,1147],{"class":126},[104,3561,3562,3565,3568,3570],{"class":106,"line":39},[104,3563,3564],{"class":162},"    '",[104,3566,3567],{"class":166},"\u002Fapi\u002F**",[104,3569,170],{"class":162},[104,3571,1147],{"class":126},[104,3573,3574,3577,3579,3582,3584,3586,3589,3591],{"class":106,"line":48},[104,3575,3576],{"class":1143},"      proxy",[104,3578,1180],{"class":126},[104,3580,3581],{"class":1143},"to",[104,3583,235],{"class":126},[104,3585,170],{"class":162},[104,3587,3588],{"class":166},"http:\u002F\u002Ffastapi-service:8000\u002F**",[104,3590,170],{"class":162},[104,3592,3593],{"class":126}," },\n",[104,3595,3596],{"class":106,"line":57},[104,3597,1198],{"class":126},[104,3599,3600],{"class":106,"line":248},[104,3601,3602],{"class":126},"  },\n",[104,3604,3605],{"class":106,"line":306},[104,3606,380],{"class":126},[82,3608,3609],{},"This collapses the cross-origin problem entirely — the browser sees one origin, and your FastAPI service never needs a CORS header.",[86,3611,2776],{"id":2775},[82,3613,3614],{},"The next evolution here is adding a message queue between the two services. For long-running tasks (deployments, report generation), returning a job ID and polling via a Nuxt server-sent-event endpoint beats blocking HTTP every time.",[409,3616,3617],{},"html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .sEi1f, html code.shiki .sEi1f{--shiki-default:#A65E2B}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 .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .suHK_, html code.shiki .suHK_{--shiki-default:#393A34}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .su6XF, html code.shiki .su6XF{--shiki-default:#998418}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}",{"title":99,"searchDepth":21,"depth":21,"links":3619},[3620,3621,3622,3623],{"id":3040,"depth":21,"text":3041},{"id":3229,"depth":21,"text":3230},{"id":3526,"depth":21,"text":3527},{"id":2775,"depth":21,"text":2776},"2026-04-28","How to architect scalable, strongly typed API layers between Python backends and modern JavaScript frontends.",{},"\u002Fblog\u002Fintegrating-fastapi-nuxt","7 min",{"title":3032,"description":3625},"blog\u002Fintegrating-fastapi-nuxt",[3632,3633,2973,3634,3635],"FastAPI","Nuxt","Python","API","\u002Fimages\u002Fthumbnails\u002Fintegrating-fastapi-nuxt.png","oeUJcbHal09PEvokXEL12iWhI43re_Pner_ID9zxtMc",{"id":3639,"title":3640,"body":3641,"date":2789,"description":2790,"extension":418,"meta":5113,"navigation":215,"path":5114,"readTime":2793,"seo":5115,"stem":5116,"tags":5117,"thumbnail":5118,"__hash__":5119},"blog\u002Fblog\u002Fmtu-troubleshooting-blog.md","When Packets Disappear: Debugging an MTU Mismatch in a Hybrid OpenStack Docker Swarm",{"type":79,"value":3642,"toc":5094},[3643,3649,3651,3655,3658,3661,3664,3672,3675,3681,3688,3691,3693,3697,3700,3703,3706,3708,3712,3715,3718,3724,3742,3745,3752,3755,3770,3801,3804,3806,3810,3813,3816,3818,3822,3825,3862,3869,3897,3902,3909,3920,3922,3926,3929,3982,3989,3995,3997,4001,4004,4060,4070,4073,4079,4082,4085,4087,4091,4098,4104,4111,4117,4119,4123,4126,4178,4181,4186,4189,4195,4200,4203,4213,4248,4254,4256,4260,4263,4278,4281,4284,4301,4304,4443,4446,4448,4452,4455,4572,4578,4580,4584,4587,4594,4922,4932,4939,4941,4945,4952,4978,4981,4983,4987,4993,4999,5005,5011,5017,5019,5023,5088,5091],[82,3644,3645],{},[3646,3647,3648],"em",{},"A real-world troubleshooting story about silent packet drops, floating IPs, and why the obvious fix is not always the right one.",[2836,3650],{},[86,3652,3654],{"id":3653},"background-why-we-have-a-hybrid-setup","Background: Why We Have a Hybrid Setup",[82,3656,3657],{},"Our infrastructure runs on a hybrid model — some nodes live inside an OpenStack private cloud, and some live on bare metal servers outside of it. This was not an accident or an oversight. It was a deliberate architectural decision born out of caution.",[82,3659,3660],{},"OpenStack is powerful, but like any cloud platform, it can have outages, networking hiccups, or capacity issues — especially when it is still being evaluated in production for the first time. We wanted a safety net. If the OpenStack environment had a bad day, our core orchestration layer would still be standing.",[82,3662,3663],{},"So our Docker Swarm cluster looks like this:",[94,3665,3670],{"className":3666,"code":3668,"language":3669},[3667],"language-text","Outside OpenStack (bare metal):\n  - 2 Swarm Manager nodes\n  - Keepalived (for VIP failover)\n  - HAProxy (load balancer)\n  - Traefik (reverse proxy and service router)\n  - 1 Data node (persistent storage workloads)\n  - 1 Monitoring node (observability stack)\n\nInside OpenStack:\n  - 1 Production node (email API and other prod services)\n  - 1 Test node (test workloads)\n  - ASG worker nodes (auto-scaled up and down based on CPU and memory)\n","text",[101,3671,3668],{"__ignoreMap":99},[82,3673,3674],{},"Traffic flows like this for any external request:",[94,3676,3679],{"className":3677,"code":3678,"language":3669},[3667],"Internet → VIP → HAProxy → Traefik → Service container\n",[101,3680,3678],{"__ignoreMap":99},[82,3682,3683,3684,3687],{},"Services talk to each other using their Traefik URLs, for example ",[101,3685,3686],{},"https:\u002F\u002Femail-service\u002Fapi\u002Fsend",". Traefik handles the routing based on labels attached to each Docker service.",[82,3689,3690],{},"This setup has been working well. Until one day, emails stopped sending.",[2836,3692],{},[86,3694,3696],{"id":3695},"the-problem-emails-timing-out-silently","The Problem: Emails Timing Out Silently",[82,3698,3699],{},"A service running on the test node was making POST requests to the email service. The email service is hosted on the production node inside OpenStack. The requests were timing out on the calling side, and the email service was not receiving anything at all — no logs, no errors, nothing. It was as if the requests were vanishing into thin air.",[82,3701,3702],{},"Meanwhile, the monitoring node — which also sends emails (a daily digest with AI-generated summaries and HTML content) — was working perfectly fine.",[82,3704,3705],{},"That asymmetry was the first interesting clue.",[2836,3707],{},[86,3709,3711],{"id":3710},"first-hypothesis-docker-overlay-mtu-problem","First Hypothesis: Docker Overlay MTU Problem",[82,3713,3714],{},"The initial instinct was a classic Docker Swarm networking issue. When Docker creates overlay networks (the virtual networks that let containers on different physical nodes talk to each other), it assumes the underlying network can carry standard Ethernet frames of 1500 bytes.",[82,3716,3717],{},"But OpenStack's virtual network adds its own wrapper around every packet. Technologies like VXLAN or Geneve are used to tunnel traffic between virtual machines, and that tunnelling eats into the available space:",[3719,3720,3721],"blockquote",{},[82,3722,3723],{},"Think of it like putting a letter inside an envelope, and then putting that envelope inside a bigger envelope to mail it. The outer envelope takes up space, so the inner letter has to be smaller.",[386,3725,3726,3733,3736],{},[389,3727,3728,3729],{},"Standard Ethernet MTU: ",[3730,3731,3732],"strong",{},"1500 bytes",[389,3734,3735],{},"VXLAN overhead: ~50 bytes",[389,3737,3738,3739],{},"Effective MTU on OpenStack: ",[3730,3740,3741],{},"~1450 bytes",[82,3743,3744],{},"If Docker thinks it can send 1500-byte packets but the network can only carry 1450, oversized packets get silently dropped. No error. No ICMP \"too big\" message. Just gone.",[82,3746,3747,3748,3751],{},"This is called an ",[3730,3749,3750],{},"MTU mismatch",", and it is a well-known pain point in containerised environments running on top of virtualised networks.",[82,3753,3754],{},"The standard fix for this is:",[3756,3757,3758,3764,3767],"ol",{},[389,3759,3760,3761],{},"Tell Docker to use a smaller MTU in ",[101,3762,3763],{},"\u002Fetc\u002Fdocker\u002Fdaemon.json",[389,3765,3766],{},"Recreate the overlay networks with the correct MTU",[389,3768,3769],{},"Recreate the ingress network",[94,3771,3775],{"className":3772,"code":3773,"language":3774,"meta":99,"style":99},"language-json shiki shiki-themes vitesse-light","{\n  \"mtu\": 1400\n}\n","json",[101,3776,3777,3781,3797],{"__ignoreMap":99},[104,3778,3779],{"class":106,"line":12},[104,3780,834],{"class":126},[104,3782,3783,3787,3790,3792,3794],{"class":106,"line":21},[104,3784,3786],{"class":3785},"s61at","  \"",[104,3788,3789],{"class":1143},"mtu",[104,3791,904],{"class":3785},[104,3793,842],{"class":126},[104,3795,3796],{"class":917}," 1400\n",[104,3798,3799],{"class":106,"line":30},[104,3800,639],{"class":126},[82,3802,3803],{},"But this approach had a serious problem for our environment.",[2836,3805],{},[86,3807,3809],{"id":3808},"why-the-standard-fix-was-not-viable","Why the Standard Fix Was Not Viable",[82,3811,3812],{},"We have a lot of services deployed across many nodes. Recreating the Docker ingress network requires all nodes to temporarily lose port routing. Recreating overlay networks means services get restarted. With dozens of services and several nodes, this would mean significant downtime.",[82,3814,3815],{},"We needed to think more carefully before touching anything.",[2836,3817],{},[86,3819,3821],{"id":3820},"digging-deeper-a-ping-test-reveals-the-truth","Digging Deeper: A Ping Test Reveals the Truth",[82,3823,3824],{},"Before making any changes, we ran a diagnostic test on the test node — the one where emails were failing:",[94,3826,3828],{"className":2725,"code":3827,"language":2727,"meta":99,"style":99},"docker run --rm alpine ping -c 5 -s 1200 8.8.8.8\n",[101,3829,3830],{"__ignoreMap":99},[104,3831,3832,3835,3838,3841,3844,3847,3850,3853,3856,3859],{"class":106,"line":12},[104,3833,3834],{"class":122},"docker",[104,3836,3837],{"class":166}," run",[104,3839,3840],{"class":298}," --rm",[104,3842,3843],{"class":166}," alpine",[104,3845,3846],{"class":166}," ping",[104,3848,3849],{"class":298}," -c",[104,3851,3852],{"class":917}," 5",[104,3854,3855],{"class":298}," -s",[104,3857,3858],{"class":917}," 1200",[104,3860,3861],{"class":917}," 8.8.8.8\n",[82,3863,3864,3865,3868],{},"Result: ",[3730,3866,3867],{},"5\u002F5 packets received."," Fine.",[94,3870,3872],{"className":2725,"code":3871,"language":2727,"meta":99,"style":99},"docker run --rm alpine ping -c 5 -s 1472 8.8.8.8\n",[101,3873,3874],{"__ignoreMap":99},[104,3875,3876,3878,3880,3882,3884,3886,3888,3890,3892,3895],{"class":106,"line":12},[104,3877,3834],{"class":122},[104,3879,3837],{"class":166},[104,3881,3840],{"class":298},[104,3883,3843],{"class":166},[104,3885,3846],{"class":166},[104,3887,3849],{"class":298},[104,3889,3852],{"class":917},[104,3891,3855],{"class":298},[104,3893,3894],{"class":917}," 1472",[104,3896,3861],{"class":917},[82,3898,3864,3899],{},[3730,3900,3901],{},"0\u002F5 packets received. 100% loss.",[82,3903,3904,3905,3908],{},"This is significant. The ",[101,3906,3907],{},"-s"," flag sets the packet payload size. Adding 28 bytes for the ICMP and IP headers, a payload of 1472 bytes makes a total packet size of exactly 1500 bytes — the standard Ethernet MTU.",[82,3910,3911,3912,3915,3916,3919],{},"So anything at or near a full-size Ethernet frame was being completely dropped when leaving the OpenStack node. This confirmed there was an MTU problem, but the question was: ",[3646,3913,3914],{},"where exactly"," was it happening, and ",[3646,3917,3918],{},"why"," was it only affecting the test node and not the monitoring node?",[2836,3921],{},[86,3923,3925],{"id":3924},"the-key-asymmetry-inside-vs-outside-openstack","The Key Asymmetry: Inside vs Outside OpenStack",[82,3927,3928],{},"Let us look at what was different between the working and failing cases:",[3930,3931,3932,3948],"table",{},[3933,3934,3935],"thead",{},[3936,3937,3938,3942,3945],"tr",{},[3939,3940,3941],"th",{},"Source",[3939,3943,3944],{},"Destination",[3939,3946,3947],{},"Result",[3949,3950,3951,3963,3972],"tbody",{},[3936,3952,3953,3957,3960],{},[3954,3955,3956],"td",{},"Monitoring node (outside OpenStack)",[3954,3958,3959],{},"Email service (inside OpenStack)",[3954,3961,3962],{},"✅ Works",[3936,3964,3965,3968,3970],{},[3954,3966,3967],{},"Old test node (outside OpenStack)",[3954,3969,3959],{},[3954,3971,3962],{},[3936,3973,3974,3977,3979],{},[3954,3975,3976],{},"New test node (inside OpenStack)",[3954,3978,3959],{},[3954,3980,3981],{},"❌ Fails",[82,3983,3984,3985,3988],{},"The common variable is not the payload size. The monitoring node sends large HTML emails and they go through fine. The common variable is ",[3730,3986,3987],{},"where the request originates",". Everything originating from inside OpenStack to the email service was failing.",[82,3990,3991,3992,254],{},"This pointed away from a Docker overlay problem and toward a ",[3730,3993,3994],{},"network path problem specific to OpenStack",[2836,3996],{},[86,3998,4000],{"id":3999},"the-real-traffic-path-a-surprising-discovery","The Real Traffic Path: A Surprising Discovery",[82,4002,4003],{},"Here is where the architecture revealed something unexpected. The OpenStack nodes join the Swarm like this:",[94,4005,4007],{"className":2725,"code":4006,"language":2727,"meta":99,"style":99},"docker swarm join --token \"$TOKEN\" \"$MANAGER\" \\\n  --advertise-addr \"$FLOATING_IP\" \\\n  --listen-addr 0.0.0.0:2377\n",[101,4008,4009,4038,4052],{"__ignoreMap":99},[104,4010,4011,4013,4016,4019,4022,4024,4027,4029,4031,4034,4036],{"class":106,"line":12},[104,4012,3834],{"class":122},[104,4014,4015],{"class":166}," swarm",[104,4017,4018],{"class":166}," join",[104,4020,4021],{"class":298}," --token",[104,4023,898],{"class":162},[104,4025,4026],{"class":166},"$TOKEN",[104,4028,904],{"class":162},[104,4030,898],{"class":162},[104,4032,4033],{"class":166},"$MANAGER",[104,4035,904],{"class":162},[104,4037,2746],{"class":298},[104,4039,4040,4043,4045,4048,4050],{"class":106,"line":21},[104,4041,4042],{"class":298},"  --advertise-addr",[104,4044,898],{"class":162},[104,4046,4047],{"class":166},"$FLOATING_IP",[104,4049,904],{"class":162},[104,4051,2746],{"class":298},[104,4053,4054,4057],{"class":106,"line":30},[104,4055,4056],{"class":298},"  --listen-addr",[104,4058,4059],{"class":166}," 0.0.0.0:2377\n",[82,4061,1253,4062,4065,4066,4069],{},[101,4063,4064],{},"--advertise-addr"," is set to the node's ",[3730,4067,4068],{},"floating IP"," — its external, publicly routable IP address. This was necessary because the Swarm managers live outside OpenStack, and the only way for an OpenStack node to reach them is via the external network.",[82,4071,4072],{},"But this has a side effect. Every other node in the Swarm — including other OpenStack nodes on the same internal subnet — now thinks the only way to reach that node is via its floating IP. So when the test node talks to the email service, even though they are on the same internal OpenStack network, the traffic takes this path:",[94,4074,4077],{"className":4075,"code":4076,"language":3669},[3667],"Test node (inside OpenStack)\n  → exits via floating IP through OpenStack router (NAT)\n    → hits external network\n      → HAProxy\n        → Traefik\n          → re-enters OpenStack via prod node floating IP (NAT)\n            → Email service container\n",[101,4078,4076],{"__ignoreMap":99},[82,4080,4081],{},"Two nodes sitting on the same internal subnet are taking a round trip through the external network to talk to each other. And each time a packet crosses the OpenStack network boundary through NAT, it picks up more overhead.",[82,4083,4084],{},"The monitoring node works because it is already outside OpenStack. Its traffic only crosses the boundary once — going in. No double NAT, less encapsulation pressure on each packet.",[2836,4086],{},[86,4088,4090],{"id":4089},"confirming-with-interface-inspection","Confirming With Interface Inspection",[82,4092,4093,4094,4097],{},"Running ",[101,4095,4096],{},"ip link show"," on the test node made the mismatch immediately visible:",[94,4099,4102],{"className":4100,"code":4101,"language":3669},[3667],"ens3:            MTU 1450   ← OpenStack network interface\ndocker0:         MTU 1500   ← Docker bridge, unaware\ndocker_gwbridge: MTU 1500   ← Docker gateway bridge, also unaware\nveth*:           MTU 1500   ← All container interfaces, also unaware\n",[101,4103,4101],{"__ignoreMap":99},[82,4105,4106,4107,4110],{},"The host network interface ",[101,4108,4109],{},"ens3"," is correctly at 1450 — OpenStack set it that way to account for VXLAN overhead. But every Docker interface on the same node is at 1500. Docker was never told about the OpenStack constraint.",[82,4112,4113,4114,4116],{},"So when a container builds a packet, it thinks it has 1500 bytes to work with. That packet travels through the veth interface, through the Docker gateway bridge, and then hits ",[101,4115,4109],{}," — which can only carry 1450 bytes. The oversized packet hits the wall and is silently dropped.",[2836,4118],{},[86,4120,4122],{"id":4121},"the-iptables-fix-that-worked","The Iptables Fix That Worked",[82,4124,4125],{},"Before understanding all of this fully, an iptables rule was applied on the test node:",[94,4127,4129],{"className":2725,"code":4128,"language":2727,"meta":99,"style":99},"sudo iptables -t mangle -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360\n",[101,4130,4131],{"__ignoreMap":99},[104,4132,4133,4136,4139,4142,4145,4148,4151,4154,4157,4160,4163,4166,4169,4172,4175],{"class":106,"line":12},[104,4134,4135],{"class":122},"sudo",[104,4137,4138],{"class":166}," iptables",[104,4140,4141],{"class":298}," -t",[104,4143,4144],{"class":166}," mangle",[104,4146,4147],{"class":298}," -I",[104,4149,4150],{"class":166}," FORWARD",[104,4152,4153],{"class":298}," -p",[104,4155,4156],{"class":166}," tcp",[104,4158,4159],{"class":298}," --tcp-flags",[104,4161,4162],{"class":166}," SYN,RST",[104,4164,4165],{"class":166}," SYN",[104,4167,4168],{"class":298}," -j",[104,4170,4171],{"class":166}," TCPMSS",[104,4173,4174],{"class":298}," --set-mss",[104,4176,4177],{"class":917}," 1360\n",[82,4179,4180],{},"And the emails started going through immediately.",[4182,4183,4185],"h3",{"id":4184},"what-does-this-actually-do","What Does This Actually Do?",[82,4187,4188],{},"To understand this fix, you need to know a little about how TCP connections work.",[82,4190,4191,4192,254],{},"When two machines want to talk over TCP (the protocol used for HTTP, HTTPS, and most internet traffic), they start with a handshake. During this handshake, both sides announce the largest chunk of data they are willing to receive at once. This is called the ",[3730,4193,4194],{},"Maximum Segment Size (MSS)",[3719,4196,4197],{},[82,4198,4199],{},"Think of it like two people agreeing on how many items to pass at once down a conveyor belt. If you agree on small batches, nothing gets dropped even if the belt has a narrow section somewhere in the middle.",[82,4201,4202],{},"The iptables rule intercepts the very first packet of every TCP connection (the SYN packet), and rewrites the MSS value to something smaller. Both sides then negotiate based on that smaller value, and the entire connection uses smaller chunks from the start. The oversized packet problem never occurs because the data is broken into pieces that fit.",[82,4204,1253,4205,4208,4209,4212],{},[101,4206,4207],{},"--set-mss 1360"," hardcodes the MSS to 1360 bytes. It works, but a smarter version uses ",[101,4210,4211],{},"--clamp-mss-to-pmtu"," instead:",[94,4214,4216],{"className":2725,"code":4215,"language":2727,"meta":99,"style":99},"iptables -t mangle -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu\n",[101,4217,4218],{"__ignoreMap":99},[104,4219,4220,4223,4225,4227,4229,4231,4233,4235,4237,4239,4241,4243,4245],{"class":106,"line":12},[104,4221,4222],{"class":122},"iptables",[104,4224,4141],{"class":298},[104,4226,4144],{"class":166},[104,4228,4147],{"class":298},[104,4230,4150],{"class":166},[104,4232,4153],{"class":298},[104,4234,4156],{"class":166},[104,4236,4159],{"class":298},[104,4238,4162],{"class":166},[104,4240,4165],{"class":166},[104,4242,4168],{"class":298},[104,4244,4171],{"class":166},[104,4246,4247],{"class":298}," --clamp-mss-to-pmtu\n",[82,4249,4250,4251,4253],{},"This tells the kernel to calculate the correct MSS automatically based on the actual outgoing interface MTU (1450 on ",[101,4252,4109],{},"), rather than using a hardcoded value. If the network MTU ever changes, the rule adapts automatically.",[2836,4255],{},[86,4257,4259],{"id":4258},"why-this-fix-and-not-the-docker-mtu-fix","Why This Fix and Not the Docker MTU Fix?",[82,4261,4262],{},"This is the important question. We could have:",[3756,4264,4265,4272,4275],{},[389,4266,4267,4268,4271],{},"Changed ",[101,4269,4270],{},"daemon.json"," to set Docker MTU to 1400",[389,4273,4274],{},"Recreated all overlay networks",[389,4276,4277],{},"Recreated the ingress network",[82,4279,4280],{},"But that approach would have caused significant downtime across all services for a problem that only affects outbound TCP from OpenStack nodes. It is the right fix if your Docker overlay traffic between nodes is dropping. It is overkill — and risky — when the actual problem is a specific outbound path.",[82,4282,4283],{},"The iptables TCPMSS approach:",[386,4285,4286,4289,4292,4295,4298],{},[389,4287,4288],{},"Touches nothing else in the stack",[389,4290,4291],{},"Requires no service restarts",[389,4293,4294],{},"Requires no network recreation",[389,4296,4297],{},"Only affects outbound TCP SYN packets from that node",[389,4299,4300],{},"Is invisible to services and containers",[82,4302,4303],{},"We confirmed this by checking the iptables rule counters after applying it:",[94,4305,4307],{"className":2725,"code":4306,"language":2727,"meta":99,"style":99},"sudo iptables -t mangle -L FORWARD -n -v --line-numbers\n\nChain FORWARD (policy ACCEPT 5894K packets, 2970M bytes)\nnum   pkts bytes target     prot opt in     out     source               destination\n1        6   360 TCPMSS     6    --  *      *       0.0.0.0\u002F0  0.0.0.0\u002F0  tcp flags:0x06\u002F0x02 TCPMSS clamp to PMTU\n",[101,4308,4309,4333,4337,4364,4395],{"__ignoreMap":99},[104,4310,4311,4313,4315,4317,4319,4322,4324,4327,4330],{"class":106,"line":12},[104,4312,4135],{"class":122},[104,4314,4138],{"class":166},[104,4316,4141],{"class":298},[104,4318,4144],{"class":166},[104,4320,4321],{"class":298}," -L",[104,4323,4150],{"class":166},[104,4325,4326],{"class":298}," -n",[104,4328,4329],{"class":298}," -v",[104,4331,4332],{"class":298}," --line-numbers\n",[104,4334,4335],{"class":106,"line":21},[104,4336,216],{"emptyLinePlaceholder":215},[104,4338,4339,4342,4344,4347,4350,4353,4356,4359,4362],{"class":106,"line":30},[104,4340,4341],{"class":122},"Chain",[104,4343,4150],{"class":166},[104,4345,4346],{"class":3059}," (policy ",[104,4348,4349],{"class":166},"ACCEPT",[104,4351,4352],{"class":166}," 5894K",[104,4354,4355],{"class":166}," packets,",[104,4357,4358],{"class":166}," 2970M",[104,4360,4361],{"class":166}," bytes",[104,4363,182],{"class":3059},[104,4365,4366,4369,4372,4374,4377,4380,4383,4386,4389,4392],{"class":106,"line":39},[104,4367,4368],{"class":122},"num",[104,4370,4371],{"class":166},"   pkts",[104,4373,4361],{"class":166},[104,4375,4376],{"class":166}," target",[104,4378,4379],{"class":166},"     prot",[104,4381,4382],{"class":166}," opt",[104,4384,4385],{"class":166}," in",[104,4387,4388],{"class":166},"     out",[104,4390,4391],{"class":166},"     source",[104,4393,4394],{"class":166},"               destination\n",[104,4396,4397,4400,4403,4406,4408,4411,4414,4417,4420,4423,4426,4429,4432,4434,4437,4440],{"class":106,"line":48},[104,4398,4399],{"class":122},"1",[104,4401,4402],{"class":917},"        6",[104,4404,4405],{"class":917},"   360",[104,4407,4171],{"class":166},[104,4409,4410],{"class":917},"     6",[104,4412,4413],{"class":298},"    --",[104,4415,4416],{"class":298},"  *",[104,4418,4419],{"class":298},"      *",[104,4421,4422],{"class":166},"       0.0.0.0\u002F0",[104,4424,4425],{"class":166},"  0.0.0.0\u002F0",[104,4427,4428],{"class":166},"  tcp",[104,4430,4431],{"class":166}," flags:0x06\u002F0x02",[104,4433,4171],{"class":166},[104,4435,4436],{"class":166}," clamp",[104,4438,4439],{"class":166}," to",[104,4441,4442],{"class":166}," PMTU\n",[82,4444,4445],{},"Only 6 packets — just the email test traffic. Browser traffic serving the client app was not going through the rule at all. The fix was surgical.",[2836,4447],{},[86,4449,4451],{"id":4450},"making-it-persistent-the-manual-node","Making It Persistent: The Manual Node",[82,4453,4454],{},"The iptables rule applied manually disappears on reboot. For the manually provisioned test node, the fix is:",[94,4456,4458],{"className":2725,"code":4457,"language":2727,"meta":99,"style":99},"# Remove the old hardcoded rule\nsudo iptables -t mangle -D FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360\n\n# Add the smarter adaptive rule\nsudo iptables -t mangle -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu\n\n# Install persistence\nsudo apt-get install -y iptables-persistent\nsudo netfilter-persistent save\n",[101,4459,4460,4465,4498,4502,4507,4537,4541,4546,4562],{"__ignoreMap":99},[104,4461,4462],{"class":106,"line":12},[104,4463,4464],{"class":109},"# Remove the old hardcoded rule\n",[104,4466,4467,4469,4471,4473,4475,4478,4480,4482,4484,4486,4488,4490,4492,4494,4496],{"class":106,"line":21},[104,4468,4135],{"class":122},[104,4470,4138],{"class":166},[104,4472,4141],{"class":298},[104,4474,4144],{"class":166},[104,4476,4477],{"class":298}," -D",[104,4479,4150],{"class":166},[104,4481,4153],{"class":298},[104,4483,4156],{"class":166},[104,4485,4159],{"class":298},[104,4487,4162],{"class":166},[104,4489,4165],{"class":166},[104,4491,4168],{"class":298},[104,4493,4171],{"class":166},[104,4495,4174],{"class":298},[104,4497,4177],{"class":917},[104,4499,4500],{"class":106,"line":30},[104,4501,216],{"emptyLinePlaceholder":215},[104,4503,4504],{"class":106,"line":39},[104,4505,4506],{"class":109},"# Add the smarter adaptive rule\n",[104,4508,4509,4511,4513,4515,4517,4519,4521,4523,4525,4527,4529,4531,4533,4535],{"class":106,"line":48},[104,4510,4135],{"class":122},[104,4512,4138],{"class":166},[104,4514,4141],{"class":298},[104,4516,4144],{"class":166},[104,4518,4147],{"class":298},[104,4520,4150],{"class":166},[104,4522,4153],{"class":298},[104,4524,4156],{"class":166},[104,4526,4159],{"class":298},[104,4528,4162],{"class":166},[104,4530,4165],{"class":166},[104,4532,4168],{"class":298},[104,4534,4171],{"class":166},[104,4536,4247],{"class":298},[104,4538,4539],{"class":106,"line":57},[104,4540,216],{"emptyLinePlaceholder":215},[104,4542,4543],{"class":106,"line":248},[104,4544,4545],{"class":109},"# Install persistence\n",[104,4547,4548,4550,4553,4556,4559],{"class":106,"line":306},[104,4549,4135],{"class":122},[104,4551,4552],{"class":166}," apt-get",[104,4554,4555],{"class":166}," install",[104,4557,4558],{"class":298}," -y",[104,4560,4561],{"class":166}," iptables-persistent\n",[104,4563,4564,4566,4569],{"class":106,"line":312},[104,4565,4135],{"class":122},[104,4567,4568],{"class":166}," netfilter-persistent",[104,4570,4571],{"class":166}," save\n",[82,4573,4574,4577],{},[101,4575,4576],{},"iptables-persistent"," saves the current rules to disk and restores them automatically on every boot.",[2836,4579],{},[86,4581,4583],{"id":4582},"making-it-automatic-the-asg-nodes","Making It Automatic: The ASG Nodes",[82,4585,4586],{},"The bigger concern was the Auto Scaling Group. Our OpenStack ASG spins up new worker nodes automatically when load increases. Each new node is an OpenStack VM and would have the same MTU mismatch out of the box. If a service happened to land on a new ASG node and made outbound HTTP calls, it would silently fail — and we might not notice until something like an email timeout surfaced it.",[82,4588,4589,4590,4593],{},"The fix belongs in the ",[101,4591,4592],{},"user_data"," cloud-init script that runs on every new node at boot. In our Heat template, right after the Swarm join:",[94,4595,4597],{"className":2725,"code":4596,"language":2727,"meta":99,"style":99},"echo \"Joining swarm at ${MANAGER} advertising ${FLOATING_IP}...\"\ndocker swarm join --token \"$TOKEN\" \"$MANAGER\" \\\n  --advertise-addr \"$FLOATING_IP\" \\\n  --listen-addr 0.0.0.0:2377\necho \"Swarm join complete.\"\n\n# ── Fix MTU mismatch between Docker (1500) and OpenStack interface (1450) ──\necho \"--- Applying MTU fix ---\"\necho \"Host interface MTU before fix:\"\nip link show ens3 | grep mtu\n\necho \"Applying TCPMSS clamp rule...\"\niptables -t mangle -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu\necho \"TCPMSS rule applied.\"\n\necho \"Verifying rule:\"\niptables -t mangle -L FORWARD -n -v --line-numbers\n\necho \"Installing iptables-persistent...\"\nDEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent\necho \"iptables-persistent installed.\"\n\necho \"Saving rules...\"\nnetfilter-persistent save\necho \"Rules saved.\"\n\necho \"--- MTU fix complete ---\"\n",[101,4598,4599,4631,4655,4667,4673,4684,4688,4693,4704,4715,4738,4742,4753,4781,4792,4796,4807,4825,4829,4840,4858,4869,4873,4885,4893,4905,4910],{"__ignoreMap":99},[104,4600,4601,4604,4606,4609,4611,4614,4616,4619,4621,4624,4626,4629],{"class":106,"line":12},[104,4602,4603],{"class":1143},"echo",[104,4605,898],{"class":162},[104,4607,4608],{"class":166},"Joining swarm at ",[104,4610,278],{"class":126},[104,4612,4613],{"class":166},"MANAGER",[104,4615,295],{"class":126},[104,4617,4618],{"class":166}," advertising ",[104,4620,278],{"class":126},[104,4622,4623],{"class":166},"FLOATING_IP",[104,4625,295],{"class":126},[104,4627,4628],{"class":166},"...",[104,4630,1999],{"class":162},[104,4632,4633,4635,4637,4639,4641,4643,4645,4647,4649,4651,4653],{"class":106,"line":21},[104,4634,3834],{"class":122},[104,4636,4015],{"class":166},[104,4638,4018],{"class":166},[104,4640,4021],{"class":298},[104,4642,898],{"class":162},[104,4644,4026],{"class":166},[104,4646,904],{"class":162},[104,4648,898],{"class":162},[104,4650,4033],{"class":166},[104,4652,904],{"class":162},[104,4654,2746],{"class":298},[104,4656,4657,4659,4661,4663,4665],{"class":106,"line":30},[104,4658,4042],{"class":298},[104,4660,898],{"class":162},[104,4662,4047],{"class":166},[104,4664,904],{"class":162},[104,4666,2746],{"class":298},[104,4668,4669,4671],{"class":106,"line":39},[104,4670,4056],{"class":298},[104,4672,4059],{"class":166},[104,4674,4675,4677,4679,4682],{"class":106,"line":48},[104,4676,4603],{"class":1143},[104,4678,898],{"class":162},[104,4680,4681],{"class":166},"Swarm join complete.",[104,4683,1999],{"class":162},[104,4685,4686],{"class":106,"line":57},[104,4687,216],{"emptyLinePlaceholder":215},[104,4689,4690],{"class":106,"line":248},[104,4691,4692],{"class":109},"# ── Fix MTU mismatch between Docker (1500) and OpenStack interface (1450) ──\n",[104,4694,4695,4697,4699,4702],{"class":106,"line":306},[104,4696,4603],{"class":1143},[104,4698,898],{"class":162},[104,4700,4701],{"class":166},"--- Applying MTU fix ---",[104,4703,1999],{"class":162},[104,4705,4706,4708,4710,4713],{"class":106,"line":312},[104,4707,4603],{"class":1143},[104,4709,898],{"class":162},[104,4711,4712],{"class":166},"Host interface MTU before fix:",[104,4714,1999],{"class":162},[104,4716,4717,4720,4723,4726,4729,4732,4735],{"class":106,"line":317},[104,4718,4719],{"class":122},"ip",[104,4721,4722],{"class":166}," link",[104,4724,4725],{"class":166}," show",[104,4727,4728],{"class":166}," ens3",[104,4730,4731],{"class":130}," |",[104,4733,4734],{"class":122}," grep",[104,4736,4737],{"class":166}," mtu\n",[104,4739,4740],{"class":106,"line":341},[104,4741,216],{"emptyLinePlaceholder":215},[104,4743,4744,4746,4748,4751],{"class":106,"line":377},[104,4745,4603],{"class":1143},[104,4747,898],{"class":162},[104,4749,4750],{"class":166},"Applying TCPMSS clamp rule...",[104,4752,1999],{"class":162},[104,4754,4755,4757,4759,4761,4763,4765,4767,4769,4771,4773,4775,4777,4779],{"class":106,"line":1019},[104,4756,4222],{"class":122},[104,4758,4141],{"class":298},[104,4760,4144],{"class":166},[104,4762,4147],{"class":298},[104,4764,4150],{"class":166},[104,4766,4153],{"class":298},[104,4768,4156],{"class":166},[104,4770,4159],{"class":298},[104,4772,4162],{"class":166},[104,4774,4165],{"class":166},[104,4776,4168],{"class":298},[104,4778,4171],{"class":166},[104,4780,4247],{"class":298},[104,4782,4783,4785,4787,4790],{"class":106,"line":1024},[104,4784,4603],{"class":1143},[104,4786,898],{"class":162},[104,4788,4789],{"class":166},"TCPMSS rule applied.",[104,4791,1999],{"class":162},[104,4793,4794],{"class":106,"line":1029},[104,4795,216],{"emptyLinePlaceholder":215},[104,4797,4798,4800,4802,4805],{"class":106,"line":1542},[104,4799,4603],{"class":1143},[104,4801,898],{"class":162},[104,4803,4804],{"class":166},"Verifying rule:",[104,4806,1999],{"class":162},[104,4808,4809,4811,4813,4815,4817,4819,4821,4823],{"class":106,"line":2654},[104,4810,4222],{"class":122},[104,4812,4141],{"class":298},[104,4814,4144],{"class":166},[104,4816,4321],{"class":298},[104,4818,4150],{"class":166},[104,4820,4326],{"class":298},[104,4822,4329],{"class":298},[104,4824,4332],{"class":298},[104,4826,4827],{"class":106,"line":2662},[104,4828,216],{"emptyLinePlaceholder":215},[104,4830,4831,4833,4835,4838],{"class":106,"line":2679},[104,4832,4603],{"class":1143},[104,4834,898],{"class":162},[104,4836,4837],{"class":166},"Installing iptables-persistent...",[104,4839,1999],{"class":162},[104,4841,4842,4845,4847,4850,4852,4854,4856],{"class":106,"line":2684},[104,4843,4844],{"class":137},"DEBIAN_FRONTEND",[104,4846,3090],{"class":126},[104,4848,4849],{"class":166},"noninteractive",[104,4851,4552],{"class":122},[104,4853,4555],{"class":166},[104,4855,4558],{"class":298},[104,4857,4561],{"class":166},[104,4859,4860,4862,4864,4867],{"class":106,"line":2692},[104,4861,4603],{"class":1143},[104,4863,898],{"class":162},[104,4865,4866],{"class":166},"iptables-persistent installed.",[104,4868,1999],{"class":162},[104,4870,4871],{"class":106,"line":2700},[104,4872,216],{"emptyLinePlaceholder":215},[104,4874,4876,4878,4880,4883],{"class":106,"line":4875},23,[104,4877,4603],{"class":1143},[104,4879,898],{"class":162},[104,4881,4882],{"class":166},"Saving rules...",[104,4884,1999],{"class":162},[104,4886,4888,4891],{"class":106,"line":4887},24,[104,4889,4890],{"class":122},"netfilter-persistent",[104,4892,4571],{"class":166},[104,4894,4896,4898,4900,4903],{"class":106,"line":4895},25,[104,4897,4603],{"class":1143},[104,4899,898],{"class":162},[104,4901,4902],{"class":166},"Rules saved.",[104,4904,1999],{"class":162},[104,4906,4908],{"class":106,"line":4907},26,[104,4909,216],{"emptyLinePlaceholder":215},[104,4911,4913,4915,4917,4920],{"class":106,"line":4912},27,[104,4914,4603],{"class":1143},[104,4916,898],{"class":162},[104,4918,4919],{"class":166},"--- MTU fix complete ---",[104,4921,1999],{"class":162},[82,4923,1253,4924,4927,4928,4931],{},[101,4925,4926],{},"DEBIAN_FRONTEND=noninteractive"," flag is important. Without it, ",[101,4929,4930],{},"apt-get install iptables-persistent"," will pause and wait for interactive input asking whether to save current IPv4 and IPv6 rules — something that cannot happen in an automated script. The flag suppresses all prompts.",[82,4933,4934,4935,4938],{},"Every new ASG node now gets the fix automatically at boot, and the log at ",[101,4936,4937],{},"\u002Fvar\u002Flog\u002Fswarm-setup.log"," will contain a full trace of the MTU fix running, so you can verify it without SSHing into the node.",[2836,4940],{},[86,4942,4944],{"id":4943},"what-we-did-not-need-to-do","What We Did Not Need to Do",[82,4946,4947,4948,4951],{},"It is worth being explicit about this. The following changes that are commonly suggested for MTU problems in Docker Swarm were ",[3730,4949,4950],{},"not needed"," for our specific situation:",[386,4953,4954,4960,4963,4966,4972,4975],{},[389,4955,4956,4957,4959],{},"❌ Changing ",[101,4958,4270],{}," MTU",[389,4961,4962],{},"❌ Recreating overlay networks",[389,4964,4965],{},"❌ Recreating the ingress network",[389,4967,4968,4969],{},"❌ Changing host interface MTU with ",[101,4970,4971],{},"ip link set",[389,4973,4974],{},"❌ Draining any nodes",[389,4976,4977],{},"❌ Any service restarts",[82,4979,4980],{},"The reason is that our problem was not in the Docker overlay between nodes. It was in outbound TCP from containers on OpenStack nodes going through a double-NAT path. The TCPMSS clamp fixed it at exactly the right layer.",[2836,4982],{},[86,4984,4986],{"id":4985},"lessons-learned","Lessons Learned",[82,4988,4989,4992],{},[3730,4990,4991],{},"1. Trace the actual traffic path before deciding where to fix.","\nMTU problems in hybrid environments are rarely a single-layer issue. Our traffic was going: container → Docker gateway bridge → OpenStack interface → external network → HAProxy → Traefik → back into OpenStack. Understanding that path was what led us to the right fix.",[82,4994,4995,4998],{},[3730,4996,4997],{},"2. Asymmetry in failures is a signal, not noise.","\nThe fact that the monitoring node worked but the test node did not was the most important clue. Same destination, same service, different result. That asymmetry pointed directly at the source node's network path being different — which led us to the floating IP and double-NAT discovery.",[82,5000,5001,5004],{},[3730,5002,5003],{},"3. The standard fix is not always the right fix.","\nThe Docker daemon MTU approach is correct for overlay network MTU mismatches. But applying it blindly would have caused unnecessary downtime and not addressed the root cause.",[82,5006,5007,5010],{},[3730,5008,5009],{},"4. Bake infrastructure fixes into provisioning, not just running nodes.","\nFixing the running node is only half the job. If your ASG spins up ten new nodes tomorrow and they all have the same problem, you will be chasing the same fire. The fix belongs in the provisioning script.",[82,5012,5013,5016],{},[3730,5014,5015],{},"5. Silent drops are the hardest bugs.","\nNo error. No ICMP response. No log entry on the receiving side. Just a timeout on the sender. These are the bugs that can send you chasing application code, DNS, TLS, or service configuration for hours before you think to check MTU.",[2836,5018],{},[86,5020,5022],{"id":5021},"summary","Summary",[3930,5024,5025,5035],{},[3933,5026,5027],{},[3936,5028,5029,5032],{},[3939,5030,5031],{},"What we thought the problem was",[3939,5033,5034],{},"Docker overlay MTU mismatch",[3949,5036,5037,5045,5053,5061,5069,5080],{},[3936,5038,5039,5042],{},[3954,5040,5041],{},"What the problem actually was",[3954,5043,5044],{},"Docker (MTU 1500) vs OpenStack interface (MTU 1450) mismatch on outbound TCP from OpenStack nodes",[3936,5046,5047,5050],{},[3954,5048,5049],{},"Why it only affected OpenStack nodes",[3954,5051,5052],{},"Outside nodes have real 1500 MTU interfaces with no mismatch",[3936,5054,5055,5058],{},[3954,5056,5057],{},"Why monitoring worked but test node failed",[3954,5059,5060],{},"Monitoring node is outside OpenStack, only crosses the boundary once",[3936,5062,5063,5066],{},[3954,5064,5065],{},"The fix",[3954,5067,5068],{},"TCPMSS iptables clamp on each OpenStack node",[3936,5070,5071,5074],{},[3954,5072,5073],{},"Where the fix lives",[3954,5075,5076,5077,5079],{},"Manually on existing nodes, baked into Heat ",[101,5078,4592],{}," for ASG nodes",[3936,5081,5082,5085],{},[3954,5083,5084],{},"What we avoided",[3954,5086,5087],{},"Any Docker network changes, downtime, service restarts",[82,5089,5090],{},"The infrastructure is hybrid by design and will stay that way until OpenStack proves itself reliable enough to trust fully. In the meantime, understanding exactly how packets move through a mixed environment — and where they can silently disappear — is what keeps things running.",[409,5092,5093],{},"html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .sEi1f, html code.shiki .sEi1f{--shiki-default:#A65E2B}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}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 .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .suHK_, html code.shiki .suHK_{--shiki-default:#393A34}html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}html pre.shiki code .su6XF, html code.shiki .su6XF{--shiki-default:#998418}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}html pre.shiki code .s61at, html code.shiki .s61at{--shiki-default:#99841877}",{"title":99,"searchDepth":21,"depth":21,"links":5095},[5096,5097,5098,5099,5100,5101,5102,5103,5104,5107,5108,5109,5110,5111,5112],{"id":3653,"depth":21,"text":3654},{"id":3695,"depth":21,"text":3696},{"id":3710,"depth":21,"text":3711},{"id":3808,"depth":21,"text":3809},{"id":3820,"depth":21,"text":3821},{"id":3924,"depth":21,"text":3925},{"id":3999,"depth":21,"text":4000},{"id":4089,"depth":21,"text":4090},{"id":4121,"depth":21,"text":4122,"children":5105},[5106],{"id":4184,"depth":30,"text":4185},{"id":4258,"depth":21,"text":4259},{"id":4450,"depth":21,"text":4451},{"id":4582,"depth":21,"text":4583},{"id":4943,"depth":21,"text":4944},{"id":4985,"depth":21,"text":4986},{"id":5021,"depth":21,"text":5022},{},"\u002Fblog\u002Fmtu-troubleshooting-blog",{"title":3640,"description":2790},"blog\u002Fmtu-troubleshooting-blog",[2797,2798,2799,2800],"\u002Fimages\u002Fthumbnails\u002Fmtu-troubleshooting-blog.png","0szugP3ReobiA0PjsiX4nQ4WIyeMV6m3ebTfvSLMwUg",{"id":5121,"title":5122,"body":5123,"date":5253,"description":5254,"extension":418,"meta":5255,"navigation":215,"path":5256,"readTime":5257,"seo":5258,"stem":5259,"tags":5260,"thumbnail":5263,"__hash__":5264},"blog\u002Fblog\u002Fserverless-data-architecture.md","The Transition to Serverless Data Architecture",{"type":79,"value":5124,"toc":5247},[5125,5128,5132,5135,5142,5146,5149,5163,5166,5170,5173,5231,5234,5238,5241,5244],[82,5126,5127],{},"I ran a self-hosted Postgres cluster on OpenStack for two years. Then I moved the same workload to a managed service. This post is an honest accounting of what changed — the good and the bad.",[86,5129,5131],{"id":5130},"what-you-give-up-running-it-yourself","What You Give Up Running It Yourself",[82,5133,5134],{},"Self-hosting gives you control, but control is a liability masquerading as an asset. Every tuning decision, every WAL configuration, every failover test is time you're not spending on the product. Our DBA runbook was 40 pages long and only one person had actually read all of it.",[82,5136,5137,5138,5141],{},"The cost of expertise compounds. When the primary went down at 2am on a Saturday, someone had to know what ",[101,5139,5140],{},"pg_ctl promote"," does and when to run it.",[86,5143,5145],{"id":5144},"what-you-actually-get-from-managed-services","What You Actually Get from Managed Services",[82,5147,5148],{},"Neon gave us:",[386,5150,5151,5154,5157,5160],{},[389,5152,5153],{},"Branching — spin up a full copy of the database for a PR in under 30 seconds",[389,5155,5156],{},"Automatic point-in-time recovery with a simple API call",[389,5158,5159],{},"Scale-to-zero during off-hours (real savings on non-critical environments)",[389,5161,5162],{},"Connection pooling via PgBouncer baked in",[82,5164,5165],{},"The branching feature alone changed how we do development. Before, dev environments shared a staging database because spinning up a fresh Postgres with a full dataset was too slow. Now it's a single CLI command.",[86,5167,5169],{"id":5168},"the-migration","The Migration",[82,5171,5172],{},"We ran both systems in parallel for three weeks using logical replication:",[94,5174,5177],{"className":5175,"code":5176,"language":1343,"meta":99,"style":99},"language-sql shiki shiki-themes vitesse-light","-- On the source (self-hosted)\nCREATE PUBLICATION migration_pub FOR ALL TABLES;\n\n-- On the target (Neon)\nCREATE SUBSCRIPTION migration_sub\n  CONNECTION 'host=old-primary ...'\n  PUBLICATION migration_pub;\n",[101,5178,5179,5184,5198,5202,5207,5214,5226],{"__ignoreMap":99},[104,5180,5181],{"class":106,"line":12},[104,5182,5183],{"class":109},"-- On the source (self-hosted)\n",[104,5185,5186,5189,5192,5195],{"class":106,"line":21},[104,5187,5188],{"class":115},"CREATE",[104,5190,5191],{"class":3059}," PUBLICATION migration_pub ",[104,5193,5194],{"class":115},"FOR",[104,5196,5197],{"class":3059}," ALL TABLES;\n",[104,5199,5200],{"class":106,"line":30},[104,5201,216],{"emptyLinePlaceholder":215},[104,5203,5204],{"class":106,"line":39},[104,5205,5206],{"class":109},"-- On the target (Neon)\n",[104,5208,5209,5211],{"class":106,"line":48},[104,5210,5188],{"class":115},[104,5212,5213],{"class":3059}," SUBSCRIPTION migration_sub\n",[104,5215,5216,5219,5221,5224],{"class":106,"line":57},[104,5217,5218],{"class":115},"  CONNECTION",[104,5220,163],{"class":162},[104,5222,5223],{"class":166},"host=old-primary ...",[104,5225,3299],{"class":162},[104,5227,5228],{"class":106,"line":248},[104,5229,5230],{"class":3059},"  PUBLICATION migration_pub;\n",[82,5232,5233],{},"Logical replication gave us a live feed of changes. The final cutover was a matter of updating the connection string in the Nuxt runtime config and flipping DNS — 4 seconds of write downtime, confirmed by our uptime monitor.",[86,5235,5237],{"id":5236},"where-self-hosting-still-wins","Where Self-Hosting Still Wins",[82,5239,5240],{},"If you have regulatory requirements that prohibit data leaving a specific jurisdiction, managed services often can't help. Likewise, if your data access patterns are unusual enough that you need to tune kernel parameters or run custom Postgres extensions not available in managed offerings, self-hosting remains necessary.",[82,5242,5243],{},"But for most product workloads? The managed path recovers 6–10 hours of engineering time per month and eliminates a class of 2am incidents entirely. That's the real calculation.",[409,5245,5246],{},"html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .suHK_, html code.shiki .suHK_{--shiki-default:#393A34}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}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":99,"searchDepth":21,"depth":21,"links":5248},[5249,5250,5251,5252],{"id":5130,"depth":21,"text":5131},{"id":5144,"depth":21,"text":5145},{"id":5168,"depth":21,"text":5169},{"id":5236,"depth":21,"text":5237},"2026-03-10","Evaluating the real trade-offs between managed database services and self-hosted high-availability clusters — after running both in production.",{},"\u002Fblog\u002Fserverless-data-architecture","6 min",{"title":5122,"description":5254},"blog\u002Fserverless-data-architecture",[5261,2800,5262,728],"Database","PostgreSQL","\u002Fimages\u002Fthumbnails\u002Fserverless-data-architecture.png","5puaNbRfvwcsew1zr4NPBWVcp6vptf8ytTvNFyvu5x0",{"id":5266,"title":5267,"body":5268,"date":5411,"description":5412,"extension":418,"meta":5413,"navigation":215,"path":5414,"readTime":5415,"seo":5416,"stem":5417,"tags":5418,"thumbnail":5422,"__hash__":5423},"blog\u002Fblog\u002Fwelcome-digital-sandbox.md","🚀 Welcome to My Digital Sandbox!",{"type":79,"value":5269,"toc":5406},[5270,5277,5290,5297,5301,5308,5311,5314,5321,5327,5331,5342,5345,5347,5351,5354,5394,5400],[82,5271,5272,5276],{},[5273,5274],"mention",{"name":5275},"Joe"," thinks I’m doing some amazing stuff and honestly, he wanted a front-row seat to read about it.",[82,5278,5279,5281,5282,5285,5286,5289],{},[5273,5280],{"name":5275}," and I spend ",[3646,5283,5284],{},"a lot"," of time on the phone geeking out over system ideas, software architectures, and the future of tech. Our current record? A massive ",[3730,5287,5288],{},"1.2-hour WhatsApp voice call"," just syncing up and talking shop about this wild field of ours.",[82,5291,5292],{},[5293,5294],"img",{"alt":5295,"src":5296},"Geeking Out over Tech","https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExM3N6Z3oxM295M3Z1bnduMms0ZXBtZ3M4b3A4dmJ6bGR5cjRyeXN6ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002F3knKct3vGqxhK\u002Fgiphy.gif",[4182,5298,5300],{"id":5299},"the-ai-superpower-the-80-trap","⚡ The AI Superpower & The \"80% Trap\"",[82,5302,5303,5304,5307],{},"Fortunately for me—and I mean ",[3646,5305,5306],{},"fortunately","—AI has completely changed the game.",[82,5309,5310],{},"I use AI to move at breakneck speed. I can spin up prototypes, test out wild ideas, and ditch them if they don't work faster than it used to take just to configure a boilerplate setup.",[82,5312,5313],{},"⚡ IDEAS -> PROTOTYPE -> NEXT BIG THING",[82,5315,5316,5317,5320],{},"I'm the kind of engineer who always needs something active to chew on. Like many creatives, I suffer from ",[3730,5318,5319],{},"\"Shiny Object Syndrome\"","—you know, that classic trap where you hit 80% completion, the core structural problems are solved, you lose interest, and your brain screams for a new challenge. Because of that, I am constantly hunting for new ideas and fresh projects.",[82,5322,5323],{},[5293,5324],{"alt":5325,"src":5326},"Moving Fast and Breaking Things","https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExM3F0ZnNxZ3F3b3Rndm85bXJ6am8xb3Z5aG83YmE0bHd5ZTN0amVxdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002FunQ3IJU2RG7DO\u002Fgiphy.gif",[4182,5328,5330],{"id":5329},"dusting-off-the-frontend-goodbye-for-now-c","🎨 Dusting off the Frontend (Goodbye for now, C#!)",[82,5332,5333,5334,5337,5338,5341],{},"I put this portfolio together to test and sharpen my ",[3730,5335,5336],{},"Vue\u002FNuxt"," skills. It’s been about 4 months since my last Vue ecosystem project because, honestly... ",[3730,5339,5340],{},"C# has completely taken over my life lately!"," 🧱",[82,5343,5344],{},"So, back to the point: I am going to spend some time writing about all the fun, chaotic things I encounter as I dive into the uncomfortable—breaking things, fixing them, and learning on the fly. I already have a few of these write-ups sitting in my Notion, so I’ll be migrating them over here soon.",[2836,5346],{},[86,5348,5350],{"id":5349},"️-what-can-you-expect-here","🗺️ What Can You Expect Here?",[82,5352,5353],{},"Here is a roadmap of the chaos and insights I'll be dumping into this space:",[386,5355,5356,5362,5370,5376,5382,5388],{},[389,5357,5358,5361],{},[3730,5359,5360],{},"🏢 On-Premise Infrastructure:"," I want to keep my bare-metal and self-hosted knowledge alive and kicking, so expect deep dives into hardware and infrastructure management.",[389,5363,5364,5367,5368,254],{},[3730,5365,5366],{},"🐍 Python Tips & Tricks:"," It's been a minute since I touched Django, so expect clean, modern ways of doing things using ",[3730,5369,3632],{},[389,5371,5372,5375],{},[3730,5373,5374],{},"🔷 C# Mastery:"," I write C# daily. Whenever I find a clever optimization at work (that I can freely share), or stumble on something fun, it's going right here.",[389,5377,5378,5381],{},[3730,5379,5380],{},"🐳 Docker, Swarm, & Linux:"," I’ve spent a lot of time breaking and fixing things in cluster environments. I've got a lot of hard-learned lessons to share.",[389,5383,5384,5387],{},[3730,5385,5386],{},"🤖 AI & RAG:"," Ha! I have an active Retrieval-Augmented Generation (RAG) project doing some incredibly cool things. I’ll be breaking down how it works and how I leverage AI daily.",[389,5389,5390,5393],{},[3730,5391,5392],{},"🌱 Life & Daily Learnings:"," General thoughts, philosophical brain dumps, and a running log of the new things I learn every single day.",[82,5395,5396],{},[5293,5397],{"alt":5398,"src":5399},"Let's Build","https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExbmswM3g4amw0NndpZ3V5cm15NmFyeXN6bTZtc3Z6bndvbmNndXFhNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002FYl5aO3gdVfsQ0\u002Fgiphy.gif",[82,5401,5402,5405],{},[3646,5403,5404],{},"Stay tuned. We're going to break some things!"," 🔥",{"title":99,"searchDepth":21,"depth":21,"links":5407},[5408,5409,5410],{"id":5299,"depth":30,"text":5300},{"id":5329,"depth":30,"text":5330},{"id":5349,"depth":21,"text":5350},"2026-05-18","Joe thinks I’m doing some amazing stuff and honestly, he wanted a front-row seat to read about it.",{},"\u002Fblog\u002Fwelcome-digital-sandbox","2 min",{"title":5267,"description":5412},"blog\u002Fwelcome-digital-sandbox",[5419,5420,5421,5275],"Intro","AI","Vue","\u002Fimages\u002Fthumbnails\u002Fdefault-og.png","hAqc8DbFG0UL-U5LDqx02VYU4PefMTXwwLjuDtlfwNY",1779361988966]