[{"data":1,"prerenderedAt":3109},["ShallowReactive",2],{"nav-stories":3,"footer-stories":61,"blog-list":74},[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,439,669,1316,2803,2950],{"id":76,"title":77,"body":78,"date":424,"description":425,"extension":426,"meta":427,"navigation":144,"path":428,"readTime":429,"seo":430,"stem":431,"tags":432,"thumbnail":437,"__hash__":438},"blog\u002Fblog\u002Fbuilding-swarm-cluster-openstack.md","Building a Highly Available Swarm Cluster on OpenStack",{"type":79,"value":80,"toc":418},"minimark",[81,85,90,93,96,100,108,341,344,348,355,400,407,411,414],[82,83,84],"p",{},"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,87,89],"h2",{"id":88},"the-architecture","The Architecture",[82,91,92],{},"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,94,95],{},"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,97,99],{"id":98},"network-configuration","Network Configuration",[82,101,102,103,107],{},"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 ",[104,105,106],"code",{},"docker-compose.yml"," deployed as a stack.",[109,110,115],"pre",{"className":111,"code":112,"language":113,"meta":114,"style":114},"language-yaml shiki shiki-themes vitesse-light","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","yaml","",[104,116,117,140,146,154,161,171,178,191,203,215,227,235,247,259,267,275,283,291,308,313,321,329],{"__ignoreMap":114},[118,119,121,125,129,133,137],"span",{"class":120,"line":12},"line",[118,122,124],{"class":123},"su6XF","version",[118,126,128],{"class":127},"sYZai",":",[118,130,132],{"class":131},"sSP4y"," \"",[118,134,136],{"class":135},"spphp","3.8",[118,138,139],{"class":131},"\"\n",[118,141,142],{"class":120,"line":21},[118,143,145],{"emptyLinePlaceholder":144},true,"\n",[118,147,148,151],{"class":120,"line":30},[118,149,150],{"class":123},"services",[118,152,153],{"class":127},":\n",[118,155,156,159],{"class":120,"line":39},[118,157,158],{"class":123},"  traefik",[118,160,153],{"class":127},[118,162,163,166,168],{"class":120,"line":48},[118,164,165],{"class":123},"    image",[118,167,128],{"class":127},[118,169,170],{"class":135}," traefik:v2.9\n",[118,172,173,176],{"class":120,"line":57},[118,174,175],{"class":123},"    command",[118,177,153],{"class":127},[118,179,181,184,186,189],{"class":120,"line":180},7,[118,182,183],{"class":127},"      -",[118,185,132],{"class":131},[118,187,188],{"class":135},"--api.insecure=true",[118,190,139],{"class":131},[118,192,194,196,198,201],{"class":120,"line":193},8,[118,195,183],{"class":127},[118,197,132],{"class":131},[118,199,200],{"class":135},"--providers.docker=true",[118,202,139],{"class":131},[118,204,206,208,210,213],{"class":120,"line":205},9,[118,207,183],{"class":127},[118,209,132],{"class":131},[118,211,212],{"class":135},"--providers.docker.swarmMode=true",[118,214,139],{"class":131},[118,216,218,220,222,225],{"class":120,"line":217},10,[118,219,183],{"class":127},[118,221,132],{"class":131},[118,223,224],{"class":135},"--entrypoints.web.address=:80",[118,226,139],{"class":131},[118,228,230,233],{"class":120,"line":229},11,[118,231,232],{"class":123},"    ports",[118,234,153],{"class":127},[118,236,238,240,242,245],{"class":120,"line":237},12,[118,239,183],{"class":127},[118,241,132],{"class":131},[118,243,244],{"class":135},"80:80",[118,246,139],{"class":131},[118,248,250,252,254,257],{"class":120,"line":249},13,[118,251,183],{"class":127},[118,253,132],{"class":131},[118,255,256],{"class":135},"8080:8080",[118,258,139],{"class":131},[118,260,262,265],{"class":120,"line":261},14,[118,263,264],{"class":123},"    networks",[118,266,153],{"class":127},[118,268,270,272],{"class":120,"line":269},15,[118,271,183],{"class":127},[118,273,274],{"class":135}," proxy\n",[118,276,278,281],{"class":120,"line":277},16,[118,279,280],{"class":123},"    deploy",[118,282,153],{"class":127},[118,284,286,289],{"class":120,"line":285},17,[118,287,288],{"class":123},"      placement",[118,290,153],{"class":127},[118,292,294,297,299,302,305],{"class":120,"line":293},18,[118,295,296],{"class":123},"        constraints",[118,298,128],{"class":127},[118,300,301],{"class":127}," [",[118,303,304],{"class":135},"node.role == manager",[118,306,307],{"class":127},"]\n",[118,309,311],{"class":120,"line":310},19,[118,312,145],{"emptyLinePlaceholder":144},[118,314,316,319],{"class":120,"line":315},20,[118,317,318],{"class":123},"networks",[118,320,153],{"class":127},[118,322,324,327],{"class":120,"line":323},21,[118,325,326],{"class":123},"  proxy",[118,328,153],{"class":127},[118,330,332,335,337],{"class":120,"line":331},22,[118,333,334],{"class":123},"    external",[118,336,128],{"class":127},[118,338,340],{"class":339},"sbBg2"," true\n",[82,342,343],{},"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,345,347],{"id":346},"automated-scaling","Automated Scaling",[82,349,350,351,354],{},"Using ",[104,352,353],{},"swarmctl"," alongside custom Prometheus metrics allows us to dynamically provision worker nodes when CPU load exceeds our baseline threshold.",[109,356,360],{"className":357,"code":358,"language":359,"meta":114,"style":114},"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",[104,361,362,381,388,395],{"__ignoreMap":114},[118,363,364,368,371,374,377],{"class":120,"line":12},[118,365,367],{"class":366},"sySUi","$",[118,369,370],{"class":135}," swarmctl",[118,372,373],{"class":135}," cluster",[118,375,376],{"class":135}," update",[118,378,380],{"class":379},"sEi1f"," \\\n",[118,382,383,386],{"class":120,"line":21},[118,384,385],{"class":379},"    --autoscale-cpu-threshold=75",[118,387,380],{"class":379},[118,389,390,393],{"class":120,"line":30},[118,391,392],{"class":379},"    --autoscale-max-nodes=10",[118,394,380],{"class":379},[118,396,397],{"class":120,"line":39},[118,398,399],{"class":379},"    --cluster-name=prod-swarm-alpha\n",[82,401,402,403,406],{},"When the system detects a sustained spike, the OpenStack API is triggered to boot a new instance, execute a ",[104,404,405],{},"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,408,410],{"id":409},"whats-next","What's Next",[82,412,413],{},"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.",[415,416,417],"style",{},"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":114,"searchDepth":21,"depth":21,"links":419},[420,421,422,423],{"id":88,"depth":21,"text":89},{"id":98,"depth":21,"text":99},{"id":346,"depth":21,"text":347},{"id":409,"depth":21,"text":410},"2026-05-14","A deep dive into networking, resource limits, and automated scaling strategies for Docker Swarm on OpenStack.","md",{},"\u002Fblog\u002Fbuilding-swarm-cluster-openstack","5 min",{"title":77,"description":425},"blog\u002Fbuilding-swarm-cluster-openstack",[433,434,435,436],"Docker","OpenStack","DevOps","Infrastructure","\u002Fimages\u002Fthumbnails\u002Fbuilding-swarm-cluster-openstack.png","No_bDZVlSIYDeHZz_sIAaWIwX7hlq0yaVrJPTRnnE8s",{"id":440,"title":441,"body":442,"date":656,"description":657,"extension":426,"meta":658,"navigation":144,"path":659,"readTime":660,"seo":661,"stem":662,"tags":663,"thumbnail":667,"__hash__":668},"blog\u002Fblog\u002Fembedding-external-content.md","Embedding External Content in Your Posts",{"type":79,"value":443,"toc":652},[444,447,451,458,649],[82,445,446],{},"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,448,450],{"id":449},"youtube","YouTube",[82,452,453,454,457],{},"Use the video ID from the URL (the part after ",[104,455,456],{},"v=",").",[449,459,462,472,475,479,485],{"id":460,"title":461},"jNQXAC9IVRw","Me at the zoo — the very first YouTube upload",[109,463,466],{"className":464,"code":465,"language":426,"meta":114,"style":114},"language-md shiki shiki-themes vitesse-light","::youtube{id=\"jNQXAC9IVRw\" title=\"Video title here\"}\n",[104,467,468],{"__ignoreMap":114},[118,469,470],{"class":120,"line":12},[118,471,465],{},[473,474],"hr",{},[86,476,478],{"id":477},"twitter-x","Twitter \u002F X",[82,480,481,482,457],{},"Use the numeric tweet ID from the end of the tweet URL (",[104,483,484],{},"twitter.com\u002Fuser\u002Fstatus\u002FID",[486,487,489,498,500,504,519],"tweet",{"id":488},"2057183802569421019",[109,490,492],{"className":464,"code":491,"language":426,"meta":114,"style":114},"::tweet{id=\"2057183802569421019\"}\n",[104,493,494],{"__ignoreMap":114},[118,495,496],{"class":120,"line":12},[118,497,491],{},[473,499],{},[86,501,503],{"id":502},"instagram","Instagram",[82,505,506,507,510,511,514,515,518],{},"Use the post shortcode from the URL — ",[104,508,509],{},"instagram.com\u002Fp\u002FSHORTCODE\u002F",". Add ",[104,512,513],{},"author"," and ",[104,516,517],{},"caption"," for context.",[502,520,524,533,535,539,560],{"author":521,"caption":522,"id":523},"@nuxt.js","Nuxt 3 is here.","B9MexqUnoIM",[109,525,527],{"className":464,"code":526,"language":426,"meta":114,"style":114},"::instagram{id=\"B9MexqUnoIM\" author=\"@nuxt.js\" caption=\"Nuxt 3 is here.\"}\n",[104,528,529],{"__ignoreMap":114},[118,530,531],{"class":120,"line":12},[118,532,526],{},[473,534],{},[86,536,538],{"id":537},"spotify","Spotify",[82,540,541,542,545,546,545,549,552,553,556,557,457],{},"Supports ",[104,543,544],{},"track",", ",[104,547,548],{},"album",[104,550,551],{},"playlist",", and ",[104,554,555],{},"episode"," types. Grab the ID from the share link (",[104,558,559],{},"open.spotify.com\u002Ftrack\u002FID",[537,561,563,582,584,588,609],{"id":562,"type":544},"44DsgT84HJBv8PaxeK397f",[109,564,566],{"className":464,"code":565,"language":426,"meta":114,"style":114},"::spotify{id=\"44DsgT84HJBv8PaxeK397f\" type=\"track\"}\n\n::spotify{id=\"37i9dQZF1DX4sWSpwq3LiO\" type=\"playlist\"}\n",[104,567,568,573,577],{"__ignoreMap":114},[118,569,570],{"class":120,"line":12},[118,571,572],{},"::spotify{id=\"44DsgT84HJBv8PaxeK397f\" type=\"track\"}\n",[118,574,575],{"class":120,"line":21},[118,576,145],{"emptyLinePlaceholder":144},[118,578,579],{"class":120,"line":30},[118,580,581],{},"::spotify{id=\"37i9dQZF1DX4sWSpwq3LiO\" type=\"playlist\"}\n",[473,583],{},[86,585,587],{"id":586},"github","GitHub",[82,589,590,591,594,595,598,599,545,602,552,605,608],{},"A styled repo card — no external scripts needed. Pass ",[104,592,593],{},"repo"," as ",[104,596,597],{},"username\u002Frepository",", with optional ",[104,600,601],{},"description",[104,603,604],{},"language",[104,606,607],{},"stars",".",[586,610,615,624,626,630,633,636],{"description":611,"language":612,"repo":613,"stars":614},"The Intuitive Vue Framework.","TypeScript","nuxt\u002Fnuxt","54k",[109,616,618],{"className":464,"code":617,"language":426,"meta":114,"style":114},"::github{repo=\"nuxt\u002Fnuxt\" description=\"The Intuitive Vue Framework.\" language=\"TypeScript\" stars=\"54k\"}\n",[104,619,620],{"__ignoreMap":114},[118,621,622],{"class":120,"line":12},[118,623,617],{},[473,625],{},[86,627,629],{"id":628},"using-embeds-in-context","Using embeds in context",[82,631,632],{},"Embeds can sit anywhere in a post, mixed with regular prose. Example:",[82,634,635],{},"I've been experimenting with this talk on microservices — worth watching if you're evaluating service mesh patterns:",[449,637,640,643],{"id":638,"title":639},"GBTdnfD6s5Q","Microservices at scale",[82,641,642],{},"The repo from the talk:",[586,644],{"description":645,"language":646,"repo":647,"stars":648},"Connect, secure, control, and observe services.","Go","istio\u002Fistio","35k",[415,650,651],{},"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":114,"searchDepth":21,"depth":21,"links":653},[654,655],{"id":449,"depth":21,"text":450},{"id":477,"depth":21,"text":478},"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":441,"description":657},"blog\u002Fembedding-external-content",[664,665,666],"Guide","MDC","Embeds","\u002Fimages\u002Fthumbnails\u002Fembedding-external-content.png","gjjAUSnqoHoIj3QhbDO1nqi-JYqUbFA9KVNWuQyqIAU",{"id":670,"title":671,"body":672,"date":1302,"description":1303,"extension":426,"meta":1304,"navigation":144,"path":1305,"readTime":1306,"seo":1307,"stem":1308,"tags":1309,"thumbnail":1314,"__hash__":1315},"blog\u002Fblog\u002Fintegrating-fastapi-nuxt.md","Integrating FastAPI with Next-Gen Vue\u002FNuxt Interfaces",{"type":79,"value":673,"toc":1296},[674,677,681,684,873,880,884,895,919,922,1194,1197,1201,1204,1285,1288,1290,1293],[82,675,676],{},"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,678,680],{"id":679},"why-fastapi","Why FastAPI",[82,682,683],{},"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.",[109,685,689],{"className":686,"code":687,"language":688,"meta":114,"style":114},"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",[104,690,691,706,718,722,736,740,759,769,778,795,799,837,859,865],{"__ignoreMap":114},[118,692,693,696,700,703],{"class":120,"line":12},[118,694,695],{"class":339},"from",[118,697,699],{"class":698},"suHK_"," fastapi ",[118,701,702],{"class":339},"import",[118,704,705],{"class":698}," FastAPI\n",[118,707,708,710,713,715],{"class":120,"line":21},[118,709,695],{"class":339},[118,711,712],{"class":698}," pydantic ",[118,714,702],{"class":339},[118,716,717],{"class":698}," BaseModel\n",[118,719,720],{"class":120,"line":30},[118,721,145],{"emptyLinePlaceholder":144},[118,723,724,727,730,733],{"class":120,"line":39},[118,725,726],{"class":698},"app ",[118,728,729],{"class":127},"=",[118,731,732],{"class":698}," FastAPI",[118,734,735],{"class":127},"()\n",[118,737,738],{"class":120,"line":48},[118,739,145],{"emptyLinePlaceholder":144},[118,741,742,746,750,753,756],{"class":120,"line":57},[118,743,745],{"class":744},"si04Y","class",[118,747,749],{"class":748},"sUxyF"," DeploymentPayload",[118,751,752],{"class":127},"(",[118,754,755],{"class":366},"BaseModel",[118,757,758],{"class":127},"):\n",[118,760,761,764,766],{"class":120,"line":180},[118,762,763],{"class":698},"    service_name",[118,765,128],{"class":127},[118,767,768],{"class":123}," str\n",[118,770,771,774,776],{"class":120,"line":193},[118,772,773],{"class":698},"    image_tag",[118,775,128],{"class":127},[118,777,768],{"class":123},[118,779,780,783,785,788,791],{"class":120,"line":205},[118,781,782],{"class":698},"    replicas",[118,784,128],{"class":127},[118,786,787],{"class":123}," int",[118,789,790],{"class":127}," =",[118,792,794],{"class":793},"s-TwI"," 1\n",[118,796,797],{"class":120,"line":217},[118,798,145],{"emptyLinePlaceholder":144},[118,800,801,804,807,809,812,814,817,820,822,825,829,831,834],{"class":120,"line":229},[118,802,803],{"class":127},"@",[118,805,806],{"class":366},"app",[118,808,608],{"class":127},[118,810,811],{"class":366},"post",[118,813,752],{"class":127},[118,815,816],{"class":131},"\"",[118,818,819],{"class":135},"\u002Fdeployments",[118,821,816],{"class":131},[118,823,824],{"class":127},",",[118,826,828],{"class":827},"svycV"," response_model",[118,830,729],{"class":127},[118,832,833],{"class":698},"DeploymentPayload",[118,835,836],{"class":127},")\n",[118,838,839,842,845,848,850,853,855,857],{"class":120,"line":237},[118,840,841],{"class":744},"async",[118,843,844],{"class":744}," def",[118,846,847],{"class":366}," create_deployment",[118,849,752],{"class":127},[118,851,852],{"class":698},"payload",[118,854,128],{"class":127},[118,856,749],{"class":698},[118,858,758],{"class":127},[118,860,861],{"class":120,"line":249},[118,862,864],{"class":863},"s8zF2","    # schedule the deployment...\n",[118,866,867,870],{"class":120,"line":261},[118,868,869],{"class":339},"    return",[118,871,872],{"class":698}," payload\n",[82,874,875,876,879],{},"The ",[104,877,878],{},"response_model"," annotation is the key — FastAPI will serialize exactly what you declare, and nothing more. No accidental data leaks.",[86,881,883],{"id":882},"type-generation-on-the-nuxt-side","Type Generation on the Nuxt Side",[82,885,886,887,890,891,894],{},"Once the OpenAPI spec exists at ",[104,888,889],{},"http:\u002F\u002Flocalhost:8000\u002Fopenapi.json",", use ",[104,892,893],{},"openapi-typescript"," to generate TypeScript types:",[109,896,898],{"className":357,"code":897,"language":359,"meta":114,"style":114},"$ npx openapi-typescript http:\u002F\u002Flocalhost:8000\u002Fopenapi.json -o src\u002Ftypes\u002Fapi.ts\n",[104,899,900],{"__ignoreMap":114},[118,901,902,904,907,910,913,916],{"class":120,"line":12},[118,903,367],{"class":366},[118,905,906],{"class":135}," npx",[118,908,909],{"class":135}," openapi-typescript",[118,911,912],{"class":135}," http:\u002F\u002Flocalhost:8000\u002Fopenapi.json",[118,914,915],{"class":379}," -o",[118,917,918],{"class":135}," src\u002Ftypes\u002Fapi.ts\n",[82,920,921],{},"Now your Nuxt composable can be fully typed with zero manual interface definitions:",[109,923,927],{"className":924,"code":925,"language":926,"meta":114,"style":114},"language-typescript shiki shiki-themes vitesse-light","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","typescript",[104,928,929,957,961,995,999,1021,1043,1047,1075,1109,1126,1137,1142,1163,1168,1172,1189],{"__ignoreMap":114},[118,930,931,933,936,939,942,945,948,951,954],{"class":120,"line":12},[118,932,702],{"class":339},[118,934,935],{"class":339}," type",[118,937,938],{"class":127}," {",[118,940,941],{"class":827}," components",[118,943,944],{"class":127}," }",[118,946,947],{"class":339}," from",[118,949,950],{"class":131}," '",[118,952,953],{"class":135},"~\u002Ftypes\u002Fapi",[118,955,956],{"class":131},"'\n",[118,958,959],{"class":120,"line":21},[118,960,145],{"emptyLinePlaceholder":144},[118,962,963,966,969,971,973,976,979,982,984,987,989,991,993],{"class":120,"line":30},[118,964,965],{"class":744},"type",[118,967,968],{"class":748}," Deployment",[118,970,790],{"class":127},[118,972,941],{"class":748},[118,974,975],{"class":127},"[",[118,977,978],{"class":131},"'",[118,980,981],{"class":135},"schemas",[118,983,978],{"class":131},[118,985,986],{"class":127},"][",[118,988,978],{"class":131},[118,990,833],{"class":135},[118,992,978],{"class":131},[118,994,307],{"class":127},[118,996,997],{"class":120,"line":39},[118,998,145],{"emptyLinePlaceholder":144},[118,1000,1001,1004,1007,1010,1012,1015,1018],{"class":120,"line":48},[118,1002,1003],{"class":339},"export",[118,1005,1006],{"class":744}," const ",[118,1008,1009],{"class":366},"useDeployments",[118,1011,790],{"class":127},[118,1013,1014],{"class":127}," ()",[118,1016,1017],{"class":127}," =>",[118,1019,1020],{"class":127}," {\n",[118,1022,1023,1026,1029,1031,1034,1037,1040],{"class":120,"line":57},[118,1024,1025],{"class":744},"  const ",[118,1027,1028],{"class":827},"list",[118,1030,790],{"class":127},[118,1032,1033],{"class":366}," ref",[118,1035,1036],{"class":127},"\u003C",[118,1038,1039],{"class":748},"Deployment",[118,1041,1042],{"class":127},"[]>([])\n",[118,1044,1045],{"class":120,"line":180},[118,1046,145],{"emptyLinePlaceholder":144},[118,1048,1049,1051,1054,1056,1059,1061,1063,1066,1068,1071,1073],{"class":120,"line":193},[118,1050,1025],{"class":744},[118,1052,1053],{"class":366},"create",[118,1055,790],{"class":127},[118,1057,1058],{"class":744}," async ",[118,1060,752],{"class":127},[118,1062,852],{"class":827},[118,1064,1065],{"class":127},": ",[118,1067,1039],{"class":748},[118,1069,1070],{"class":127},")",[118,1072,1017],{"class":127},[118,1074,1020],{"class":127},[118,1076,1077,1080,1083,1085,1088,1091,1093,1095,1098,1100,1103,1105,1107],{"class":120,"line":205},[118,1078,1079],{"class":744},"    const ",[118,1081,1082],{"class":827},"data",[118,1084,790],{"class":127},[118,1086,1087],{"class":339}," await",[118,1089,1090],{"class":366}," $fetch",[118,1092,1036],{"class":127},[118,1094,1039],{"class":748},[118,1096,1097],{"class":127},">(",[118,1099,978],{"class":131},[118,1101,1102],{"class":135},"\u002Fapi\u002Fdeployments",[118,1104,978],{"class":131},[118,1106,824],{"class":127},[118,1108,1020],{"class":127},[118,1110,1111,1114,1116,1118,1121,1123],{"class":120,"line":217},[118,1112,1113],{"class":123},"      method",[118,1115,1065],{"class":127},[118,1117,978],{"class":131},[118,1119,1120],{"class":135},"POST",[118,1122,978],{"class":131},[118,1124,1125],{"class":127},",\n",[118,1127,1128,1131,1133,1135],{"class":120,"line":229},[118,1129,1130],{"class":123},"      body",[118,1132,1065],{"class":127},[118,1134,852],{"class":827},[118,1136,1125],{"class":127},[118,1138,1139],{"class":120,"line":237},[118,1140,1141],{"class":127},"    })\n",[118,1143,1144,1147,1149,1152,1154,1157,1159,1161],{"class":120,"line":249},[118,1145,1146],{"class":827},"    list",[118,1148,608],{"class":127},[118,1150,1151],{"class":827},"value",[118,1153,608],{"class":127},[118,1155,1156],{"class":366},"push",[118,1158,752],{"class":127},[118,1160,1082],{"class":827},[118,1162,836],{"class":127},[118,1164,1165],{"class":120,"line":261},[118,1166,1167],{"class":127},"  }\n",[118,1169,1170],{"class":120,"line":269},[118,1171,145],{"emptyLinePlaceholder":144},[118,1173,1174,1177,1180,1182,1184,1186],{"class":120,"line":277},[118,1175,1176],{"class":339},"  return",[118,1178,1179],{"class":127}," { ",[118,1181,1028],{"class":827},[118,1183,545],{"class":127},[118,1185,1053],{"class":827},[118,1187,1188],{"class":127}," }\n",[118,1190,1191],{"class":120,"line":285},[118,1192,1193],{"class":127},"}\n",[82,1195,1196],{},"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,1198,1200],{"id":1199},"cors-and-the-proxy-pattern","CORS and the Proxy Pattern",[82,1202,1203],{},"Never expose your Python backend directly to the browser in production. Run Nuxt's built-in server as a reverse proxy instead:",[109,1205,1207],{"className":924,"code":1206,"language":926,"meta":114,"style":114},"\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",[104,1208,1209,1214,1227,1235,1247,1270,1275,1280],{"__ignoreMap":114},[118,1210,1211],{"class":120,"line":12},[118,1212,1213],{"class":863},"\u002F\u002F nuxt.config.ts\n",[118,1215,1216,1218,1221,1224],{"class":120,"line":21},[118,1217,1003],{"class":339},[118,1219,1220],{"class":339}," default",[118,1222,1223],{"class":366}," defineNuxtConfig",[118,1225,1226],{"class":127},"({\n",[118,1228,1229,1232],{"class":120,"line":30},[118,1230,1231],{"class":123},"  routeRules",[118,1233,1234],{"class":127},": {\n",[118,1236,1237,1240,1243,1245],{"class":120,"line":39},[118,1238,1239],{"class":131},"    '",[118,1241,1242],{"class":135},"\u002Fapi\u002F**",[118,1244,978],{"class":131},[118,1246,1234],{"class":127},[118,1248,1249,1252,1255,1258,1260,1262,1265,1267],{"class":120,"line":48},[118,1250,1251],{"class":123},"      proxy",[118,1253,1254],{"class":127},": { ",[118,1256,1257],{"class":123},"to",[118,1259,1065],{"class":127},[118,1261,978],{"class":131},[118,1263,1264],{"class":135},"http:\u002F\u002Ffastapi-service:8000\u002F**",[118,1266,978],{"class":131},[118,1268,1269],{"class":127}," },\n",[118,1271,1272],{"class":120,"line":57},[118,1273,1274],{"class":127},"    },\n",[118,1276,1277],{"class":120,"line":180},[118,1278,1279],{"class":127},"  },\n",[118,1281,1282],{"class":120,"line":193},[118,1283,1284],{"class":127},"})\n",[82,1286,1287],{},"This collapses the cross-origin problem entirely — the browser sees one origin, and your FastAPI service never needs a CORS header.",[86,1289,410],{"id":409},[82,1291,1292],{},"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.",[415,1294,1295],{},"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":114,"searchDepth":21,"depth":21,"links":1297},[1298,1299,1300,1301],{"id":679,"depth":21,"text":680},{"id":882,"depth":21,"text":883},{"id":1199,"depth":21,"text":1200},{"id":409,"depth":21,"text":410},"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":671,"description":1303},"blog\u002Fintegrating-fastapi-nuxt",[1310,1311,612,1312,1313],"FastAPI","Nuxt","Python","API","\u002Fimages\u002Fthumbnails\u002Fintegrating-fastapi-nuxt.png","oeUJcbHal09PEvokXEL12iWhI43re_Pner_ID9zxtMc",{"id":1317,"title":1318,"body":1319,"date":424,"description":425,"extension":426,"meta":2796,"navigation":144,"path":2797,"readTime":429,"seo":2798,"stem":2799,"tags":2800,"thumbnail":2801,"__hash__":2802},"blog\u002Fblog\u002Fmtu-troubleshooting-blog.md","When Packets Disappear: Debugging an MTU Mismatch in a Hybrid OpenStack Docker Swarm",{"type":79,"value":1320,"toc":2777},[1321,1327,1329,1333,1336,1339,1342,1350,1353,1359,1366,1369,1371,1375,1378,1381,1384,1386,1390,1393,1396,1402,1422,1425,1432,1435,1450,1482,1485,1487,1491,1494,1497,1499,1503,1506,1543,1550,1578,1583,1590,1601,1603,1607,1610,1663,1670,1676,1678,1682,1685,1741,1751,1754,1760,1763,1766,1768,1772,1779,1785,1792,1798,1800,1804,1807,1859,1862,1867,1870,1876,1881,1884,1894,1929,1935,1937,1941,1944,1959,1962,1965,1982,1985,2124,2127,2129,2133,2136,2253,2259,2261,2265,2268,2275,2605,2615,2622,2624,2628,2635,2661,2664,2666,2670,2676,2682,2688,2694,2700,2702,2706,2771,2774],[82,1322,1323],{},[1324,1325,1326],"em",{},"A real-world troubleshooting story about silent packet drops, floating IPs, and why the obvious fix is not always the right one.",[473,1328],{},[86,1330,1332],{"id":1331},"background-why-we-have-a-hybrid-setup","Background: Why We Have a Hybrid Setup",[82,1334,1335],{},"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,1337,1338],{},"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,1340,1341],{},"So our Docker Swarm cluster looks like this:",[109,1343,1348],{"className":1344,"code":1346,"language":1347},[1345],"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",[104,1349,1346],{"__ignoreMap":114},[82,1351,1352],{},"Traffic flows like this for any external request:",[109,1354,1357],{"className":1355,"code":1356,"language":1347},[1345],"Internet → VIP → HAProxy → Traefik → Service container\n",[104,1358,1356],{"__ignoreMap":114},[82,1360,1361,1362,1365],{},"Services talk to each other using their Traefik URLs, for example ",[104,1363,1364],{},"https:\u002F\u002Femail-service\u002Fapi\u002Fsend",". Traefik handles the routing based on labels attached to each Docker service.",[82,1367,1368],{},"This setup has been working well. Until one day, emails stopped sending.",[473,1370],{},[86,1372,1374],{"id":1373},"the-problem-emails-timing-out-silently","The Problem: Emails Timing Out Silently",[82,1376,1377],{},"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,1379,1380],{},"Meanwhile, the monitoring node — which also sends emails (a daily digest with AI-generated summaries and HTML content) — was working perfectly fine.",[82,1382,1383],{},"That asymmetry was the first interesting clue.",[473,1385],{},[86,1387,1389],{"id":1388},"first-hypothesis-docker-overlay-mtu-problem","First Hypothesis: Docker Overlay MTU Problem",[82,1391,1392],{},"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,1394,1395],{},"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:",[1397,1398,1399],"blockquote",{},[82,1400,1401],{},"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.",[1403,1404,1405,1413,1416],"ul",{},[1406,1407,1408,1409],"li",{},"Standard Ethernet MTU: ",[1410,1411,1412],"strong",{},"1500 bytes",[1406,1414,1415],{},"VXLAN overhead: ~50 bytes",[1406,1417,1418,1419],{},"Effective MTU on OpenStack: ",[1410,1420,1421],{},"~1450 bytes",[82,1423,1424],{},"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,1426,1427,1428,1431],{},"This is called an ",[1410,1429,1430],{},"MTU mismatch",", and it is a well-known pain point in containerised environments running on top of virtualised networks.",[82,1433,1434],{},"The standard fix for this is:",[1436,1437,1438,1444,1447],"ol",{},[1406,1439,1440,1441],{},"Tell Docker to use a smaller MTU in ",[104,1442,1443],{},"\u002Fetc\u002Fdocker\u002Fdaemon.json",[1406,1445,1446],{},"Recreate the overlay networks with the correct MTU",[1406,1448,1449],{},"Recreate the ingress network",[109,1451,1455],{"className":1452,"code":1453,"language":1454,"meta":114,"style":114},"language-json shiki shiki-themes vitesse-light","{\n  \"mtu\": 1400\n}\n","json",[104,1456,1457,1462,1478],{"__ignoreMap":114},[118,1458,1459],{"class":120,"line":12},[118,1460,1461],{"class":127},"{\n",[118,1463,1464,1468,1471,1473,1475],{"class":120,"line":21},[118,1465,1467],{"class":1466},"s61at","  \"",[118,1469,1470],{"class":123},"mtu",[118,1472,816],{"class":1466},[118,1474,128],{"class":127},[118,1476,1477],{"class":793}," 1400\n",[118,1479,1480],{"class":120,"line":30},[118,1481,1193],{"class":127},[82,1483,1484],{},"But this approach had a serious problem for our environment.",[473,1486],{},[86,1488,1490],{"id":1489},"why-the-standard-fix-was-not-viable","Why the Standard Fix Was Not Viable",[82,1492,1493],{},"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,1495,1496],{},"We needed to think more carefully before touching anything.",[473,1498],{},[86,1500,1502],{"id":1501},"digging-deeper-a-ping-test-reveals-the-truth","Digging Deeper: A Ping Test Reveals the Truth",[82,1504,1505],{},"Before making any changes, we ran a diagnostic test on the test node — the one where emails were failing:",[109,1507,1509],{"className":357,"code":1508,"language":359,"meta":114,"style":114},"docker run --rm alpine ping -c 5 -s 1200 8.8.8.8\n",[104,1510,1511],{"__ignoreMap":114},[118,1512,1513,1516,1519,1522,1525,1528,1531,1534,1537,1540],{"class":120,"line":12},[118,1514,1515],{"class":366},"docker",[118,1517,1518],{"class":135}," run",[118,1520,1521],{"class":379}," --rm",[118,1523,1524],{"class":135}," alpine",[118,1526,1527],{"class":135}," ping",[118,1529,1530],{"class":379}," -c",[118,1532,1533],{"class":793}," 5",[118,1535,1536],{"class":379}," -s",[118,1538,1539],{"class":793}," 1200",[118,1541,1542],{"class":793}," 8.8.8.8\n",[82,1544,1545,1546,1549],{},"Result: ",[1410,1547,1548],{},"5\u002F5 packets received."," Fine.",[109,1551,1553],{"className":357,"code":1552,"language":359,"meta":114,"style":114},"docker run --rm alpine ping -c 5 -s 1472 8.8.8.8\n",[104,1554,1555],{"__ignoreMap":114},[118,1556,1557,1559,1561,1563,1565,1567,1569,1571,1573,1576],{"class":120,"line":12},[118,1558,1515],{"class":366},[118,1560,1518],{"class":135},[118,1562,1521],{"class":379},[118,1564,1524],{"class":135},[118,1566,1527],{"class":135},[118,1568,1530],{"class":379},[118,1570,1533],{"class":793},[118,1572,1536],{"class":379},[118,1574,1575],{"class":793}," 1472",[118,1577,1542],{"class":793},[82,1579,1545,1580],{},[1410,1581,1582],{},"0\u002F5 packets received. 100% loss.",[82,1584,1585,1586,1589],{},"This is significant. The ",[104,1587,1588],{},"-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,1591,1592,1593,1596,1597,1600],{},"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: ",[1324,1594,1595],{},"where exactly"," was it happening, and ",[1324,1598,1599],{},"why"," was it only affecting the test node and not the monitoring node?",[473,1602],{},[86,1604,1606],{"id":1605},"the-key-asymmetry-inside-vs-outside-openstack","The Key Asymmetry: Inside vs Outside OpenStack",[82,1608,1609],{},"Let us look at what was different between the working and failing cases:",[1611,1612,1613,1629],"table",{},[1614,1615,1616],"thead",{},[1617,1618,1619,1623,1626],"tr",{},[1620,1621,1622],"th",{},"Source",[1620,1624,1625],{},"Destination",[1620,1627,1628],{},"Result",[1630,1631,1632,1644,1653],"tbody",{},[1617,1633,1634,1638,1641],{},[1635,1636,1637],"td",{},"Monitoring node (outside OpenStack)",[1635,1639,1640],{},"Email service (inside OpenStack)",[1635,1642,1643],{},"✅ Works",[1617,1645,1646,1649,1651],{},[1635,1647,1648],{},"Old test node (outside OpenStack)",[1635,1650,1640],{},[1635,1652,1643],{},[1617,1654,1655,1658,1660],{},[1635,1656,1657],{},"New test node (inside OpenStack)",[1635,1659,1640],{},[1635,1661,1662],{},"❌ Fails",[82,1664,1665,1666,1669],{},"The common variable is not the payload size. The monitoring node sends large HTML emails and they go through fine. The common variable is ",[1410,1667,1668],{},"where the request originates",". Everything originating from inside OpenStack to the email service was failing.",[82,1671,1672,1673,608],{},"This pointed away from a Docker overlay problem and toward a ",[1410,1674,1675],{},"network path problem specific to OpenStack",[473,1677],{},[86,1679,1681],{"id":1680},"the-real-traffic-path-a-surprising-discovery","The Real Traffic Path: A Surprising Discovery",[82,1683,1684],{},"Here is where the architecture revealed something unexpected. The OpenStack nodes join the Swarm like this:",[109,1686,1688],{"className":357,"code":1687,"language":359,"meta":114,"style":114},"docker swarm join --token \"$TOKEN\" \"$MANAGER\" \\\n  --advertise-addr \"$FLOATING_IP\" \\\n  --listen-addr 0.0.0.0:2377\n",[104,1689,1690,1719,1733],{"__ignoreMap":114},[118,1691,1692,1694,1697,1700,1703,1705,1708,1710,1712,1715,1717],{"class":120,"line":12},[118,1693,1515],{"class":366},[118,1695,1696],{"class":135}," swarm",[118,1698,1699],{"class":135}," join",[118,1701,1702],{"class":379}," --token",[118,1704,132],{"class":131},[118,1706,1707],{"class":135},"$TOKEN",[118,1709,816],{"class":131},[118,1711,132],{"class":131},[118,1713,1714],{"class":135},"$MANAGER",[118,1716,816],{"class":131},[118,1718,380],{"class":379},[118,1720,1721,1724,1726,1729,1731],{"class":120,"line":21},[118,1722,1723],{"class":379},"  --advertise-addr",[118,1725,132],{"class":131},[118,1727,1728],{"class":135},"$FLOATING_IP",[118,1730,816],{"class":131},[118,1732,380],{"class":379},[118,1734,1735,1738],{"class":120,"line":30},[118,1736,1737],{"class":379},"  --listen-addr",[118,1739,1740],{"class":135}," 0.0.0.0:2377\n",[82,1742,875,1743,1746,1747,1750],{},[104,1744,1745],{},"--advertise-addr"," is set to the node's ",[1410,1748,1749],{},"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,1752,1753],{},"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:",[109,1755,1758],{"className":1756,"code":1757,"language":1347},[1345],"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",[104,1759,1757],{"__ignoreMap":114},[82,1761,1762],{},"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,1764,1765],{},"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.",[473,1767],{},[86,1769,1771],{"id":1770},"confirming-with-interface-inspection","Confirming With Interface Inspection",[82,1773,1774,1775,1778],{},"Running ",[104,1776,1777],{},"ip link show"," on the test node made the mismatch immediately visible:",[109,1780,1783],{"className":1781,"code":1782,"language":1347},[1345],"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",[104,1784,1782],{"__ignoreMap":114},[82,1786,1787,1788,1791],{},"The host network interface ",[104,1789,1790],{},"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,1793,1794,1795,1797],{},"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 ",[104,1796,1790],{}," — which can only carry 1450 bytes. The oversized packet hits the wall and is silently dropped.",[473,1799],{},[86,1801,1803],{"id":1802},"the-iptables-fix-that-worked","The Iptables Fix That Worked",[82,1805,1806],{},"Before understanding all of this fully, an iptables rule was applied on the test node:",[109,1808,1810],{"className":357,"code":1809,"language":359,"meta":114,"style":114},"sudo iptables -t mangle -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360\n",[104,1811,1812],{"__ignoreMap":114},[118,1813,1814,1817,1820,1823,1826,1829,1832,1835,1838,1841,1844,1847,1850,1853,1856],{"class":120,"line":12},[118,1815,1816],{"class":366},"sudo",[118,1818,1819],{"class":135}," iptables",[118,1821,1822],{"class":379}," -t",[118,1824,1825],{"class":135}," mangle",[118,1827,1828],{"class":379}," -I",[118,1830,1831],{"class":135}," FORWARD",[118,1833,1834],{"class":379}," -p",[118,1836,1837],{"class":135}," tcp",[118,1839,1840],{"class":379}," --tcp-flags",[118,1842,1843],{"class":135}," SYN,RST",[118,1845,1846],{"class":135}," SYN",[118,1848,1849],{"class":379}," -j",[118,1851,1852],{"class":135}," TCPMSS",[118,1854,1855],{"class":379}," --set-mss",[118,1857,1858],{"class":793}," 1360\n",[82,1860,1861],{},"And the emails started going through immediately.",[1863,1864,1866],"h3",{"id":1865},"what-does-this-actually-do","What Does This Actually Do?",[82,1868,1869],{},"To understand this fix, you need to know a little about how TCP connections work.",[82,1871,1872,1873,608],{},"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 ",[1410,1874,1875],{},"Maximum Segment Size (MSS)",[1397,1877,1878],{},[82,1879,1880],{},"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,1882,1883],{},"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,1885,875,1886,1889,1890,1893],{},[104,1887,1888],{},"--set-mss 1360"," hardcodes the MSS to 1360 bytes. It works, but a smarter version uses ",[104,1891,1892],{},"--clamp-mss-to-pmtu"," instead:",[109,1895,1897],{"className":357,"code":1896,"language":359,"meta":114,"style":114},"iptables -t mangle -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu\n",[104,1898,1899],{"__ignoreMap":114},[118,1900,1901,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922,1924,1926],{"class":120,"line":12},[118,1902,1903],{"class":366},"iptables",[118,1905,1822],{"class":379},[118,1907,1825],{"class":135},[118,1909,1828],{"class":379},[118,1911,1831],{"class":135},[118,1913,1834],{"class":379},[118,1915,1837],{"class":135},[118,1917,1840],{"class":379},[118,1919,1843],{"class":135},[118,1921,1846],{"class":135},[118,1923,1849],{"class":379},[118,1925,1852],{"class":135},[118,1927,1928],{"class":379}," --clamp-mss-to-pmtu\n",[82,1930,1931,1932,1934],{},"This tells the kernel to calculate the correct MSS automatically based on the actual outgoing interface MTU (1450 on ",[104,1933,1790],{},"), rather than using a hardcoded value. If the network MTU ever changes, the rule adapts automatically.",[473,1936],{},[86,1938,1940],{"id":1939},"why-this-fix-and-not-the-docker-mtu-fix","Why This Fix and Not the Docker MTU Fix?",[82,1942,1943],{},"This is the important question. We could have:",[1436,1945,1946,1953,1956],{},[1406,1947,1948,1949,1952],{},"Changed ",[104,1950,1951],{},"daemon.json"," to set Docker MTU to 1400",[1406,1954,1955],{},"Recreated all overlay networks",[1406,1957,1958],{},"Recreated the ingress network",[82,1960,1961],{},"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,1963,1964],{},"The iptables TCPMSS approach:",[1403,1966,1967,1970,1973,1976,1979],{},[1406,1968,1969],{},"Touches nothing else in the stack",[1406,1971,1972],{},"Requires no service restarts",[1406,1974,1975],{},"Requires no network recreation",[1406,1977,1978],{},"Only affects outbound TCP SYN packets from that node",[1406,1980,1981],{},"Is invisible to services and containers",[82,1983,1984],{},"We confirmed this by checking the iptables rule counters after applying it:",[109,1986,1988],{"className":357,"code":1987,"language":359,"meta":114,"style":114},"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",[104,1989,1990,2014,2018,2045,2076],{"__ignoreMap":114},[118,1991,1992,1994,1996,1998,2000,2003,2005,2008,2011],{"class":120,"line":12},[118,1993,1816],{"class":366},[118,1995,1819],{"class":135},[118,1997,1822],{"class":379},[118,1999,1825],{"class":135},[118,2001,2002],{"class":379}," -L",[118,2004,1831],{"class":135},[118,2006,2007],{"class":379}," -n",[118,2009,2010],{"class":379}," -v",[118,2012,2013],{"class":379}," --line-numbers\n",[118,2015,2016],{"class":120,"line":21},[118,2017,145],{"emptyLinePlaceholder":144},[118,2019,2020,2023,2025,2028,2031,2034,2037,2040,2043],{"class":120,"line":30},[118,2021,2022],{"class":366},"Chain",[118,2024,1831],{"class":135},[118,2026,2027],{"class":698}," (policy ",[118,2029,2030],{"class":135},"ACCEPT",[118,2032,2033],{"class":135}," 5894K",[118,2035,2036],{"class":135}," packets,",[118,2038,2039],{"class":135}," 2970M",[118,2041,2042],{"class":135}," bytes",[118,2044,836],{"class":698},[118,2046,2047,2050,2053,2055,2058,2061,2064,2067,2070,2073],{"class":120,"line":39},[118,2048,2049],{"class":366},"num",[118,2051,2052],{"class":135},"   pkts",[118,2054,2042],{"class":135},[118,2056,2057],{"class":135}," target",[118,2059,2060],{"class":135},"     prot",[118,2062,2063],{"class":135}," opt",[118,2065,2066],{"class":135}," in",[118,2068,2069],{"class":135},"     out",[118,2071,2072],{"class":135},"     source",[118,2074,2075],{"class":135},"               destination\n",[118,2077,2078,2081,2084,2087,2089,2092,2095,2098,2101,2104,2107,2110,2113,2115,2118,2121],{"class":120,"line":48},[118,2079,2080],{"class":366},"1",[118,2082,2083],{"class":793},"        6",[118,2085,2086],{"class":793},"   360",[118,2088,1852],{"class":135},[118,2090,2091],{"class":793},"     6",[118,2093,2094],{"class":379},"    --",[118,2096,2097],{"class":379},"  *",[118,2099,2100],{"class":379},"      *",[118,2102,2103],{"class":135},"       0.0.0.0\u002F0",[118,2105,2106],{"class":135},"  0.0.0.0\u002F0",[118,2108,2109],{"class":135},"  tcp",[118,2111,2112],{"class":135}," flags:0x06\u002F0x02",[118,2114,1852],{"class":135},[118,2116,2117],{"class":135}," clamp",[118,2119,2120],{"class":135}," to",[118,2122,2123],{"class":135}," PMTU\n",[82,2125,2126],{},"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.",[473,2128],{},[86,2130,2132],{"id":2131},"making-it-persistent-the-manual-node","Making It Persistent: The Manual Node",[82,2134,2135],{},"The iptables rule applied manually disappears on reboot. For the manually provisioned test node, the fix is:",[109,2137,2139],{"className":357,"code":2138,"language":359,"meta":114,"style":114},"# 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",[104,2140,2141,2146,2179,2183,2188,2218,2222,2227,2243],{"__ignoreMap":114},[118,2142,2143],{"class":120,"line":12},[118,2144,2145],{"class":863},"# Remove the old hardcoded rule\n",[118,2147,2148,2150,2152,2154,2156,2159,2161,2163,2165,2167,2169,2171,2173,2175,2177],{"class":120,"line":21},[118,2149,1816],{"class":366},[118,2151,1819],{"class":135},[118,2153,1822],{"class":379},[118,2155,1825],{"class":135},[118,2157,2158],{"class":379}," -D",[118,2160,1831],{"class":135},[118,2162,1834],{"class":379},[118,2164,1837],{"class":135},[118,2166,1840],{"class":379},[118,2168,1843],{"class":135},[118,2170,1846],{"class":135},[118,2172,1849],{"class":379},[118,2174,1852],{"class":135},[118,2176,1855],{"class":379},[118,2178,1858],{"class":793},[118,2180,2181],{"class":120,"line":30},[118,2182,145],{"emptyLinePlaceholder":144},[118,2184,2185],{"class":120,"line":39},[118,2186,2187],{"class":863},"# Add the smarter adaptive rule\n",[118,2189,2190,2192,2194,2196,2198,2200,2202,2204,2206,2208,2210,2212,2214,2216],{"class":120,"line":48},[118,2191,1816],{"class":366},[118,2193,1819],{"class":135},[118,2195,1822],{"class":379},[118,2197,1825],{"class":135},[118,2199,1828],{"class":379},[118,2201,1831],{"class":135},[118,2203,1834],{"class":379},[118,2205,1837],{"class":135},[118,2207,1840],{"class":379},[118,2209,1843],{"class":135},[118,2211,1846],{"class":135},[118,2213,1849],{"class":379},[118,2215,1852],{"class":135},[118,2217,1928],{"class":379},[118,2219,2220],{"class":120,"line":57},[118,2221,145],{"emptyLinePlaceholder":144},[118,2223,2224],{"class":120,"line":180},[118,2225,2226],{"class":863},"# Install persistence\n",[118,2228,2229,2231,2234,2237,2240],{"class":120,"line":193},[118,2230,1816],{"class":366},[118,2232,2233],{"class":135}," apt-get",[118,2235,2236],{"class":135}," install",[118,2238,2239],{"class":379}," -y",[118,2241,2242],{"class":135}," iptables-persistent\n",[118,2244,2245,2247,2250],{"class":120,"line":205},[118,2246,1816],{"class":366},[118,2248,2249],{"class":135}," netfilter-persistent",[118,2251,2252],{"class":135}," save\n",[82,2254,2255,2258],{},[104,2256,2257],{},"iptables-persistent"," saves the current rules to disk and restores them automatically on every boot.",[473,2260],{},[86,2262,2264],{"id":2263},"making-it-automatic-the-asg-nodes","Making It Automatic: The ASG Nodes",[82,2266,2267],{},"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,2269,2270,2271,2274],{},"The fix belongs in the ",[104,2272,2273],{},"user_data"," cloud-init script that runs on every new node at boot. In our Heat template, right after the Swarm join:",[109,2276,2278],{"className":357,"code":2277,"language":359,"meta":114,"style":114},"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",[104,2279,2280,2314,2338,2350,2356,2367,2371,2376,2387,2398,2421,2425,2436,2464,2475,2479,2490,2508,2512,2523,2541,2552,2556,2568,2576,2588,2593],{"__ignoreMap":114},[118,2281,2282,2285,2287,2290,2293,2296,2299,2302,2304,2307,2309,2312],{"class":120,"line":12},[118,2283,2284],{"class":123},"echo",[118,2286,132],{"class":131},[118,2288,2289],{"class":135},"Joining swarm at ",[118,2291,2292],{"class":127},"${",[118,2294,2295],{"class":135},"MANAGER",[118,2297,2298],{"class":127},"}",[118,2300,2301],{"class":135}," advertising ",[118,2303,2292],{"class":127},[118,2305,2306],{"class":135},"FLOATING_IP",[118,2308,2298],{"class":127},[118,2310,2311],{"class":135},"...",[118,2313,139],{"class":131},[118,2315,2316,2318,2320,2322,2324,2326,2328,2330,2332,2334,2336],{"class":120,"line":21},[118,2317,1515],{"class":366},[118,2319,1696],{"class":135},[118,2321,1699],{"class":135},[118,2323,1702],{"class":379},[118,2325,132],{"class":131},[118,2327,1707],{"class":135},[118,2329,816],{"class":131},[118,2331,132],{"class":131},[118,2333,1714],{"class":135},[118,2335,816],{"class":131},[118,2337,380],{"class":379},[118,2339,2340,2342,2344,2346,2348],{"class":120,"line":30},[118,2341,1723],{"class":379},[118,2343,132],{"class":131},[118,2345,1728],{"class":135},[118,2347,816],{"class":131},[118,2349,380],{"class":379},[118,2351,2352,2354],{"class":120,"line":39},[118,2353,1737],{"class":379},[118,2355,1740],{"class":135},[118,2357,2358,2360,2362,2365],{"class":120,"line":48},[118,2359,2284],{"class":123},[118,2361,132],{"class":131},[118,2363,2364],{"class":135},"Swarm join complete.",[118,2366,139],{"class":131},[118,2368,2369],{"class":120,"line":57},[118,2370,145],{"emptyLinePlaceholder":144},[118,2372,2373],{"class":120,"line":180},[118,2374,2375],{"class":863},"# ── Fix MTU mismatch between Docker (1500) and OpenStack interface (1450) ──\n",[118,2377,2378,2380,2382,2385],{"class":120,"line":193},[118,2379,2284],{"class":123},[118,2381,132],{"class":131},[118,2383,2384],{"class":135},"--- Applying MTU fix ---",[118,2386,139],{"class":131},[118,2388,2389,2391,2393,2396],{"class":120,"line":205},[118,2390,2284],{"class":123},[118,2392,132],{"class":131},[118,2394,2395],{"class":135},"Host interface MTU before fix:",[118,2397,139],{"class":131},[118,2399,2400,2403,2406,2409,2412,2415,2418],{"class":120,"line":217},[118,2401,2402],{"class":366},"ip",[118,2404,2405],{"class":135}," link",[118,2407,2408],{"class":135}," show",[118,2410,2411],{"class":135}," ens3",[118,2413,2414],{"class":744}," |",[118,2416,2417],{"class":366}," grep",[118,2419,2420],{"class":135}," mtu\n",[118,2422,2423],{"class":120,"line":229},[118,2424,145],{"emptyLinePlaceholder":144},[118,2426,2427,2429,2431,2434],{"class":120,"line":237},[118,2428,2284],{"class":123},[118,2430,132],{"class":131},[118,2432,2433],{"class":135},"Applying TCPMSS clamp rule...",[118,2435,139],{"class":131},[118,2437,2438,2440,2442,2444,2446,2448,2450,2452,2454,2456,2458,2460,2462],{"class":120,"line":249},[118,2439,1903],{"class":366},[118,2441,1822],{"class":379},[118,2443,1825],{"class":135},[118,2445,1828],{"class":379},[118,2447,1831],{"class":135},[118,2449,1834],{"class":379},[118,2451,1837],{"class":135},[118,2453,1840],{"class":379},[118,2455,1843],{"class":135},[118,2457,1846],{"class":135},[118,2459,1849],{"class":379},[118,2461,1852],{"class":135},[118,2463,1928],{"class":379},[118,2465,2466,2468,2470,2473],{"class":120,"line":261},[118,2467,2284],{"class":123},[118,2469,132],{"class":131},[118,2471,2472],{"class":135},"TCPMSS rule applied.",[118,2474,139],{"class":131},[118,2476,2477],{"class":120,"line":269},[118,2478,145],{"emptyLinePlaceholder":144},[118,2480,2481,2483,2485,2488],{"class":120,"line":277},[118,2482,2284],{"class":123},[118,2484,132],{"class":131},[118,2486,2487],{"class":135},"Verifying rule:",[118,2489,139],{"class":131},[118,2491,2492,2494,2496,2498,2500,2502,2504,2506],{"class":120,"line":285},[118,2493,1903],{"class":366},[118,2495,1822],{"class":379},[118,2497,1825],{"class":135},[118,2499,2002],{"class":379},[118,2501,1831],{"class":135},[118,2503,2007],{"class":379},[118,2505,2010],{"class":379},[118,2507,2013],{"class":379},[118,2509,2510],{"class":120,"line":293},[118,2511,145],{"emptyLinePlaceholder":144},[118,2513,2514,2516,2518,2521],{"class":120,"line":310},[118,2515,2284],{"class":123},[118,2517,132],{"class":131},[118,2519,2520],{"class":135},"Installing iptables-persistent...",[118,2522,139],{"class":131},[118,2524,2525,2528,2530,2533,2535,2537,2539],{"class":120,"line":315},[118,2526,2527],{"class":827},"DEBIAN_FRONTEND",[118,2529,729],{"class":127},[118,2531,2532],{"class":135},"noninteractive",[118,2534,2233],{"class":366},[118,2536,2236],{"class":135},[118,2538,2239],{"class":379},[118,2540,2242],{"class":135},[118,2542,2543,2545,2547,2550],{"class":120,"line":323},[118,2544,2284],{"class":123},[118,2546,132],{"class":131},[118,2548,2549],{"class":135},"iptables-persistent installed.",[118,2551,139],{"class":131},[118,2553,2554],{"class":120,"line":331},[118,2555,145],{"emptyLinePlaceholder":144},[118,2557,2559,2561,2563,2566],{"class":120,"line":2558},23,[118,2560,2284],{"class":123},[118,2562,132],{"class":131},[118,2564,2565],{"class":135},"Saving rules...",[118,2567,139],{"class":131},[118,2569,2571,2574],{"class":120,"line":2570},24,[118,2572,2573],{"class":366},"netfilter-persistent",[118,2575,2252],{"class":135},[118,2577,2579,2581,2583,2586],{"class":120,"line":2578},25,[118,2580,2284],{"class":123},[118,2582,132],{"class":131},[118,2584,2585],{"class":135},"Rules saved.",[118,2587,139],{"class":131},[118,2589,2591],{"class":120,"line":2590},26,[118,2592,145],{"emptyLinePlaceholder":144},[118,2594,2596,2598,2600,2603],{"class":120,"line":2595},27,[118,2597,2284],{"class":123},[118,2599,132],{"class":131},[118,2601,2602],{"class":135},"--- MTU fix complete ---",[118,2604,139],{"class":131},[82,2606,875,2607,2610,2611,2614],{},[104,2608,2609],{},"DEBIAN_FRONTEND=noninteractive"," flag is important. Without it, ",[104,2612,2613],{},"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,2616,2617,2618,2621],{},"Every new ASG node now gets the fix automatically at boot, and the log at ",[104,2619,2620],{},"\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.",[473,2623],{},[86,2625,2627],{"id":2626},"what-we-did-not-need-to-do","What We Did Not Need to Do",[82,2629,2630,2631,2634],{},"It is worth being explicit about this. The following changes that are commonly suggested for MTU problems in Docker Swarm were ",[1410,2632,2633],{},"not needed"," for our specific situation:",[1403,2636,2637,2643,2646,2649,2655,2658],{},[1406,2638,2639,2640,2642],{},"❌ Changing ",[104,2641,1951],{}," MTU",[1406,2644,2645],{},"❌ Recreating overlay networks",[1406,2647,2648],{},"❌ Recreating the ingress network",[1406,2650,2651,2652],{},"❌ Changing host interface MTU with ",[104,2653,2654],{},"ip link set",[1406,2656,2657],{},"❌ Draining any nodes",[1406,2659,2660],{},"❌ Any service restarts",[82,2662,2663],{},"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.",[473,2665],{},[86,2667,2669],{"id":2668},"lessons-learned","Lessons Learned",[82,2671,2672,2675],{},[1410,2673,2674],{},"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,2677,2678,2681],{},[1410,2679,2680],{},"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,2683,2684,2687],{},[1410,2685,2686],{},"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,2689,2690,2693],{},[1410,2691,2692],{},"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,2695,2696,2699],{},[1410,2697,2698],{},"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.",[473,2701],{},[86,2703,2705],{"id":2704},"summary","Summary",[1611,2707,2708,2718],{},[1614,2709,2710],{},[1617,2711,2712,2715],{},[1620,2713,2714],{},"What we thought the problem was",[1620,2716,2717],{},"Docker overlay MTU mismatch",[1630,2719,2720,2728,2736,2744,2752,2763],{},[1617,2721,2722,2725],{},[1635,2723,2724],{},"What the problem actually was",[1635,2726,2727],{},"Docker (MTU 1500) vs OpenStack interface (MTU 1450) mismatch on outbound TCP from OpenStack nodes",[1617,2729,2730,2733],{},[1635,2731,2732],{},"Why it only affected OpenStack nodes",[1635,2734,2735],{},"Outside nodes have real 1500 MTU interfaces with no mismatch",[1617,2737,2738,2741],{},[1635,2739,2740],{},"Why monitoring worked but test node failed",[1635,2742,2743],{},"Monitoring node is outside OpenStack, only crosses the boundary once",[1617,2745,2746,2749],{},[1635,2747,2748],{},"The fix",[1635,2750,2751],{},"TCPMSS iptables clamp on each OpenStack node",[1617,2753,2754,2757],{},[1635,2755,2756],{},"Where the fix lives",[1635,2758,2759,2760,2762],{},"Manually on existing nodes, baked into Heat ",[104,2761,2273],{}," for ASG nodes",[1617,2764,2765,2768],{},[1635,2766,2767],{},"What we avoided",[1635,2769,2770],{},"Any Docker network changes, downtime, service restarts",[82,2772,2773],{},"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.",[415,2775,2776],{},"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":114,"searchDepth":21,"depth":21,"links":2778},[2779,2780,2781,2782,2783,2784,2785,2786,2787,2790,2791,2792,2793,2794,2795],{"id":1331,"depth":21,"text":1332},{"id":1373,"depth":21,"text":1374},{"id":1388,"depth":21,"text":1389},{"id":1489,"depth":21,"text":1490},{"id":1501,"depth":21,"text":1502},{"id":1605,"depth":21,"text":1606},{"id":1680,"depth":21,"text":1681},{"id":1770,"depth":21,"text":1771},{"id":1802,"depth":21,"text":1803,"children":2788},[2789],{"id":1865,"depth":30,"text":1866},{"id":1939,"depth":21,"text":1940},{"id":2131,"depth":21,"text":2132},{"id":2263,"depth":21,"text":2264},{"id":2626,"depth":21,"text":2627},{"id":2668,"depth":21,"text":2669},{"id":2704,"depth":21,"text":2705},{},"\u002Fblog\u002Fmtu-troubleshooting-blog",{"title":1318,"description":425},"blog\u002Fmtu-troubleshooting-blog",[433,434,435,436],"\u002Fimages\u002Fthumbnails\u002Fmtu-troubleshooting-blog.png","0szugP3ReobiA0PjsiX4nQ4WIyeMV6m3ebTfvSLMwUg",{"id":2804,"title":2805,"body":2806,"date":2937,"description":2938,"extension":426,"meta":2939,"navigation":144,"path":2940,"readTime":2941,"seo":2942,"stem":2943,"tags":2944,"thumbnail":2948,"__hash__":2949},"blog\u002Fblog\u002Fserverless-data-architecture.md","The Transition to Serverless Data Architecture",{"type":79,"value":2807,"toc":2931},[2808,2811,2815,2818,2825,2829,2832,2846,2849,2853,2856,2915,2918,2922,2925,2928],[82,2809,2810],{},"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,2812,2814],{"id":2813},"what-you-give-up-running-it-yourself","What You Give Up Running It Yourself",[82,2816,2817],{},"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,2819,2820,2821,2824],{},"The cost of expertise compounds. When the primary went down at 2am on a Saturday, someone had to know what ",[104,2822,2823],{},"pg_ctl promote"," does and when to run it.",[86,2826,2828],{"id":2827},"what-you-actually-get-from-managed-services","What You Actually Get from Managed Services",[82,2830,2831],{},"Neon gave us:",[1403,2833,2834,2837,2840,2843],{},[1406,2835,2836],{},"Branching — spin up a full copy of the database for a PR in under 30 seconds",[1406,2838,2839],{},"Automatic point-in-time recovery with a simple API call",[1406,2841,2842],{},"Scale-to-zero during off-hours (real savings on non-critical environments)",[1406,2844,2845],{},"Connection pooling via PgBouncer baked in",[82,2847,2848],{},"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,2850,2852],{"id":2851},"the-migration","The Migration",[82,2854,2855],{},"We ran both systems in parallel for three weeks using logical replication:",[109,2857,2861],{"className":2858,"code":2859,"language":2860,"meta":114,"style":114},"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","sql",[104,2862,2863,2868,2882,2886,2891,2898,2910],{"__ignoreMap":114},[118,2864,2865],{"class":120,"line":12},[118,2866,2867],{"class":863},"-- On the source (self-hosted)\n",[118,2869,2870,2873,2876,2879],{"class":120,"line":21},[118,2871,2872],{"class":339},"CREATE",[118,2874,2875],{"class":698}," PUBLICATION migration_pub ",[118,2877,2878],{"class":339},"FOR",[118,2880,2881],{"class":698}," ALL TABLES;\n",[118,2883,2884],{"class":120,"line":30},[118,2885,145],{"emptyLinePlaceholder":144},[118,2887,2888],{"class":120,"line":39},[118,2889,2890],{"class":863},"-- On the target (Neon)\n",[118,2892,2893,2895],{"class":120,"line":48},[118,2894,2872],{"class":339},[118,2896,2897],{"class":698}," SUBSCRIPTION migration_sub\n",[118,2899,2900,2903,2905,2908],{"class":120,"line":57},[118,2901,2902],{"class":339},"  CONNECTION",[118,2904,950],{"class":131},[118,2906,2907],{"class":135},"host=old-primary ...",[118,2909,956],{"class":131},[118,2911,2912],{"class":120,"line":180},[118,2913,2914],{"class":698},"  PUBLICATION migration_pub;\n",[82,2916,2917],{},"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,2919,2921],{"id":2920},"where-self-hosting-still-wins","Where Self-Hosting Still Wins",[82,2923,2924],{},"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,2926,2927],{},"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.",[415,2929,2930],{},"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":114,"searchDepth":21,"depth":21,"links":2932},[2933,2934,2935,2936],{"id":2813,"depth":21,"text":2814},{"id":2827,"depth":21,"text":2828},{"id":2851,"depth":21,"text":2852},{"id":2920,"depth":21,"text":2921},"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":2805,"description":2938},"blog\u002Fserverless-data-architecture",[2945,436,2946,2947],"Database","PostgreSQL","Architecture","\u002Fimages\u002Fthumbnails\u002Fserverless-data-architecture.png","5puaNbRfvwcsew1zr4NPBWVcp6vptf8ytTvNFyvu5x0",{"id":2951,"title":2952,"body":2953,"date":3096,"description":3097,"extension":426,"meta":3098,"navigation":144,"path":3099,"readTime":3100,"seo":3101,"stem":3102,"tags":3103,"thumbnail":3107,"__hash__":3108},"blog\u002Fblog\u002Fwelcome-digital-sandbox.md","🚀 Welcome to My Digital Sandbox!",{"type":79,"value":2954,"toc":3091},[2955,2962,2975,2982,2986,2993,2996,2999,3006,3012,3016,3027,3030,3032,3036,3039,3079,3085],[82,2956,2957,2961],{},[2958,2959],"mention",{"name":2960},"Joe"," thinks I’m doing some amazing stuff and honestly, he wanted a front-row seat to read about it.",[82,2963,2964,2966,2967,2970,2971,2974],{},[2958,2965],{"name":2960}," and I spend ",[1324,2968,2969],{},"a lot"," of time on the phone geeking out over system ideas, software architectures, and the future of tech. Our current record? A massive ",[1410,2972,2973],{},"1.2-hour WhatsApp voice call"," just syncing up and talking shop about this wild field of ours.",[82,2976,2977],{},[2978,2979],"img",{"alt":2980,"src":2981},"Geeking Out over Tech","https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExM3N6Z3oxM295M3Z1bnduMms0ZXBtZ3M4b3A4dmJ6bGR5cjRyeXN6ayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002F3knKct3vGqxhK\u002Fgiphy.gif",[1863,2983,2985],{"id":2984},"the-ai-superpower-the-80-trap","⚡ The AI Superpower & The \"80% Trap\"",[82,2987,2988,2989,2992],{},"Fortunately for me—and I mean ",[1324,2990,2991],{},"fortunately","—AI has completely changed the game.",[82,2994,2995],{},"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,2997,2998],{},"⚡ IDEAS -> PROTOTYPE -> NEXT BIG THING",[82,3000,3001,3002,3005],{},"I'm the kind of engineer who always needs something active to chew on. Like many creatives, I suffer from ",[1410,3003,3004],{},"\"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,3007,3008],{},[2978,3009],{"alt":3010,"src":3011},"Moving Fast and Breaking Things","https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExM3F0ZnNxZ3F3b3Rndm85bXJ6am8xb3Z5aG83YmE0bHd5ZTN0amVxdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002FunQ3IJU2RG7DO\u002Fgiphy.gif",[1863,3013,3015],{"id":3014},"dusting-off-the-frontend-goodbye-for-now-c","🎨 Dusting off the Frontend (Goodbye for now, C#!)",[82,3017,3018,3019,3022,3023,3026],{},"I put this portfolio together to test and sharpen my ",[1410,3020,3021],{},"Vue\u002FNuxt"," skills. It’s been about 4 months since my last Vue ecosystem project because, honestly... ",[1410,3024,3025],{},"C# has completely taken over my life lately!"," 🧱",[82,3028,3029],{},"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.",[473,3031],{},[86,3033,3035],{"id":3034},"️-what-can-you-expect-here","🗺️ What Can You Expect Here?",[82,3037,3038],{},"Here is a roadmap of the chaos and insights I'll be dumping into this space:",[1403,3040,3041,3047,3055,3061,3067,3073],{},[1406,3042,3043,3046],{},[1410,3044,3045],{},"🏢 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.",[1406,3048,3049,3052,3053,608],{},[1410,3050,3051],{},"🐍 Python Tips & Tricks:"," It's been a minute since I touched Django, so expect clean, modern ways of doing things using ",[1410,3054,1310],{},[1406,3056,3057,3060],{},[1410,3058,3059],{},"🔷 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.",[1406,3062,3063,3066],{},[1410,3064,3065],{},"🐳 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.",[1406,3068,3069,3072],{},[1410,3070,3071],{},"🤖 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.",[1406,3074,3075,3078],{},[1410,3076,3077],{},"🌱 Life & Daily Learnings:"," General thoughts, philosophical brain dumps, and a running log of the new things I learn every single day.",[82,3080,3081],{},[2978,3082],{"alt":3083,"src":3084},"Let's Build","https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExbmswM3g4amw0NndpZ3V5cm15NmFyeXN6bTZtc3Z6bndvbmNndXFhNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002FYl5aO3gdVfsQ0\u002Fgiphy.gif",[82,3086,3087,3090],{},[1324,3088,3089],{},"Stay tuned. We're going to break some things!"," 🔥",{"title":114,"searchDepth":21,"depth":21,"links":3092},[3093,3094,3095],{"id":2984,"depth":30,"text":2985},{"id":3014,"depth":30,"text":3015},{"id":3034,"depth":21,"text":3035},"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":2952,"description":3097},"blog\u002Fwelcome-digital-sandbox",[3104,3105,3106,2960],"Intro","AI","Vue","\u002Fimages\u002Fthumbnails\u002Fdefault-og.png","hAqc8DbFG0UL-U5LDqx02VYU4PefMTXwwLjuDtlfwNY",1779361989256]