For independent developers and small teams, renting managed cloud infrastructures (like AWS RDS or Managed EKS) for personal projects quickly results in bloated monthly bills.
By leveraging Docker Compose and Nginx Proxy Manager (or Caddy), you can orchestrate dozens of distinct web apps, PostgreSQL/MySQL databases, Redis instances, and asynchronous background workers on a single affordable Linux VPS with complete network isolation.
graph TD
User[Internet Traffic] -->|Port 80 & 443| NPM[Nginx Proxy Manager / Ingress]
NPM -->|SSL Termination via Cloudflare| NPM
subgraph Isolated Docker Network: App-1
NPM -->|Internal Proxy: Port 8000| Web1[Laravel Octane / App 1]
Web1 --> DB1[(PostgreSQL 16 DB)]
Web1 --> RD1[(Redis 7)]
end
subgraph Isolated Docker Network: App-2
NPM -->|Internal Proxy: Port 3000| Web2[Node.js / React App 2]
Web2 --> DB2[(MySQL 8 DB)]
end
1. Network Isolation via Docker Bridge Networks
The foundational security rule when hosting multi-tenant containers on one machine is: Never expose database ports to the public (0.0.0.0) interface:
- Configure isolated private bridge networks per application service stack (e.g.
app1-network,app2-network). - Only attach the Ingress Reverse Proxy container to both the outer proxy network and individual application bridges.
- Internal services (PostgreSQL
5432, Redis6379) remain completely unreachable from outside the host machine.
networks:
proxy-network:
external: true
internal-network:
driver: bridge
services:
app:
image: my-app:latest
networks:
- proxy-network
- internal-network
db:
image: postgres:16-alpine
networks:
- internal-network
# No public port bindings -> 100% Isolated
2. Automated Wildcard SSL via Cloudflare DNS Challenge
Requesting individual TLS certificates per subdomain frequently hits Let’s Encrypt rate limits. The optimal solution is provisioning Wildcard SSL certificates (*.domain.com):
- Connect a scoped Cloudflare DNS API Token in Nginx Proxy Manager.
- Certificates auto-renew quietly in the background without needing port 80 HTTP challenges.
3. Health Checks & Zero-Downtime Rollouts
Declare container healthcheck specifications to verify incoming replacement containers are responding before terminating previous instances:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/up"]
interval: 10s
timeout: 5s
retries: 3