Ephemeral Review App Containers per Pull Request with Docker
AI generated
FROM
RUN
Docker · Review Apps · CI/CD
Ephemeral review apps
one container stack per pull request

An ephemeral review app is automatically created as soon as a pull request is opened, and disappears without a trace once it is merged or closed. Docker Compose builds a dedicated, isolated stack per branch for this, reachable through a dynamic subdomain, with no manual environment management at all.

17 min read Docker Compose · dynamic subdomains · cleanup GitLab review apps · Traefik

1. What an ephemeral review app delivers

An ephemeral review app is a complete, isolated instance of the application that is automatically created for a single pull request and just as automatically disappears once that pull request is no longer relevant. The core idea: instead of sharing a staging environment where changes from different branches overwrite each other, every pull request gets its own, fully independent container stack with its own database, its own cache and its own reachable URL.

The benefit of an ephemeral review app shows up most clearly in the review process itself. A reviewer no longer has to just read code, they can click through the actual change in a running environment, test it, and share it with product designers or product owners without checking anything out locally. Especially for visual changes to a shop frontend or complex form workflows, a short test round in a real environment replaces hours of reading code.

Technically, an ephemeral review app is based in most setups on Docker Compose, which starts the complete application stack, web server, application, database and optionally cache, in its own Compose project per branch. The CI pipeline handles the entire lifecycle management: setup on every push to the pull request branch, updates on further commits, and a complete teardown when merged or closed.

2. Generating a Compose setup per pull request

For multiple ephemeral review apps to run simultaneously on the same CI runner or host, every Compose project has to be uniquely named and isolated. Docker Compose supports this through the -p parameter (project name), which serves as a prefix for container, network and volume names. If the project name is derived from the pull request number, for example review-pr-482, multiple review apps running in parallel never collide.

Ports must never be hardcoded in the Compose file when multiple ephemeral review apps run simultaneously on the same host. Instead, containers are bound either to randomly assigned host ports or, much cleaner, to a shared network where a reverse proxy routes by hostname instead of port. The latter scales much better once more than a handful of pull requests are open at the same time.


# docker-compose.review.yml -- template rendered per pull request
services:
  app:
    image: registry.example.com/myapp:${CI_COMMIT_SHA}
    environment:
      APP_ENV: review
      DATABASE_URL: mysql://app:app@db:3306/app
    networks: [review-network]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.pr-${PR_NUMBER}.rule=Host(`pr-${PR_NUMBER}.review.example.com`)"

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: app
    networks: [review-network]
    tmpfs:
      - /var/lib/mysql  # ephemeral storage -- no cleanup of data files needed

networks:
  review-network:
    name: review-pr-${PR_NUMBER}

3. Dynamic routing: one subdomain per branch

Reviewers should be able to reach an ephemeral review app with a single click, without knowing ports or IP addresses. A reverse proxy such as Traefik solves this elegantly through labels: every app container receives a Traefik label at startup with a routing rule based on a unique subdomain, usually derived from the pull request number, for example pr-482.review.example.com. Traefik automatically discovers new containers through its Docker provider integration and updates its routing without a restart.

For ephemeral review apps with HTTPS, a wildcard certificate for the review domain is recommended, for example *.review.example.com, instead of issuing a separate certificate for every pull request. Traefik can manage wildcard certificates automatically through the DNS-01 challenge mode of Let's Encrypt, which decouples the entire certificate process from the pull request lifetime.

4. Isolating databases without rebuilding fixtures for every PR

A dedicated, isolated database per ephemeral review app is mandatory, so that test data from different pull requests never mix. The obvious approach, starting a completely empty database container for every pull request and then restoring a full production backup, is too slow with larger data volumes to deliver a usable review app within a few minutes.

Preloading a compact, anonymized seed dataset into a Docker volume or image, which then serves as the starting point for every new ephemeral review app, works faster. Such a seed image is built once from a representative data extract and versioned in the registry, so that the actual startup of every review app only requires copying an already prepared database volume, instead of running migrations and fixtures live.


#!/usr/bin/env bash
# seed-review-db.sh -- fast database seeding for ephemeral review apps
set -euo pipefail

PR_NUMBER="${1:?Usage: seed-review-db.sh <pr-number>}"
VOLUME_NAME="review-pr-${PR_NUMBER}-db"

echo "[seed] Creating isolated volume for PR #${PR_NUMBER}"
docker volume create "${VOLUME_NAME}"

# Copy a pre-built, anonymized seed dataset into the fresh volume
docker run --rm \
  -v "${VOLUME_NAME}:/var/lib/mysql" \
  -v review-seed-data:/seed:ro \
  alpine sh -c "cp -a /seed/. /var/lib/mysql/"

echo "[seed] Volume ${VOLUME_NAME} ready in seconds, not minutes"

5. Automating setup and teardown in the CI pipeline

The entire lifecycle of an ephemeral review app has to be controlled through CI pipeline triggers, not manual intervention. GitLab CI offers the native environment concept with on_stop jobs for this, which run automatically as soon as a merge request is closed or merged. GitHub Actions achieves the same through a workflow that reacts to the closed event of a pull request.

What matters when automating ephemeral review apps is that the stop job runs reliably even if the corresponding deploy job previously failed. A review app stack that was half built and then never torn down, because the cleanup job was tied to a previously successful pipeline, accumulates into significant resource waste over the course of weeks.


# .gitlab-ci.yml -- ephemeral review app with automatic teardown
deploy-review:
  stage: deploy
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://pr-$CI_MERGE_REQUEST_IID.review.example.com
    on_stop: stop-review
  script:
    - export PR_NUMBER=$CI_MERGE_REQUEST_IID
    - envsubst < docker-compose.review.yml > compose.rendered.yml
    - docker compose -p review-pr-$PR_NUMBER -f compose.rendered.yml up -d
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

stop-review:
  stage: deploy
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  script:
    - docker compose -p review-pr-$CI_MERGE_REQUEST_IID down -v
  when: manual
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

6. Labels and metadata for reliable cleanup

Even with a well configured on_stop job, orphaned resources occasionally remain, for example when a runner crashes during cleanup. For ephemeral review apps, an additional, time triggered cleanup job is therefore recommended, one that identifies all containers and volumes with a specific label and automatically removes them once a maximum lifetime has elapsed, independent of the pipeline status of the original pull request.

A consistent label scheme is a prerequisite for this: every container of an ephemeral review app receives a label such as review-app=true at startup, together with the pull request number and the creation timestamp. A daily cron job then filters all affected resources via docker ps --filter "label=review-app=true" and removes those whose creation timestamp exceeds a defined threshold, for example 14 days.

7. Resource usage and limits with many parallel PRs

The more actively a team works, the more ephemeral review apps run simultaneously, and each of them occupies CPU, memory and disk space on the review host. Without limits, a single resource hungry pull request can slow down all other review apps on the same host. For every service in the Compose file, deploy.resources.limits should therefore be set, even though Compose only passes these on to the Docker engine as a recommendation outside of Swarm, not as a hard Kubernetes style guarantee.

In addition, an upper bound on the number of simultaneously running ephemeral review apps per host is recommended. Once a team reaches this limit, the pipeline can automatically stop older, inactive review apps before starting a new one, instead of overloading the host through unbounded growth. This limit can easily be implemented through a counter job that checks the number of running review app networks before every deploy.

8. Common mistakes with ephemeral review apps

The most common mistake when building ephemeral review apps is copying production adjacent secrets into the review environment, for example real payment provider credentials or real customer email configuration. Reviewers typically test with careless input, and a review app that accidentally sends real emails or triggers real payments causes tangible problems well outside the actual test environment.


# WRONG: production secrets leaking into a review app
docker compose -p review-pr-482 --env-file .env.production up -d

# RIGHT: dedicated, sandboxed secrets for every review app
docker compose -p review-pr-482 --env-file .env.review-sandbox up -d
# .env.review-sandbox points to test payment gateways, mail catchers, etc.

A second widespread mistake is not making the cleanup job robust against partial failures. If a docker compose down fails due to missing permissions, or the runner restarts during the process, an ephemeral review app stays active unnoticed. A regular, independent cleanup job based on labels and age catches exactly these partial failures, instead of relying solely on the success of the original stop job.

9. Review apps compared to shared staging environments

The following table contrasts shared staging environments with ephemeral review apps per pull request.

Aspect Shared staging Ephemeral review app Consequence
Isolation between branches None, changes overwrite each other Complete, one stack per PR No more mutual overwrites
Availability for review Queue with many PRs Immediately and parallel available Faster feedback during review
Lifetime Permanent, manually maintained Tied to the PR lifecycle No manual reset needed
Resource usage Constant, even when unused Only while the PR is active Lower ongoing cost
Setup effort Low, one time Higher, but automated once Pays off with a few parallel PRs

The extra effort of initially building ephemeral review apps pays off once a team regularly works with several pull requests open in parallel. The time saved through immediately available, conflict free test environments usually exceeds the automation effort already after the first few weeks of productive use.

Mironsoft

Review apps, container automation and deployment infrastructure

Setting up review apps per pull request?

We build ephemeral review app pipelines with Docker Compose, dynamic routing and reliable cleanup, so every pull request gets its own, instantly testable environment.

Compose templates

Setting up per-PR stacks with isolated networks and volumes

Dynamic routing

Setting up Traefik based subdomain routing including wildcard TLS

Cleanup automation

Implementing label based cleanup jobs against orphaned resources

10. Summary

An ephemeral review app per pull request solves the fundamental problem of shared staging environments: changes from different branches no longer overwrite each other, and every reviewer gets an instantly reachable, isolated test environment. Docker Compose with project based isolation, dynamic routing through Traefik and preloaded seed databases makes setup fast enough to be practical per pull request.

The decisive success factor lies in reliable teardown: without robust, label based cleanup automation, orphaned containers and volumes accumulate, tying up resources and causing costs. Anyone who consistently integrates the entire lifecycle, from setup on every push to guaranteed teardown when the pull request closes, into the CI pipeline gains one of the most effective levers for faster, conflict free code review.

Ephemeral review apps — the essentials at a glance

One stack per PR

Derive the Compose project name from the PR number so parallel review apps never collide.

Dynamic routing

Traefik labels and wildcard TLS make every review app reachable through its own subdomain.

Fast seeding

Preloaded, anonymized datasets instead of live migrations per review app.

Label based cleanup

A time triggered cleanup job as a safety net against failed on_stop jobs.

11. FAQ: Ephemeral Review App Containers per Pull Request

1What is an ephemeral review app?
An isolated instance per pull request that is created automatically and disappears when closed.
2How are multiple review apps isolated?
Through a unique Compose project name per pull request, usually based on the PR number.
3How is a review app reachable?
Through Traefik labels that automatically route a dedicated subdomain per pull request.
4How are databases filled quickly?
With a preloaded, anonymized seed dataset instead of live migrations.
5How is the lifecycle automated?
Through CI triggers for setup, updates, and an on_stop job for teardown.
6What if the cleanup job fails?
A time triggered, label based cleanup job as an additional safety net.
7Can production secrets be used?
No, always use dedicated, sandboxed credentials for review apps.
8How are resource limits set?
Through deploy.resources.limits per service, plus an upper bound on parallel review apps.
9Difference from shared staging?
Fully isolated and tied to the PR lifecycle, no mutual overwrites.
10Which tool for dynamic routing?
Traefik, thanks to automatic container discovery and label based routing.