Running Multiple Apps on One Home Server — Single Caddy + web_edge Pattern, Practical Guide
How I run a stock app, an art gallery, and a bot on the same server — sharing the full setup
Right now, three apps are running simultaneously on my home server: stock portfolio app trading_mvp, online art gallery Hansaiam, and Discord ↔ second-brain loop bot hermes-discord. Monthly server cost: $0.
Not using the cloud was never part of the original plan. It started with “I’ve got a computer sitting at home anyway, let me test it there” — and it worked well enough that I just stayed.
The problem: port conflicts every time you add another app
Adding a single app is straightforward. Open port 80, spin up Caddy, done. But the moment you add a second app, you’ve got a problem. Two apps can’t share the same 80/443.
The common solution is splitting ports — 8080, 3000, 5000, and so on — but then you’ve got port numbers cluttering your URLs, and you’re managing TLS certificates separately for each app. The bigger headache is the manual “is this port free?” check every time you want to add something new.
Approach: single Caddy + shared web_edge network
Here’s the pattern I use.
One Caddy instance owns 80/443, and any app that needs to be reachable from the outside connects to a Docker network called web_edge. Caddy lives inside that network and routes to each app by container name.
Internet → Caddy(80/443) → web_edge network
├── trading_mvp (container)
└── hansaiam (container)
hermes-discord → Discord gateway (outbound only, no web_edge needed)
The Caddyfile is simple. Just declare which container each domain should route to:
trading.duckdns.org {
reverse_proxy trading_backend:8000
}
hansaiam.duckdns.org {
reverse_proxy hansaiam_web:3000
}
To add a new app, you add two lines connecting it to web_edge in docker-compose.yml and drop one domain block in the Caddyfile. No port conflicts to worry about.
Step by step
Step 1: Create the web_edge network
docker network create web_edge
Do this once. After that, any container that joins this network is automatically reachable by Caddy.
Step 2: Playbook for deploying a new app
ssh hong
git clone https://github.com/korat070/<app-name>.git ~/app-name
cd ~/app-name
cp .env.example .env
nano .env # fill in secrets
Add this to docker-compose.yml to connect to web_edge:
networks:
web_edge:
external: true
Then docker compose up -d and Caddy picks it up automatically.
Step 3: Point a domain with DuckDNS
This is where I need to share a detour. I initially tried using korat.iptime.org (the router’s built-in DDNS) as-is, but Let’s Encrypt refused to issue a certificate. I spent two days going through Caddy logs, port forwarding settings, and firewall rules without finding the cause — until I happened to run this:
dig +short CAA iptime.org
Result: 0 issue ";" — a record that blocks every CA from issuing certificates for that domain. Since I have no control over the iptime.org parent domain DNS, there was nothing I could do. Switching to DuckDNS was the only way out.
DuckDNS is free, has no CAA record restrictions, and handles dynamic IP tracking out of the box. Ever since, I make it a habit to run dig +short CAA <domain> before using any new domain.
Why this works
The reason this pattern stays clean is that the responsibilities are clearly separated.
- Caddy: sole entry point for external traffic, handles TLS and domain routing
- web_edge: internal communication network between Caddy and app containers
- Each app: only needs to expose its port inside web_edge — no host port exposure required
Apps like hermes-discord that don’t receive inbound traffic (the bot connects out to Discord, not the other way around) don’t need web_edge at all. They only make outbound connections. You just docker compose up -d and they run completely independently.
Next
Part 2 covers the SSH brute-force attempts that started filling up my logs the moment I opened the server to the outside, and a three-step security hardening playbook (SSH keys → disable password auth → fail2ban — the order matters).
If you have a computer sitting idle at home, it can become a $0-per-month server. With the web_edge pattern from this post, adding an app is one line in docker-compose.yml.
Next →: Three painful mistakes deploying a stock app to my home server Background: Why I ended up running Hermes on a home server · See also: Discord ↔ wiki loop bot build log · Stock app deployment war stories · Hansaiam art gallery dev log