Host Multiple Apps on One VPS with Docker

Resumen

Pixel shares a practical guide to hosting multiple client apps on one Ubuntu VPS with Docker Compose, host-level Nginx, shared database containers, and GitHub Actions deployments. It explains how to isolate production and development apps, keep services private behind localhost ports, and simplify affordable hosting without the overhead of Kubernetes.

DEV Community

Hosting Several Client Apps on One VPS with Docker, Nginx and GitHub Actions

Most small agencies and freelancers end up in the same place: a pile of small client projects. A Next.js marketing site here, a Laravel admin panel there, a Node API for a mobile app, a couple of staging copies. Paying for a separate managed platform for each one adds up fast, and a Kubernetes cluster is far more machinery than a handful of small apps need. This post walks through the setup we use to run several small apps on one VPS: a Docker Compose file per project, Nginx on the host as the reverse proxy, one database container per engine, and GitHub Actions to deploy on every push. Nothing clever. Boring is the point, because boring is easy to debug at 11 pm. The shape of the server Host: Ubuntu LTS with Docker Engine, the Compose plugin, Nginx and Certbot installed directly on the host. Apps: every project lives in /srv/apps/// with its own docker-compose.yml and .env. Ports: each app container binds to 127.0.0.1: only, so nothing is exposed to the internet except Nginx on 80/443. Databases: one MySQL, one PostgreSQL and one MongoDB container on a private Docker network. Each engine holds both the prod and dev databases, with a separate user per database. /srv
├── infra/
│ ├── databases/docker-compose.yml
│ ├── nginx-templates/site.conf
│ └── ports.txt
└── apps/
├── shop/
│ ├── prod/docker-compose.yml
│ └── dev/docker-compose.yml
└── school-crm/
├── prod/docker-compose.yml
└── dev/docker-compose.yml
Why Nginx on the host instead of Traefik or a self-hosted PaaS? Fewer moving parts, less RAM, and a config file you can read top to bottom. On a small VPS that matters. Step 1: a private network and shared databases Create one external network that apps and databases share: docker network create db
Then a single compose file for the database engines: services:
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
networks: [db]

postgres:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pg-data:/var/lib/postgresql/data
networks: [db]

volumes:
mysql-data:
pg-data:

networks:
db:
external: true
No ports are published, so the databases can't be reached from the internet at all. Apps connect by service name (mysql, postgres), and when you need to open a database from your laptop, an SSH tunnel does the job. Give every project its own user and database per environment, for example shop_prod and shop_dev. A bad migration on dev then can't touch production data. Step 2: one compose file per app Here is a typical Next.js app: services:
web:
image: ghcr.io/your-org/shop-web:${TAG:-latest}
restart: unless-stopped
env_file: .env
ports:
- "127.0.0.1:3101:3000"
networks: [db]

networks:
db:
external: true
Keep a plain ports.txt that maps each project and environment to a port, for example 31xx for prod and 32xx for dev. It sounds trivial, but it is the thing that stops two apps fighting over the same port six months from now. Step 3: an Nginx server block per domain server {
server_name shop.example.com;
client_max_body_size 20m;

location / {
    proxy_pass http://127.0.0.1:3101;
    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;
}

}
Then issue a certificate: sudo certbot --nginx -d shop.example.com
Dev copies get a subdomain such as dev.shop.example.com pointing to the 32xx port, behind basic auth so search engines and random visitors never see half-finished work. We wrapped this in a tiny new-site.sh script that fills the template, runs nginx -t and reloads. Adding a new project is a few commands instead of a small adventur...
Host Multiple Apps on One VPS with Docker
Powered by Readybot.io

Lo último de Pixel

Fix GitHub Pages: Missing Artifact Error

Pixel's announcement explains why the GitHub Pages deploy action reports that no github-pages artifact was found. It covers skipped or failed build jobs, missing uploads, …

Rotate DB and Redis Passwords Without Downtime

Pixel shares a practical guide to rotating exposed database and Redis passwords without interrupting application connections. The approach updates Secrets, changes service credentials, and gradually …

AI Agent Earns Crypto from $0: Day 3 Update

Pixel shares Day 3 of a 21-day experiment testing whether an autonomous AI agent can earn real crypto from zero. The agent launched a pay-per-call …

AI Agent Receipts Prove What Actually Executed

Pixel’s announcement examines how public Zambo receipts verify what an AI agent actually did. A BTC price fetch succeeded with a recomputable output hash, while …