Back to articles
Technology Insight

Building Ephemeral Previews for Pull Requests on a VPS with Webhooks, Docker, and Caddy

May 26, 2026

Introduction: The Power of Preview Environments

In modern software development, code reviews are essential for maintaining quality. However, reviewing raw code changes in a Pull Request (PR) often falls short of capturing the full picture. Static analysis and unit tests cannot fully replicate how features behave in a live, integrated environment. Traditionally, teams relied on a shared staging server, but this frequently creates a bottleneck when multiple developers compete to test their features simultaneously.

Enter Ephemeral Previews—isolated, dynamic environments created on-demand for every open Pull Request and automatically destroyed once the PR is closed or merged. While managed Platforms as a Service (PaaS) offer this out of the box, costs can escalate rapidly. This comprehensive guide demonstrates how to build a robust, cost-effective ephemeral preview infrastructure on your own Virtual Private Server (VPS) leveraging the combined power of GitHub/GitLab Webhooks, Docker, and Caddy Router.

The Architecture Overview

Before diving into the implementation details, it is crucial to understand how these components interact to form a seamless automation pipeline. The entire workflow operates in a self-sustaining loop:

  • The Trigger: A developer opens or updates a Pull Request on a Git platform (e.g., GitHub or GitLab).
  • The Messenger: A configured Webhook captures this event and sends a secure HTTP POST payload to a lightweight webhook listener running on your VPS.
  • The Executor: The listener processes the payload, clones or pulls the specific branch, and spins up an isolated application instance using Docker Compose.
  • The Router: Caddy dynamically detects the new container, provisions a Let's Encrypt SSL certificate, and routes a unique subdomain (e.g., pr-123.preview.yourdomain.com) directly to the container.
  • The Cleanup: When the PR is merged or closed, another webhook event triggers a cleanup script that stops the container, deletes the associated Docker volumes, and frees up VPS resources.

Step 1: Preparing the VPS Environment

To begin, ensure your VPS is running a modern Linux distribution (such as Ubuntu 24.04 LTS) with a public IP address. You will also need a wildcard DNS record pointing to your server. For instance, configure an A record for *.preview.yourdomain.com to resolve to your VPS IP address.

Installing Essential Dependencies

Log into your server via SSH and install Docker, Docker Compose, and Caddy. Ensure your user belongs to the Docker group to execute commands without sudo prefixing:

sudo apt update && sudo apt install -y curl git debian-keyring debian-archive-keyring apt-transport-https
# Install Docker following official Docker engine documentation
sudo usermod -aG docker $USER

Step 2: Configuring Caddy for Dynamic Reverse Proxying

Caddy is an exceptionally powerful, modern web server that handles automatic SSL generation natively. To route traffic dynamically to our ephemeral Docker containers without manually restarting the web server every time a PR is opened, we leverage Caddy's On-Demand TLS capability or its internal API.

Create a Caddyfile that listens to wildcard subdomains and proxies requests based on the PR number extracted from the hostname:

*.preview.yourdomain.com {
    # Enable automatic TLS
    tls internal 

    # Reverse proxy to the dynamically named Docker containers
    reverse_proxy http://localhost:{labels.3}
}

Alternatively, a highly scalable approach is utilizing Caddy's Docker Proxy plugin, which reads labels directly from running Docker containers and configures routing automatically on the fly. This eliminates any hardcoded port mapping complexities.

Step 3: Creating the Webhook Listener Daemon

The core orchestrator of this setup is a lightweight webhook receiver daemon. You can write this easily using Node.js, Go, or Python. Its primary responsibility is verifying payload signatures from GitHub/GitLab for security, parsing the action (opened, synchronized, closed), and executing predefined shell scripts.

Sample Script for PR Deployment (deploy.sh)

When an opened or synchronize event occurs, the daemon invokes a deployment script similar to the following:

#!/bin/bash
PR_NUMBER=$1
BRANCH_NAME=$2
REPO_URL=$3

TARGET_DIR="/opt/previews/pr-$PR_NUMBER"

if [ ! -d "$TARGET_DIR" ]; then
    git clone --depth 1 --branch "$BRANCH_NAME" "$REPO_URL" "$TARGET_DIR"
else
    cd "$TARGET_DIR" && git fetch origin "$BRANCH_NAME" && git reset --hard origin/"$BRANCH_NAME"
fi

cd "$TARGET_DIR"

# Run docker-compose with a unique project name
docker compose -p "pr-$PR_NUMBER" up -d --build

Using the -p or --project-name flag in Docker Compose ensures that containers, networks, and volumes are isolated and uniquely identifiable under the namespace of that specific Pull Request.

Step 4: Automating Teardown and Resource Management

Unused preview environments can rapidly exhaust your server's CPU, RAM, and disk space. Implementing a strict teardown strategy is mandatory for long-term viability. When a PR event returns closed, your webhook server must trigger a cleanup script:

#!/bin/bash
PR_NUMBER=$1
TARGET_DIR="/opt/previews/pr-$PR_NUMBER"

if [ -d "$TARGET_DIR" ]; then
    cd "$TARGET_DIR"
    docker compose -p "pr-$PR_NUMBER" down -v
    cd /opt/previews
    rm -rf "$TARGET_DIR"
fi

# Prune unused docker systems to reclaim disk space
docker system prune -f --volumes
Security Best Practice: Always validate the X-Hub-Signature-256 header using a shared secret key configured in your GitHub/GitLab repository settings. This ensures your server never executes arbitrary commands from unauthorized web requests.

Optimizing the Setup for Production

To scale this self-hosted preview infrastructure efficiently across a development team, consider implementing the following optimizations:

  1. Docker Layer Caching: Use external caching mechanisms or shared volume targets for package managers (like node_modules or Maven caches) to keep container build times under two minutes.
  2. Resource Constraints: Limit CPU and memory usage per PR container within your docker-compose.yml file using the deploy.resources.limits block to prevent a single buggy PR from crashing the entire VPS host.
  3. Automated Pruning Cronjobs: Setup a daily cronjob that automatically terminates any preview environment older than 7 days, guarding against missed webhook delivery events.

Conclusion

By leveraging open-source tools like Docker and Caddy along with Git webhooks, you can build a robust, production-grade Ephemeral Previews engine directly on a budget-friendly VPS. This architectural pattern democratizes advanced CI/CD capabilities, enabling engineering teams to accelerate code review feedback loops, improve product quality, and significantly reduce infrastructure overhead. Implement this setup today to experience enterprise-level preview automation tailored entirely to your workflow.

Building Ephemeral Previews for Pull Requests on a VPS with Webhooks, Docker, and Caddy | DPTCloud