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 nginxPoint 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/.envThe 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.