Global Nginx Reverse Proxy: Unified Multi-Project Server Architecture with Docker
This article presents a server architecture using a single global Nginx container as the unified public entry point for multiple projects, detailing Docker Compose setup, public network configuration, SSL certificate management, and operational practices for maintainable multi-project deployments.
This is the 23rd article in the "Deploy Yourself to the Internet" series. Previous articles discussed whether Nginx should run on the host or in Docker (article 21) and why not to run a separate Nginx container per project (article 22). The author runs multiple projects — blog, personal site, portfolio, tools, experiments — on a single personal server and needs a manageable architecture.
Problems Solved
Public Entry Must Be Centralized
A server can host multiple projects, but external access should be concentrated. Users should reach blogs or sites via standard ports 80 and 443 without remembering port numbers.
Certificates Must Not Be Scattered
If HTTPS certificates live inside each project, renewal, mounting, reload, and backup become cumbersome.
Databases and Internal Services Must Not Be Exposed
Internal services stay in the internal network, proxied by Nginx; databases are only accessible to their own project.
Multi-Project Must Be Extensible
Today a blog, tomorrow a site, later more tools. Adding a project should follow a fixed onboarding pattern, not a redesign each time.
Migration Must Be Clear
When moving servers, the locations of Nginx config, certificates, project code, and data must be obvious.
These combine into one principle: external entry centralized, internal projects independent .
Overall Architecture Design
High-level flow:
Public User
-> Domain DNS
-> Server 80/443
-> Global Nginx Container
-> Different Project ServicesConcrete domain routing examples:
blog.aicultiv.com -> Global Nginx -> blog-app container
www.aicultiv.com -> Global Nginx -> site-app container or static directory
api.aicultiv.com -> Global Nginx -> api-service containerDatabases are not public: blog-app container -> blog-db container Each project manages its own application, database, cache, and background jobs via its own Docker Compose. The global Nginx handles only public ingress.
Server Directory Layout
For long-term maintainability, the author plans:
/opt/coduty/
nginx/
compose.yml
conf.d/
certs/
logs/
projects/
blog/
compose.yml
.env
data/
site/
compose.yml
.env
dist/
backups/
scripts/Core ideas: global Nginx gets its own directory; each project gets its own; config, certs, logs, data are separated; backup and script locations are fixed. This structure directly affects future troubleshooting and migration.
Global Nginx Docker Compose
services:
nginx:
image: nginx:alpine
container_name: global-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/nginx/certs:ro
- ./logs:/var/log/nginx
networks:
- public
networks:
public:
external: trueKey points:
Only global Nginx binds public 80 and 443.
Other projects avoid direct public port mapping.
Nginx config mounted into container for easy editing, backup, migration.
Certificates also mounted.
Nginx joins an external public network; proxied project services also join this network.
Public Network
To proxy to different project containers, global Nginx must reach them. Separate Compose projects cannot communicate by default, so create a shared network: docker network create coduty-public Global Nginx joins this network. Application containers that need to be proxied also join it. Internal project services keep their own default network. Databases typically do not join the public network; they are only accessible to the project's own app.
Project service Compose snippet:
services:
app:
networks:
- default
- coduty-public
networks:
coduty-public:
external: trueThis separates external ingress from internal data services.
Domain Routing in Global Nginx
Domain matching lives in global Nginx server blocks. Example for blog (HTTP only; HTTPS adds 443, cert paths, and redirect):
server {
listen 80;
server_name blog.example.com;
location / {
proxy_pass http://blog-app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Core idea: domain matching in global Nginx, project services only expose internal ports, Nginx forwards based on domain .
New Project Onboarding Workflow
Project service joins the public network.
Confirm service name and port.
Add a server block in global Nginx conf.d.
Check Nginx syntax.
Reload Nginx.
Configure DNS and HTTPS.
SSL Certificate Management
Certificates centralized under global Nginx directory:
/opt/coduty/nginx/certs/
example.com/
fullchain.pem
privkey.pemNginx configs reference these paths. Application, renewal, and reload all happen in one place. Wildcard certificates (e.g., *.example.com) can serve multiple subdomains; the author notes wildcard certs involve DNS validation and private key protection, to be handled separately.
Summary
Global Nginx as unified entry does not mean projects are mixed. Each project remains independent with its own compose.yml, .env, app container, database container, volumes, init scripts, and backup directory. The site project can have its own build process, static files, and deploy container.
Global Nginx cares about only one thing: which internal service a domain should forward to. It does not manage project databases or business logic.
Benefits
Clear public entry: only global Nginx occupies 80 and 443.
Fewer exposed ports: internal services not directly public.
Centralized certificate management.
Fixed onboarding process for new projects.
Layered troubleshooting: DNS → security group → firewall → global Nginx → project container → app logs.
Migration-friendly: Nginx config, certs, project directories have fixed locations.
Risks and Mitigations
Global Nginx becomes a critical node; config error can affect multiple projects.
Strict operational discipline: backup before changes; check syntax after changes; reload only after syntax passes.
One config file per domain; avoid stuffing all rules into a single large file.
Logs must be queryable.
Containerized Nginx syntax check and reload:
docker exec global-nginx nginx -t
docker exec global-nginx nginx -s reloadSuggested config file layout:
conf.d/
blog.aicultiv.com.conf
www.aicultiv.com.conf
api.aicultiv.com.confFinal architecture in bullets:
Public traffic enters only global Nginx on 80 and 443.
Nginx routes by domain to different projects.
Projects stay independent, managed by their own Docker Compose (app, DB, volumes).
Databases not exposed publicly.
Certificates managed centrally.
New projects onboard via uniform rules.
The author emphasizes this suits a single-machine, multi-project, solo-maintainer scenario. The next article will cover hands-on implementation: global Nginx container, public network, config files, and startup verification.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code of Duty
"Code of Duty" — Every line of code has its own mission. We avoid shortcuts and quick fixes, focusing on authentic coding reflections and the joys and challenges of technical growth. The journey of learning matters more than any destination. Join us as we humbly forge ahead in the world of code.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
