slowlp
← Blog
Method 2026.06.11 · 8 min read

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

Method

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

Related
All posts →
COMMENTS