If you run a busy homelab, you already know the pattern.
You spin up a new app. Then another one. Then you rebuild one, move ports around, rename a
container, or split a stack into two services. Suddenly Nginx is broken, your proxy targets
are stale, and you are SSHing into the box again to “just fix one small thing.”
That gets old fast.
The fix is not more manual documentation. The fix is a better deployment pattern.
## The Problem With Port-Based Homelab Growth
A lot of homelabs start like this:
- app A on localhost:5000
- app B on localhost:8080
- app C on localhost:3000
- Nginx reverse proxies to host ports
- every new stack means another port decision This works at first, but it does not age well. As your homelab grows, you end up with:
- random port sprawl
- collisions between stacks
- broken Nginx routes after redeploys
- hard-to-remember service locations
- fragile configs tied to container names or host ports The worst part is that every app change leaks into your proxy layer. That is the wrong dependency direction. The Better Pattern The clean model is:
- Each app lives in its own Docker Compose stack
- The app joins a shared external Docker network
- Nginx also joins that same shared network
- Each app exposes a stable network alias
- Nginx routes to the alias, not to a changing host port That means your proxy stops caring how the app is built internally. It just knows:
- service name: auction-fastview
- port: 5050 If you rebuild the stack, recreate containers, or update the image, Nginx keeps talking to the
same internal DNS name. Why This Matters in a Homelab If you are constantly changing your lab, stability matters more than elegance. You want: - stack internals to be flexible
- reverse proxy targets to stay fixed
- new apps to be easy to onboard
- old apps to survive redeploys without extra proxy edits A shared Docker network gives you that. Docker provides internal DNS on the network. If your app has the alias auction-fastview, anyt
hing else on that network can reach it at http://auction-fastview:5050. That is the whole trick. The Compose Pattern Here is the important part of the Compose file: services:
app:
build: .
ports:
– “5050:5050”
networks:
default: {}
proxy_shared:
aliases:
– auction-fastview networks:
proxy_shared:
external: true That does two things: - keeps the app on its normal default stack network
- also attaches it to proxy_shared with a stable alias The default network is for internal stack traffic. The proxy_shared network is for cross-stack access from Nginx. Why Use an Alias? Because container names and Compose-generated names are not a stable contract. You do not want Nginx targeting something like: auction-fastview-app-1 That can change. The alias should not. This is what Nginx should care about:
- hostname: auction-fastview
- port: 5050 That is stable, readable, and easy to reason about. Nginx Side Your Nginx container also needs to be attached to the same external network. Then your upstream can be as simple as: location / {
proxy_pass http://auction-fastview:5050;
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;
} That is much better than pointing at random host ports and hoping nothing changes. What About Nginx Proxy Manager? Same idea. If you use Nginx Proxy Manager, the target should be: - Forward Hostname / IP: auction-fastview
- Forward Port: 5050
- Scheme: http As long as both containers are on proxy_shared, it works. One Shared Network, Many Stacks This pattern scales well across a homelab. You can have:
- whoami
- grafana
- paperless
- immich
- auction-fastview All on the same proxy network, each with its own stable alias. Your reverse proxy becomes a routing layer, not a guessing layer. Should You Still Publish Host Ports? Usually, yes during early setup. It makes debugging easier. For example: ports:
- “5050:5050”
and keep the service reachable only over the shared Docker network. That is often cleaner. One Important Caution An external network must already exist before Compose can use it. Create it once on the host: docker network create proxy_shared After that, every stack can join it with: networks:
proxy_shared:
external: true Do not redefine it differently in each stack. Just reuse it. The Real Win The real benefit is not technical purity. It is operational sanity. When you are constantly updating your homelab, you want app deployments to feel routine: - update the stack
- redeploy it
- Nginx keeps working No new port hunting. No proxy rewrites. No “what is this container called now?” That is the payoff. Recommended Homelab Rule For every new web app stack:
- give it its own Compose file
- attach the app service to proxy_shared
- give it a stable alias
- point Nginx or NPM at that alias
- stop routing to random host ports unless you actually need them That one habit removes a lot of friction. Final Thought Homelabs get messy because they grow faster than their conventions. If you put one convention in place, make it this one: Route by Docker network name, not by container churn. Your future self will spend less time fixing proxies and more time actually building the next
stack.
Leave a Reply