Your Staging Server Is a Directory

A lot of small teams skip staging because they assume it means duplicating their infrastructure: another VPS, another database server, another line on the monthly bill. So they test locally, deploy straight to production, and find out about problems around the same time their users do.

On a typical VPS running Nginx, though, staging doesn’t need any of that. It’s a second directory, a second database, and a second vhost, which is about fifteen minutes of setup for a place to verify deploys before they reach production.

Setting it up

You already have your production app at /var/www/myapp/. Create a staging directory next to it:

/var/www/
    myapp/           # Production
    myapp-staging/   # Staging

Create a staging database:

CREATE DATABASE myapp_staging CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON myapp_staging.* TO 'myapp_staging'@'localhost';
FLUSH PRIVILEGES;

Add an Nginx vhost at /etc/nginx/sites-available/myapp-staging:

server {
    listen 443 ssl;
    http2 on;
    server_name staging.myapp.example.com;

    ssl_certificate /etc/letsencrypt/live/staging.myapp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/staging.myapp.example.com/privkey.pem;

    root /var/www/myapp-staging/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Enable it, request an SSL certificate, and reload Nginx:

sudo ln -s /etc/nginx/sites-available/myapp-staging /etc/nginx/sites-enabled/
sudo certbot --nginx -d staging.myapp.example.com
sudo nginx -t && sudo systemctl reload nginx

Point staging.myapp.example.com at your server’s IP in DNS, and the infrastructure side is done.

The deploy workflow

Keep a .env.staging file on your workstation with the same keys as production but different values: the staging database, APP_DEBUG=true, and MAIL_DRIVER=log so you don’t accidentally email real users.

Deploy to staging the same way you deploy to production. If you use an rsync script, make a copy that points at the staging directory:

rsync -avz --delete --exclude-from="rsync-excludes.txt" ./ myserver:/var/www/myapp-staging/
rsync .env.staging myserver:/var/www/myapp-staging/.env

The routine is to deploy to staging, test in the browser, and then deploy to production with the same script pointed at a different target. A side benefit is that the deploy process itself gets exercised twice.

Keeping production data out

The staging database must never touch production data. Use a sanitized copy or seed data instead. If you do import a production dump for more realistic testing, strip the email addresses and passwords first. Get this wrong and staging can send a real user a password reset email, or do something worse than that.

Setting MAIL_DRIVER=log in the staging .env acts as a safety net: even if the application code tries to send email, it writes to a log file instead.

When to move to a separate server

Staging on the same box won’t catch problems that only show up on different hardware, a different kernel, or under production load. If your server has 4 GB of RAM and production already uses 3 GB, staging is competing for the remaining 1 GB. For most small PHP applications that doesn’t matter much in practice, because the staging deployment only ever sees one user, you, working through a checklist rather than hundreds of concurrent requests.

If you do outgrow it, the signs are usually clear: staging starts feeling slow, or you hit a bug that only reproduces under load. At that point a separate server is worth it. Until then, a directory and a vhost give you most of what a separate staging environment would: code tested in a real server environment before users see it.

I wrote a book about this. Own Your Stack: PHP for Small Teams covers staging environments, deployment scripts, and the full workflow from local development to production for small teams.