What you'll need
- A VPS on Ubuntu 22.04/24.04 with a dedicated IPv4.
- Two or more subdomains with A records pointing at the IP (e.g. app1.example.com, app2.example.com).
- Docker installed (see Step 1).
Step 1: Install Docker and open ports
curl -fsSL https://get.docker.com | sh
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enableStep 2: Plan the network
Every app and the proxy join one shared Docker network so Caddy can reach each container by its service name. The apps themselves do NOT publish ports to the host; only Caddy does. That keeps your apps off the public internet except through the proxy.
Step 3: Write the compose file
Create /opt/stack/docker-compose.yml with a proxy and two example web apps. Whoami and a static nginx stand in for your real services; swap in your own images.
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
networks:
- web
app1:
image: traefik/whoami
restart: unless-stopped
networks:
- web
app2:
image: nginx:alpine
restart: unless-stopped
networks:
- web
networks:
web:
volumes:
caddy-data:Step 4: Route by hostname in the Caddyfile
Each domain block proxies to a container by service name and internal port. Caddy issues a certificate per domain on first request.
app1.example.com {
reverse_proxy app1:80
}
app2.example.com {
reverse_proxy app2:80
}Step 5: Launch the stack
cd /opt/stack
docker compose up -d
docker compose psVisit each subdomain over HTTPS. app1 shows the whoami debug page; app2 shows the nginx welcome page. Adding a third app is three lines in compose plus one block in the Caddyfile.
Step 6: Watch resources on a small box
Containers share the host's RAM and CPU, so a tiny box can run out of memory if one app is hungry. Cap each service and keep an eye on usage.
docker stats --no-streamAdd limits per service in compose when needed:
deploy:
resources:
limits:
memory: 256mKeep it running
- restart: unless-stopped brings every container back after a reboot.
- Update everything: docker compose pull && docker compose up -d.
- Reclaim disk from old images: docker system prune -af.
- If RAM gets tight on a small box, add a swap file as a safety net rather than oversubscribing.
This is the cheapest way to run several side projects at once: one box, one bill, hourly. NVMe storage keeps image pulls and container starts fast, and you can deploy a fresh server in about 60 seconds, then destroy it the hour your experiment ends.