
Dokku Nginx Path
A Dokku plugin enabling path-based routing for multiple applications under a single domain, featuring automated Nginx configuration merging and unified SSL management.
Tech Stack
Duration
1 months
Published
2025
Table of Contents
The Problem
Standard Dokku deployments force a subdomain-based architecture (e.g., app.domain.com), which creates significant friction for specific architectural patterns. Developers faced challenges with:
- SEO Fragmentation: Splitting traffic across subdomains dilutes domain authority compared to subdirectories.
- SSL Complexity: Requiring wildcard certificates or managing separate certs for every microservice.
- 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.
My Solution
I engineered a custom Dokku plugin that intercepts the Nginx configuration lifecycle to enable Path-Based Routing (e.g., domain.com/app1, domain.com/api).
- Unified Domain Root: Serves multiple isolated Dokku apps under one domain name.
- "Default App" Authority: Implemented a master-slave logic where one "Default App" controls the root configuration (
/), while secondary apps inject their specificlocationblocks. - Automated Config Merging: Hooks into Dokku's deploy cycle to automatically rebuild the Nginx map whenever a linked application is deployed or updated.
- Hybrid Architecture: Utilizes Bash for Dokku lifecycle hooks and Go for high-performance property parsing.
Technical Deep Dive
Architecture Decisions
Why Hybrid Bash & Go?
- Bash: Essential for deep integration with Dokku's plugin trigger system (
pre-deploy,proxy-build-config) which relies on shell scripts. - Go: Replaced complex shell string manipulation with Go for the property management system (
nginx-property). This ensures type safety and faster execution when parsing complex configuration maps for hundreds of potential vhosts.
Why Sigil over standard Nginx includes?
- Standard Nginx
includedirectives were insufficient for dynamic upstream port mapping. - I leveraged Glider Labs' Sigil templating engine to dynamically loop through
PROXY_PORT_MAPandDOKKU_APP_LISTENERSat runtime, generating a valid, cohesivenginx.confthat routes traffic based on internal Docker IP changes.
Handling the "Default App" Concept
- To prevent routing collisions, I architected a priority system. The plugin identifies a
default-appvia a custom property. This app generates the main server block, while all other apps identified byapp-pathare rendered as upstream definitions and included viaincludedirectives, preventing "duplicate location" errors.
Key Features I Built
1. Dynamic Location Block Generation (Sigil Template)
This snippet demonstrates how the plugin iterates through listeners to construct Nginx location blocks dynamically, handling SSL redirects and header forwarding automatically.
# nginx
{{ range $port_map := .PROXY_PORT_MAP | split " " }}
# ... (upstream logic)
# Current app routing
{{ $current_app_path := $.APP_PATH | default $.APP }}
location /{{ $current_app_path }}/ {
{{ if eq $.STRIP_PATH "true" }}
rewrite "^/{{ $current_app_path }}/(.*)" "/$1" break;
{{ end }}
proxy_pass http://{{ $.APP }}-{{ $upstream_port }}/;
# Automated Header Injection for Context
proxy_set_header X-Forwarded-Prefix /{{ $current_app_path }}/;
proxy_set_header X-Script-Name /{{ $current_app_path }};
}
{{ end }}
2. Lifecycle Hook Interception
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.
# From: noes/proxy-build-config
trigger-nginx-path-vhosts-proxy-build-config() {
declare APP="$1"
# ... checks ...
# Trigger network config build ensuring IP tables are updated
plugn trigger network-build-config "$APP"
# Rebuild the complex Nginx config map
nginx_build_config "$APP"
}
