Every production deployment carries risk. A staging environment — a full copy of your stack that you can test against before pushing to production — reduces that risk dramatically. But dedicated staging servers are expensive, and most single-VPS setups cannot justify the cost. The solution is to run staging alongside production on the same VPS using Docker Compose, with careful isolation through separate Compose projects, distinct ports, and reverse-proxy routing. This guide walks through the entire setup. If you are still choosing a host, our VPS comparison table helps you find one with enough RAM to run both environments comfortably.
Why Docker Compose Instead of Separate VPSes
A second VPS for staging doubles your costs — not just the monthly bill, but also the maintenance overhead of patching, monitoring, and securing two servers. Docker Compose lets you define your entire stack (web server, application server, database, cache, queue) in a single docker-compose.yml file and run multiple isolated instances of that stack on the same host. Each instance gets its own network namespace, volumes, and environment variables. The production instance listens on ports 80/443; the staging instance listens on a high port like 8080, or you route it through a subdomain via a reverse proxy.
Prerequisites
- A VPS with at least 2 GB RAM (4 GB recommended for most stacks). Check our memory and specs comparison to find a suitable plan.
- Docker and Docker Compose installed. If you are on Ubuntu 22.04+, install with:
sudo apt install docker.io docker-compose-v2. - A domain or subdomain for staging (e.g.,
staging.yourdomain.com) or a separate port number. - Git access to your project repository.
Step 1: Structure Your Project for Two Environments
The key to running multiple environments on one host is Docker Compose’s project name feature. When you run docker compose -p staging up -d, Docker prefixes all container names, networks, and volumes with staging_, keeping them completely separate from the production_ prefix. Your project directory should look like this:
/opt/
├── production/
│ ├── docker-compose.yml
│ ├── .env
│ └── nginx/
└── staging/
├── docker-compose.yml
├── .env
└── nginx/
Each docker-compose.yml defines the same services but with different port mappings, environment variables, and volume mounts. The .env file in each directory sets the differences: database names, API keys, and domain names.
Step 2: Docker Compose Configuration for Staging
Here is a minimal docker-compose.yml for a typical LEMP stack staging environment:
version: "3.9"
services:
db:
image: mysql:8.0
container_name: staging_db
volumes:
- staging_db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${STAGING_DB_ROOT_PASS}
MYSQL_DATABASE: ${STAGING_DB_NAME}
ports:
- "127.0.0.1:3307:3306"
restart: unless-stopped
app:
image: your-app:staging
container_name: staging_app
depends_on:
- db
environment:
DB_HOST: db
DB_NAME: ${STAGING_DB_NAME}
DB_USER: ${STAGING_DB_USER}
DB_PASS: ${STAGING_DB_PASS}
APP_ENV: staging
volumes:
- ./app:/var/www/html
restart: unless-stopped
web:
image: nginx:alpine
container_name: staging_web
depends_on:
- app
volumes:
- ./nginx/staging.conf:/etc/nginx/conf.d/default.conf
- ./app:/var/www/html
ports:
- "8080:80"
restart: unless-stopped
volumes:
staging_db_data:
Key differences from production: the database port is 3307 (not 3306), the web port is 8080 (not 80), and the container names are prefixed with staging_. The MySQL port is bound to 127.0.0.1 only, so it is not accessible from outside the VPS.
Step 3: Use a Reverse Proxy for Domain-Based Routing
Instead of remembering port numbers, route staging traffic through a subdomain. Install Nginx on the host (not in Docker) and add a virtual host configuration:
server {
listen 80;
server_name staging.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8080;
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 run certbot to get a Let’s Encrypt certificate for the staging subdomain: sudo certbot --nginx -d staging.yourdomain.com. This gives you HTTPS on staging just like production, which is important for testing features that depend on secure contexts (service workers, payment flows, OAuth redirects).
Step 4: Data Isolation and Seed Data
Staging should use a separate database with either anonymized production data or synthetic test data. Never point staging at your production database — a staging bug could corrupt real data. Script the database seeding process:
#!/bin/bash
# scripts/seed-staging.sh
docker compose -p staging exec -T db mysql -u root -p"$STAGING_DB_ROOT_PASS" $STAGING_DB_NAME < ./backups/anonymized_prod.sql
Run this script after each staging deployment to refresh the data. For application file storage (user uploads, media), either use a separate volume or configure your application to use a staging-specific storage backend.
Step 5: Deployment Workflow
With this setup, your deployment workflow becomes:
- Push code to a staging branch in your git repository.
- SSH into the VPS and pull the staging branch.
- Rebuild the staging image:
docker compose -p staging build. - Restart:
docker compose -p staging up -d. - Run migration and seed scripts against the staging database.
- Test at
https://staging.yourdomain.com. - If everything passes, merge to main and deploy to production:
docker compose -p production up -d.
This workflow catches environment-specific bugs (wrong config values, missing dependencies, database migration conflicts) that unit tests running in CI cannot detect. The cost is zero extra VPS spend — just the disk space for the staging volumes and the RAM for the extra containers.
Step 6: Resource Limits and Monitoring
On a single VPS, staging containers must not starve production. Set resource limits in each Compose file:
services:
app:
deploy:
resources:
limits:
cpus: "0.5"
memory: "512M"
Monitor resource usage with docker stats and set up alerts for when the staging environment approaches your VPS’s memory ceiling. If you are consistently above 80% RAM usage with both environments running, consider upgrading to a plan with more memory — our memory and specs comparison can help you identify the right tier.
Conclusion
A staging environment on a single VPS is not a compromise — it is a pragmatic solution for teams and solo developers who need production-like testing without a second server bill. Docker Compose’s project isolation makes it safe, reverse-proxy routing makes it accessible, and resource limits keep it from interfering with production. The next time you deploy without testing on staging first, remember: the cost of a staging environment is near zero; the cost of a production outage is not. Compare VPS plans to find one with enough headroom for both environments.




Leave a Reply
You must be logged in to post a comment.