Code review is better when you can see the changes running live. Reading a diff in GitHub, GitLab, or Bitbucket tells you what changed, but clicking through a live preview tells you whether it actually works. Preview environments give every pull request its own temporary deployment, complete with a unique URL, a fresh database, and the full application stack. Reviewers can test features, designers can verify UI changes, and product managers can sign off on behavior without ever pulling code locally.
This guide shows you how to build a preview environment workflow using Deploynix, where every pull request gets its own live site that is automatically cleaned up when the PR is merged or closed.
The Value of Preview Environments
Preview environments solve several problems that slow down development teams.
Faster code reviews. Reviewers can click a link instead of checking out a branch, installing dependencies, running migrations, and starting a development server. A preview URL in the PR description takes seconds to open.
Better QA coverage. Manual testing on a live server catches issues that automated tests miss: broken layouts, missing assets, incorrect redirects, and subtle interaction bugs that only surface in a real browser on a real server.
Stakeholder feedback. Product managers and designers do not have local development environments. Preview URLs let them review changes on their own schedule without needing developer assistance.
Reduced merge conflicts. When reviewers can quickly validate changes, PRs get approved and merged faster. Shorter-lived branches mean fewer merge conflicts.
Confidence in deployments. If a feature works correctly in a preview environment that mirrors production, you can be much more confident it will work in production too.
Architecture Overview
The preview environment workflow has four components:
- A Deploynix server dedicated to hosting preview sites
- A CI/CD pipeline (GitHub Actions, GitLab CI, or Bitbucket Pipelines) that triggers site creation on PR open
- The Deploynix API used to create and manage temporary sites programmatically
- A cleanup mechanism that removes preview sites when PRs are merged or closed
The Deploynix API uses Sanctum token authentication with granular scopes, giving you fine-grained control over what your CI/CD pipeline can do. You can create a token with only the permissions needed for site management, following the principle of least privilege.
Step 1: Provision a Preview Server
Start by provisioning a dedicated server for preview environments through the Deploynix dashboard. This server will host multiple sites simultaneously, one for each open pull request.
Choose a server size based on how many concurrent preview environments you expect. A mid-range server can comfortably host 10 to 20 preview sites for a typical Laravel application. If your team regularly has more open PRs than that, consider a larger server or multiple preview servers.
When selecting your cloud provider (DigitalOcean, Vultr, Hetzner, Linode, AWS, or a custom provider), choose the same provider and region as your production server to ensure similar network behavior and latency characteristics.
Provision the server as an App server type with the same PHP version and configuration as production. Install the same database engine as well. If production runs MySQL, your preview server should run MySQL too.
Step 2: Create a Deploynix API Token
In your Deploynix dashboard, create an API token with the scopes needed for site management. The Deploynix API uses Sanctum token authentication, and you can assign granular scopes to each token.
Your preview environment automation needs permissions to:
- Create new sites on the preview server
- Configure site settings (domain, branch, environment variables)
- Trigger deployments
- Delete sites when PRs are closed
Store this API token as a secret in your CI/CD platform. In GitHub Actions, add it as a repository secret named DEPLOYNIX_API_TOKEN.
Step 3: Set Up the CI/CD Pipeline
Here is how to configure GitHub Actions to create preview environments automatically. The same concepts apply to GitLab CI and Bitbucket Pipelines.
Creating a Preview on PR Open
Create a workflow file at .github/workflows/preview-deploy.yml:
name: Deploy Preview Environment
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
deploy-preview:
runs-on: ubuntu-latest
steps:
- name: Create or update preview site
env:
DEPLOYNIX_API_TOKEN: ${{ secrets.DEPLOYNIX_API_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BRANCH: ${{ github.head_ref }}
run: |
# Check if site already exists for this PR
SITE_EXISTS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $DEPLOYNIX_API_TOKEN" \
"https://deploynix.io/api/v1/servers/{server-id}/sites/pr-$PR_NUMBER")
if [ "$SITE_EXISTS" = "404" ]; then
# Create new site
curl -X POST \
-H "Authorization: Bearer $DEPLOYNIX_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"domain\": \"pr-$PR_NUMBER.preview.yourdomain.com\",
\"repository\": \"your-org/your-repo\",
\"branch\": \"$BRANCH\",
\"php_version\": \"8.4\"
}" \
"https://deploynix.io/api/v1/servers/{server-id}/sites"
else
# Trigger redeployment for updated code
curl -X POST \
-H "Authorization: Bearer $DEPLOYNIX_API_TOKEN" \
"https://deploynix.io/api/v1/servers/{server-id}/sites/pr-$PR_NUMBER/deploy"
fi
- name: Comment PR with preview URL
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.pull_request.number;
const previewUrl = `https://pr-${prNumber}.preview.yourdomain.com`;
// Check if we already commented
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const botComment = comments.data.find(c =>
c.body.includes('Preview Environment')
);
const body = `## Preview Environment\n\nYour preview is live at: ${previewUrl}\n\nThis environment will be automatically cleaned up when this PR is merged or closed.`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
Cleaning Up on PR Close
Create a second workflow at .github/workflows/preview-cleanup.yml:
name: Cleanup Preview Environment
on:
pull_request:
types: [closed]
jobs:
cleanup-preview:
runs-on: ubuntu-latest
steps:
- name: Delete preview site
env:
DEPLOYNIX_API_TOKEN: ${{ secrets.DEPLOYNIX_API_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
curl -X DELETE \
-H "Authorization: Bearer $DEPLOYNIX_API_TOKEN" \
"https://deploynix.io/api/v1/servers/{server-id}/sites/pr-$PR_NUMBER"
This runs when a PR is merged or closed, automatically removing the preview site and freeing up server resources.
Step 4: Configure DNS for Preview Subdomains
Preview environments need URLs. The cleanest approach is to use wildcard subdomains so any pr-*.preview.yourdomain.com resolves to your preview server.
Add a wildcard DNS A record:
*.preview.yourdomain.com → A → [preview-server-ip]
This single DNS record covers all current and future preview environments. You do not need to add new DNS records for each pull request.
For SSL, you will want a wildcard certificate covering *.preview.yourdomain.com. Deploynix supports SSL auto-provisioning through DNS providers including Cloudflare, DigitalOcean, AWS Route 53, and Vultr. Configure wildcard SSL for your preview domain to ensure all preview sites are served over HTTPS.
Alternatively, you can use Deploynix's vanity domain feature. Every Deploynix site can use a *.deploynix.cloud subdomain, giving you instant HTTPS-enabled URLs without any DNS configuration. This is perfect for getting started quickly.
Step 5: Environment Variables for Previews
Each preview site needs its own environment configuration. Some values will be shared across all previews, while others are unique per site.
Shared across all previews:
- Third-party API keys (use sandbox/test credentials)
- Mail configuration (use Mailtrap or similar)
- Cache and queue drivers
Unique per preview:
-
APP_URL(the preview site's URL) -
APP_KEY(generate a unique key for each preview) - Database name (use a dedicated database per preview, like
preview_pr_42)
When creating the site via the Deploynix API, pass the environment variables as part of the site creation request. For the database, your CI/CD pipeline should create a fresh database on the preview server and run migrations to set up the schema.
Step 6: Database Strategy for Previews
Each preview environment needs its own database to prevent data conflicts between simultaneous PRs. There are two approaches:
Fresh Database with Seeders
Create a new database for each preview and seed it with test data:
# In your deploy script
php artisan migrate --force
php artisan db:seed --force
This gives each preview a clean starting point with predictable data. It is the simplest approach and works well for most teams.
Snapshot from Staging
For features that require realistic data, restore a sanitized copy of your staging or production database into the preview's database. This takes more server resources but provides a more realistic testing experience.
Step 7: Automatic Redeployment on Push
The synchronize event type in the GitHub Actions workflow triggers a redeployment whenever new commits are pushed to the PR branch. This means reviewers always see the latest version of the code.
When a developer pushes a fix based on review feedback, the preview environment automatically updates. The reviewer gets a notification from GitHub and can immediately check the fix at the same preview URL.
Handling Common Challenges
Resource Management
Multiple preview environments consume server resources. Monitor your preview server's CPU, memory, and disk usage through the Deploynix monitoring dashboard. Set up health alerts to notify you if the preview server is running low on resources.
Implement a maximum age policy. If a PR has been open for more than two weeks without activity, automatically clean up its preview environment. This can be done with a scheduled GitHub Action that checks for stale PRs.
Database Cleanup
Each preview creates a database. Over time, forgotten databases from already-merged PRs can accumulate. Your cleanup workflow should drop the preview database when deleting the site:
mysql -e "DROP DATABASE IF EXISTS preview_pr_${PR_NUMBER};"
Secrets Management
Preview environments should never use production credentials for anything. Use sandbox or test credentials for all third-party services. Store these as shared environment variables on the preview server rather than passing them through CI/CD pipelines, reducing the risk of credential exposure.
Large Teams
For teams with many developers and dozens of simultaneous PRs, consider multiple preview servers behind a simple routing layer. You could also implement an on-demand approach where preview environments are only created when a specific label is applied to the PR, rather than for every PR automatically.
Using Vanity Domains for Quick Setup
If configuring custom DNS and wildcard certificates feels like too much overhead to get started, Deploynix's vanity domain feature offers an immediate alternative. Every site on Deploynix can be assigned a *.deploynix.cloud subdomain with automatic SSL.
Name your preview sites something like pr-42-yourapp.deploynix.cloud. No DNS configuration needed, no SSL provisioning needed. This is the fastest path to getting preview environments up and running, and you can always switch to custom domains later.
Conclusion
Preview environments transform the code review experience from reading diffs to interacting with live applications. They catch bugs that automated tests miss, speed up the review cycle, and give non-technical stakeholders a way to participate in the development process.
With Deploynix's API, zero-downtime deployments, vanity domains, and flexible server provisioning, you can build a preview environment pipeline that creates a fresh, production-like environment for every pull request and cleans it up automatically when the PR is done. Start simple with vanity domains and seeded databases, then evolve toward custom domains and production data snapshots as your team's needs grow. The investment in developer experience pays dividends in code quality and shipping speed.
Top comments (0)