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.
Table of Contents
- 1. What an ephemeral review app delivers
- 2. Generating a Compose setup per pull request
- 3. Dynamic routing: one subdomain per branch
- 4. Isolating databases without rebuilding fixtures for every PR
- 5. Automating setup and teardown in the CI pipeline
- 6. Labels and metadata for reliable cleanup
- 7. Resource usage and limits with many parallel PRs
- 8. Common mistakes with ephemeral review apps
- 9. Review apps compared to shared staging environments
- 10. Summary
- 11. FAQ
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.