[{"data":1,"prerenderedAt":527},["ShallowReactive",2],{"project-dokku-nginx-path":3},{"id":4,"title":5,"body":6,"description":507,"duration":508,"extension":509,"featured":510,"github":511,"image":512,"live":513,"meta":514,"navigation":240,"order":256,"path":515,"role":513,"seo":516,"status":517,"stem":518,"team_size":513,"tech":519,"type":524,"year":525,"__hash__":526},"projects\u002Fproject\u002Fdokku-nginx-path.md","Dokku Nginx Path",{"type":7,"value":8,"toc":499},"minimark",[9,14,23,46,50,65,99,103,108,113,138,143,171,176,192,196,201,204,399,403,406,495],[10,11,13],"h2",{"id":12},"the-problem","The Problem",[15,16,17,18,22],"p",{},"Standard Dokku deployments force a subdomain-based architecture (e.g., ",[19,20,21],"code",{},"app.domain.com","), which creates significant friction for specific architectural patterns. Developers faced challenges with:",[24,25,26,34,40],"ul",{},[27,28,29,33],"li",{},[30,31,32],"strong",{},"SEO Fragmentation:"," Splitting traffic across subdomains dilutes domain authority compared to subdirectories.",[27,35,36,39],{},[30,37,38],{},"SSL Complexity:"," Requiring wildcard certificates or managing separate certs for every microservice.",[27,41,42,45],{},[30,43,44],{},"Unified Gateway Needs:"," Difficulty in presenting multiple microservices (e.g., frontend, api, admin) as a single monolithic endpoint to the client without an external reverse proxy layer.",[10,47,49],{"id":48},"my-solution","My Solution",[15,51,52,53,56,57,60,61,64],{},"I engineered a custom Dokku plugin that intercepts the Nginx configuration lifecycle to enable ",[30,54,55],{},"Path-Based Routing"," (e.g., ",[19,58,59],{},"domain.com\u002Fapp1",", ",[19,62,63],{},"domain.com\u002Fapi",").",[24,66,67,73,87,93],{},[27,68,69,72],{},[30,70,71],{},"Unified Domain Root:"," Serves multiple isolated Dokku apps under one domain name.",[27,74,75,78,79,82,83,86],{},[30,76,77],{},"\"Default App\" Authority:"," Implemented a master-slave logic where one \"Default App\" controls the root configuration (",[19,80,81],{},"\u002F","), while secondary apps inject their specific ",[19,84,85],{},"location"," blocks.",[27,88,89,92],{},[30,90,91],{},"Automated Config Merging:"," Hooks into Dokku's deploy cycle to automatically rebuild the Nginx map whenever a linked application is deployed or updated.",[27,94,95,98],{},[30,96,97],{},"Hybrid Architecture:"," Utilizes Bash for Dokku lifecycle hooks and Go for high-performance property parsing.",[10,100,102],{"id":101},"technical-deep-dive","Technical Deep Dive",[104,105,107],"h3",{"id":106},"architecture-decisions","Architecture Decisions",[15,109,110],{},[30,111,112],{},"Why Hybrid Bash & Go?",[24,114,115,128],{},[27,116,117,120,121,60,124,127],{},[30,118,119],{},"Bash:"," Essential for deep integration with Dokku's plugin trigger system (",[19,122,123],{},"pre-deploy",[19,125,126],{},"proxy-build-config",") which relies on shell scripts.",[27,129,130,133,134,137],{},[30,131,132],{},"Go:"," Replaced complex shell string manipulation with Go for the property management system (",[19,135,136],{},"nginx-property","). This ensures type safety and faster execution when parsing complex configuration maps for hundreds of potential vhosts.",[15,139,140],{},[30,141,142],{},"Why Sigil over standard Nginx includes?",[24,144,145,152],{},[27,146,147,148,151],{},"Standard Nginx ",[19,149,150],{},"include"," directives were insufficient for dynamic upstream port mapping.",[27,153,154,155,158,159,162,163,166,167,170],{},"I leveraged ",[30,156,157],{},"Glider Labs' Sigil"," templating engine to dynamically loop through ",[19,160,161],{},"PROXY_PORT_MAP"," and ",[19,164,165],{},"DOKKU_APP_LISTENERS"," at runtime, generating a valid, cohesive ",[19,168,169],{},"nginx.conf"," that routes traffic based on internal Docker IP changes.",[15,172,173],{},[30,174,175],{},"Handling the \"Default App\" Concept",[24,177,178],{},[27,179,180,181,184,185,188,189,191],{},"To prevent routing collisions, I architected a priority system. The plugin identifies a ",[19,182,183],{},"default-app"," via a custom property. This app generates the main server block, while all other apps identified by ",[19,186,187],{},"app-path"," are rendered as upstream definitions and included via ",[19,190,150],{}," directives, preventing \"duplicate location\" errors.",[104,193,195],{"id":194},"key-features-i-built","Key Features I Built",[197,198,200],"h4",{"id":199},"_1-dynamic-location-block-generation-sigil-template","1. Dynamic Location Block Generation (Sigil Template)",[15,202,203],{},"This snippet demonstrates how the plugin iterates through listeners to construct Nginx location blocks dynamically, handling SSL redirects and header forwarding automatically.",[205,206,211],"pre",{"className":207,"code":208,"language":209,"meta":210,"style":210},"language-bash shiki shiki-themes github-light github-dark","# nginx\n{{ range $port_map := .PROXY_PORT_MAP | split \" \" }}\n  # ... (upstream logic)\n\n  # Current app routing\n  {{ $current_app_path := $.APP_PATH | default $.APP }}\n  location \u002F{{ $current_app_path }}\u002F {\n    {{ if eq $.STRIP_PATH \"true\" }}\n      rewrite \"^\u002F{{ $current_app_path }}\u002F(.*)\" \"\u002F$1\" break;\n    {{ end }}\n    proxy_pass http:\u002F\u002F{{ $.APP }}-{{ $upstream_port }}\u002F;\n\n    # Automated Header Injection for Context\n    proxy_set_header X-Forwarded-Prefix \u002F{{ $current_app_path }}\u002F;\n    proxy_set_header X-Script-Name \u002F{{ $current_app_path }};\n  }\n{{ end }}\n","bash","",[19,212,213,222,229,235,242,248,254,274,280,311,317,342,347,353,370,387,393],{"__ignoreMap":210},[214,215,218],"span",{"class":216,"line":217},"line",1,[214,219,221],{"class":220},"sJ8bj","# nginx\n",[214,223,225],{"class":216,"line":224},2,[214,226,228],{"class":227},"sVt8B","{{ range $port_map := .PROXY_PORT_MAP | split \" \" }}\n",[214,230,232],{"class":216,"line":231},3,[214,233,234],{"class":220},"  # ... (upstream logic)\n",[214,236,238],{"class":216,"line":237},4,[214,239,241],{"emptyLinePlaceholder":240},true,"\n",[214,243,245],{"class":216,"line":244},5,[214,246,247],{"class":220},"  # Current app routing\n",[214,249,251],{"class":216,"line":250},6,[214,252,253],{"class":227},"  {{ $current_app_path := $.APP_PATH | default $.APP }}\n",[214,255,257,261,265,268,271],{"class":216,"line":256},7,[214,258,260],{"class":259},"sScJk","  location",[214,262,264],{"class":263},"sZZnC"," \u002F{{",[214,266,267],{"class":227}," $current_app_path ",[214,269,270],{"class":263},"}}\u002F",[214,272,273],{"class":263}," {\n",[214,275,277],{"class":216,"line":276},8,[214,278,279],{"class":227},"    {{ if eq $.STRIP_PATH \"true\" }}\n",[214,281,283,286,289,292,295,298,302,305,308],{"class":216,"line":282},9,[214,284,285],{"class":259},"      rewrite",[214,287,288],{"class":263}," \"^\u002F{{ ",[214,290,291],{"class":227},"$current_app_path",[214,293,294],{"class":263}," }}\u002F(.*)\"",[214,296,297],{"class":263}," \"\u002F",[214,299,301],{"class":300},"sj4cs","$1",[214,303,304],{"class":263},"\"",[214,306,307],{"class":263}," break",[214,309,310],{"class":227},";\n",[214,312,314],{"class":216,"line":313},10,[214,315,316],{"class":227},"    {{ end }}\n",[214,318,320,323,326,329,332,335,338,340],{"class":216,"line":319},11,[214,321,322],{"class":259},"    proxy_pass",[214,324,325],{"class":263}," http:\u002F\u002F{{",[214,327,328],{"class":227}," $",[214,330,331],{"class":263},".APP",[214,333,334],{"class":263}," }}-{{",[214,336,337],{"class":227}," $upstream_port ",[214,339,270],{"class":263},[214,341,310],{"class":227},[214,343,345],{"class":216,"line":344},12,[214,346,241],{"emptyLinePlaceholder":240},[214,348,350],{"class":216,"line":349},13,[214,351,352],{"class":220},"    # Automated Header Injection for Context\n",[214,354,356,359,362,364,366,368],{"class":216,"line":355},14,[214,357,358],{"class":259},"    proxy_set_header",[214,360,361],{"class":263}," X-Forwarded-Prefix",[214,363,264],{"class":263},[214,365,267],{"class":227},[214,367,270],{"class":263},[214,369,310],{"class":227},[214,371,373,375,378,380,382,385],{"class":216,"line":372},15,[214,374,358],{"class":259},[214,376,377],{"class":263}," X-Script-Name",[214,379,264],{"class":263},[214,381,267],{"class":227},[214,383,384],{"class":263},"}}",[214,386,310],{"class":227},[214,388,390],{"class":216,"line":389},16,[214,391,392],{"class":227},"  }\n",[214,394,396],{"class":216,"line":395},17,[214,397,398],{"class":227},"{{ end }}\n",[197,400,402],{"id":401},"_2-lifecycle-hook-interception","2. Lifecycle Hook Interception",[15,404,405],{},"The plugin doesn't just sit idle; it actively listens to deployment events. If a secondary app is deployed, the plugin forces a rebuild of the primary proxy configuration to ensure the new path is live immediately.",[205,407,409],{"className":207,"code":408,"language":209,"meta":210,"style":210},"# From: noes\u002Fproxy-build-config\ntrigger-nginx-path-vhosts-proxy-build-config() {\n  declare APP=\"$1\"\n  # ... checks ...\n\n  # Trigger network config build ensuring IP tables are updated\n  plugn trigger network-build-config \"$APP\"\n\n  # Rebuild the complex Nginx config map\n  nginx_build_config \"$APP\"\n}\n",[19,410,411,416,424,437,442,446,451,470,474,479,490],{"__ignoreMap":210},[214,412,413],{"class":216,"line":217},[214,414,415],{"class":220},"# From: noes\u002Fproxy-build-config\n",[214,417,418,421],{"class":216,"line":224},[214,419,420],{"class":259},"trigger-nginx-path-vhosts-proxy-build-config",[214,422,423],{"class":227},"() {\n",[214,425,426,429,432,434],{"class":216,"line":231},[214,427,428],{"class":300},"  declare",[214,430,431],{"class":263}," APP=\"",[214,433,301],{"class":300},[214,435,436],{"class":263},"\"\n",[214,438,439],{"class":216,"line":237},[214,440,441],{"class":220},"  # ... checks ...\n",[214,443,444],{"class":216,"line":244},[214,445,241],{"emptyLinePlaceholder":240},[214,447,448],{"class":216,"line":250},[214,449,450],{"class":220},"  # Trigger network config build ensuring IP tables are updated\n",[214,452,453,456,459,462,465,468],{"class":216,"line":256},[214,454,455],{"class":259},"  plugn",[214,457,458],{"class":263}," trigger",[214,460,461],{"class":263}," network-build-config",[214,463,464],{"class":263}," \"",[214,466,467],{"class":227},"$APP",[214,469,436],{"class":263},[214,471,472],{"class":216,"line":276},[214,473,241],{"emptyLinePlaceholder":240},[214,475,476],{"class":216,"line":282},[214,477,478],{"class":220},"  # Rebuild the complex Nginx config map\n",[214,480,481,484,486,488],{"class":216,"line":313},[214,482,483],{"class":259},"  nginx_build_config",[214,485,464],{"class":263},[214,487,467],{"class":227},[214,489,436],{"class":263},[214,491,492],{"class":216,"line":319},[214,493,494],{"class":227},"}\n",[496,497,498],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":210,"searchDepth":224,"depth":224,"links":500},[501,502,503],{"id":12,"depth":224,"text":13},{"id":48,"depth":224,"text":49},{"id":101,"depth":224,"text":102,"children":504},[505,506],{"id":106,"depth":231,"text":107},{"id":194,"depth":231,"text":195},"A Dokku plugin enabling path-based routing for multiple applications under a single domain, featuring automated Nginx configuration merging and unified SSL management.","1 months","md",false,"https:\u002F\u002Fgithub.com\u002Fszuryuu\u002Fnginx-path-vhost","\u002Fimages\u002Fprojects\u002Fdokku.png",null,{},"\u002Fproject\u002Fdokku-nginx-path",{"title":5,"description":507},"Completed","project\u002Fdokku-nginx-path",[520,521,522,523],"Bash","Go","Nginx","Dokku","Intern Project","2025","O8Vk3PTV0iumMcyJIG2lFwWmsAYVYs4lZzR-jfCoQoc",1776582962961]