CloudPloy

Staging Environments for PHP, Laravel, and WordPress on CloudPloy

A staging environment is a copy of your production app running on a separate URL where you can test changes before they go live. Without one, every deployment is a gamble - a schema migration that drops a column, a plugin update that breaks checkout, a config change that kills the mail queue. This guide covers how CloudPloy staging deployments work and how to set up a reliable staging-to-production workflow for PHP, Laravel, and WordPress applications.

Why Staging Matters

The case for staging is simple: bugs found in staging cost nothing. Bugs found in production cost customers, revenue, and trust. Specific risks that staging catches before they hit production:

Risk How Staging Catches It If Missed
Database migration with breaking changes Run migration on staging database first Production schema corruption, 500 errors
Plugin/package update incompatibility Update on staging and test before production White screen of death, checkout failures
Environment variable misconfiguration Staging has separate env vars with similar structure App boots but silently uses wrong API keys
PHP version incompatibility Test on new PHP version before switching production Deprecated function calls crash production
N+1 queries under real data Test with a production data clone Performance degrades after deployment
Third-party API changes Test against sandbox APIs in staging Payment failures, webhook delivery errors

Part 1: Creating a Staging App on CloudPloy

The simplest staging setup on CloudPloy is a second application deployment pointing to the same repository but a different branch. Each app runs in its own container with its own environment variables, database, and domain.

Step 1: Create the Staging App

In your CloudPloy dashboard:

  1. Go to Apps > New Application
  2. Connect the same repository as your production app
  3. Set the branch to staging (or develop - whatever your pre-production branch is)
  4. Give it a name like myapp-staging
  5. Configure a staging subdomain: staging.yourdomain.com

CloudPloy provisions a separate container with its own SSL certificate and isolated storage. The staging app is independent - you can break it completely without affecting production.

Step 2: Configure Staging Environment Variables

Staging needs its own environment variables in App > Settings > Environment Variables. Key differences from production:

Variable Production Value Staging Value Reason
APP_ENV production staging Enables debug features, disables production-only code paths
APP_DEBUG false true Show error details on staging, never on production
APP_URL https://yourdomain.com https://staging.yourdomain.com Correct URL for generated links, emails, OAuth callbacks
DB_DATABASE myapp_prod myapp_staging Separate database so staging changes never affect production data
MAIL_MAILER smtp (real emails) log or Mailtrap Prevent staging from sending real emails to real users
STRIPE_KEY Live key Test key Use payment sandbox to avoid charging real cards
QUEUE_CONNECTION redis sync Run jobs synchronously on staging to simplify debugging

Step 3: Password-Protect Staging

Staging should not be publicly accessible - it may contain unreleased features, test data, or incomplete UI. Add HTTP basic auth in your Nginx configuration or via a middleware:

# Laravel: Add to app/Http/Middleware/StagingAuth.php
public function handle(Request $request, Closure $next): Response
{
    if (app()->environment('staging') && !$this->isAuthenticated($request)) {
        return response('Unauthorized', 401, [
            'WWW-Authenticate' => 'Basic realm="Staging"',
        ]);
    }
    return $next($request);
}

private function isAuthenticated(Request $request): bool
{
    return $request->getUser() === env('STAGING_USER') &&
           $request->getPassword() === env('STAGING_PASSWORD');
}

Add STAGING_USER and STAGING_PASSWORD to your staging environment variables. For WordPress, use a plugin like "Password Protected" or configure Nginx basic auth at the server level.

Part 2: Database Cloning for Realistic Testing

Testing against an empty staging database misses bugs that only appear with real data - missing indexes that become slow at scale, queries that rely on specific data patterns, or UI that breaks with edge-case content. Cloning production data to staging makes tests meaningful.

Cloning a Laravel/MySQL Database

# On your production server (from CloudPloy terminal):
# 1. Export production database (anonymize sensitive data)
mysqldump \
  --single-transaction \
  --set-gtid-purged=OFF \
  myapp_prod \
  | gzip > /tmp/prod_backup.sql.gz

# 2. Transfer to staging server
scp /tmp/prod_backup.sql.gz staging-server:/tmp/

# On staging server:
# 3. Import to staging database
gunzip -c /tmp/prod_backup.sql.gz | mysql myapp_staging

# 4. Run any pending migrations on staging
php artisan migrate

# 5. Anonymize PII (never keep real user data on staging)
php artisan db:seed --class=AnonymizeUsersSeeder

Anonymizing User Data

Before using production data on staging, scrub personally identifiable information. Create a seeder that replaces real data with fake values:

// database/seeders/AnonymizeUsersSeeder.php
public function run(): void
{
    DB::table('users')->orderBy('id')->chunk(500, function ($users) {
        foreach ($users as $user) {
            DB::table('users')
                ->where('id', $user->id)
                ->update([
                    'name'  => fake()->name(),
                    'email' => "user{$user->id}@staging-test.invalid",
                    'phone' => fake()->phoneNumber(),
                ]);
        }
    });
}

WordPress Database Clone

# Export and import with search-replace for the domain change
wp db export --add-drop-table /tmp/prod.sql

# On staging (after import):
wp db import /tmp/prod.sql
wp search-replace 'https://yourdomain.com' 'https://staging.yourdomain.com' --all-tables

# Flush cache after domain change
wp cache flush
wp rewrite flush

The search-replace command updates serialized data safely - important for WordPress where options like theme settings store full URLs in serialized arrays.

Part 3: Branch Deployments

For teams, a single staging branch is often insufficient - multiple features in development simultaneously means the staging branch becomes a merge queue where features block each other. CloudPloy supports deploying any branch as a separate app for per-feature staging environments.

Per-Feature Branch Workflow

  1. Developer creates a feature branch: git checkout -b feature/checkout-redesign
  2. Opens a PR on GitHub - CloudPloy detects the PR and can deploy it automatically
  3. Feature is tested on its own isolated URL before merge
  4. PR is merged to staging for integration testing
  5. After QA approval, staging is merged to main and deployed to production

Setting Up Automatic Branch Deployments

Configure CloudPloy to deploy pull requests automatically in App > Settings > Git Deployments. Each PR gets a unique preview URL following the pattern feature-branch-name-appname.yourdomain.com. These environments are spun up on PR open and torn down on PR merge.

GitHub Actions Integration

Trigger CloudPloy deployments from GitHub Actions for tighter CI/CD control:

# .github/workflows/staging-deploy.yml
name: Deploy to Staging

on:
  push:
    branches: [staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run tests
        run: |
          composer install --no-dev
          php artisan test --testsuite=Feature

      - name: Deploy to CloudPloy staging
        uses: ploy-cloud/deploy-action@v1
        with:
          app-id: ${{ secrets.CLOUDPLOY_STAGING_APP_ID }}
          api-key: ${{ secrets.CLOUDPLOY_API_KEY }}
          branch: staging

Part 4: Testing in Staging

Smoke Tests After Every Deployment

A smoke test verifies that the most critical user paths work after deployment. It catches configuration errors (wrong database URL, missing environment variable) that unit tests miss because they test the live environment, not isolated code.

Create a basic smoke test with Laravel's built-in HTTP tests:

// tests/Feature/SmokeTest.php
class SmokeTest extends TestCase
{
    public function test_homepage_loads(): void
    {
        $response = $this->get('/');
        $response->assertStatus(200);
    }

    public function test_login_page_loads(): void
    {
        $response = $this->get('/login');
        $response->assertStatus(200);
    }

    public function test_database_connectivity(): void
    {
        $response = $this->get('/health');
        $response->assertStatus(200)
                 ->assertJsonPath('checks.database', 'ok');
    }

    public function test_can_register_user(): void
    {
        $response = $this->post('/register', [
            'name'                  => 'Test User',
            'email'                 => 'smoke-test-' . time() . '@example.com',
            'password'              => 'SecurePass123!',
            'password_confirmation' => 'SecurePass123!',
        ]);
        $response->assertRedirect();
    }
}

Database Migration Testing

Before deploying a migration to production, always run it on the staging database clone first. Check that:

  • The migration completes without errors
  • Existing data is preserved (or transformed as expected)
  • Query performance is acceptable with real data volume
  • The rollback (down() method) also works correctly
# Test migration on staging
php artisan migrate --pretend    # Show SQL without executing
php artisan migrate              # Execute on staging database
php artisan migrate:rollback     # Test rollback
php artisan migrate              # Re-apply after confirming rollback works

WordPress Plugin Testing Protocol

WordPress plugins are the most common source of production incidents. Follow this protocol when testing plugin updates on staging:

  1. Update plugin on staging only
  2. Test all critical user paths: login, checkout, form submission, key admin pages
  3. Check PHP error log for new warnings or notices
  4. Test with different user roles (admin, editor, customer, subscriber)
  5. Check with browser console open for JavaScript errors
  6. If all passes: update production and immediately monitor for 30 minutes

Part 5: Promoting Staging to Production

Pre-Deployment Checklist

Before promoting staging changes to production:

Check How to Verify
All smoke tests pass on staging Run test suite against staging URL
No new PHP errors in staging logs Check App > Logs for ERROR or WARNING entries
Database migrations tested on staging data Confirmed in staging deployment
Environment-specific config not hardcoded No staging URLs or test keys in committed code
Rollback plan defined Identify what to revert if production deployment fails

Zero-Downtime Deployment

CloudPloy uses a blue-green deployment strategy for production. When you push to your production branch:

  1. New container is built from your Dockerfile
  2. New container starts and health check is run
  3. Once healthy, traffic switches atomically to the new container
  4. Old container drains existing connections for 30 seconds then stops

If the health check fails, the deployment is aborted and the old container continues serving traffic. Your production app never goes down due to a failed deployment.

Rolling Back

In App > Deployments, every past deployment is listed. Click any previous deployment and select "Redeploy" to roll back instantly. CloudPloy rebuilds the container from the exact same commit, no git revert needed. The rollback is as fast as a normal deployment.

Frequently Asked Questions

How much does staging cost on CloudPloy?

Staging apps count as a separate application. If your plan includes multiple applications, staging uses one of those slots. For resource sizing, staging typically needs less CPU and memory than production - a staging environment for a moderate-traffic site can often run on a 1GB or 2GB container. You can also scale staging down to zero outside of working hours to reduce costs.

Can staging share the same database as production?

No. Never share a database between staging and production. Staging schema changes and test data writes will corrupt production data. Always use a separate staging database, even if it means more maintenance work keeping the schema in sync.

How do I handle staging in multi-tenant or SaaS apps?

Set up a staging tenant or test organization that mirrors a production tenant's configuration. Use factories and seeders to create realistic multi-tenant test data. Avoid using real production tenants' data on staging unless you have explicit permission and a robust anonymization process.

How do I keep staging and production environment variables in sync?

Maintain a .env.staging.example file in your repository (with placeholder values, never real credentials) that mirrors the structure of production env vars. When a new environment variable is added for a feature, update the example file and the staging environment simultaneously. This prevents the common bug where staging works but production fails because a required env var was never set.

Should WooCommerce staging use test orders?

Yes. Set your payment gateway to test/sandbox mode on staging using the test API key. Configure Stripe's webhook endpoint to point to your staging URL for testing payment webhooks. Never test with real customer payment methods or process real transactions on staging.


Ready to set up your staging environment? Start with the GitHub Actions deployment guide for CI/CD integration, or contact CloudPloy support for help configuring staging for your specific setup.