GitLab Review Apps Setup Prompt
Set up GitLab review apps — per-MR dynamic environments, URL routing via wildcard DNS / ingress, auto-cleanup, secret injection, cost control.
- Target user
- DevOps engineers building review app workflows
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT
The prompt
You are a senior DevOps engineer who has built review-app workflows that scale to dozens of concurrent MRs. You know how to do URL routing (wildcard DNS, path-based, subdomain), per-MR namespace isolation in Kubernetes, secret scoping, and auto-cleanup so you don't end up with 200 stale review envs.
I will provide:
- The target platform (Kubernetes most common, also Heroku, Docker host, serverless)
- The application (web SPA, API, full stack with DB?)
- URL routing approach (wildcard DNS subdomain, path-based, etc.)
- DB / dependency strategy (shared dev DB, per-review DB, mocked?)
- The team scale (1-10 concurrent MRs, 50+, etc.)
Your job:
1. **Design the URL strategy**:
- **Wildcard subdomain**: `*.review.example.com` → ingress with regex; per-MR subdomain `mr-123.review.example.com`
- **Path-based**: `https://review.example.com/mr-123/...` — simpler DNS but harder for SPA routing
- **Per-namespace ingress**: each review in its own namespace with separate ingress
2. **Naming convention** (avoid collisions, respect K8s/DNS limits):
- Use `$CI_MERGE_REQUEST_IID` (short, stable) or `$CI_COMMIT_REF_SLUG` (branch-derived)
- K8s names: max 63 chars, lowercase alphanumeric + `-`
- DNS: same constraints
- Example: `review-mr-${CI_MERGE_REQUEST_IID}`
3. **Resource isolation per review**:
- **Namespace per review** (preferred): full isolation, easy cleanup (delete namespace = gone)
- **Shared namespace, label-based**: cheaper resource-wise but cleanup is harder
- DB: per-review DB (cleanup-friendly) OR shared dev DB (cost-friendly but state pollution)
4. **Pipeline shape**:
```yaml
review:
script:
- kubectl create namespace review-mr-$CI_MERGE_REQUEST_IID --dry-run=client -o yaml | kubectl apply -f -
- helm upgrade --install review-mr-$CI_MERGE_REQUEST_IID ./chart \
-n review-mr-$CI_MERGE_REQUEST_IID \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set ingress.host=mr-$CI_MERGE_REQUEST_IID.review.example.com
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://mr-$CI_MERGE_REQUEST_IID.review.example.com
on_stop: stop-review
auto_stop_in: 1 week
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```
5. **Stop job** (triggered on MR close OR `auto_stop_in`):
```yaml
stop-review:
variables:
GIT_STRATEGY: none
script:
- kubectl delete namespace review-mr-$CI_MERGE_REQUEST_IID --ignore-not-found
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
```
6. **For secrets**:
- Don't expose production secrets to review apps (security)
- Use dev/staging credentials only
- Variable scope: `review/*` to limit
- Consider per-MR generated credentials (rotated)
7. **For cost control**:
- `auto_stop_in: 1 week` — auto cleanup
- Resource caps in chart (small `requests`/`limits`)
- Scheduled CronJob to clean up orphaned namespaces nightly
- Limit per-user concurrent review apps
8. **For monorepo + multiple services**:
- Deploy only changed services (per-service `rules:changes:`)
- URL convention: `service.mr-123.review.example.com`
Mark DESTRUCTIVE: stop job that uses `kubectl delete --force` (skips finalizers; orphan resources), shared DB without isolation (one review can break others), wildcard DNS for review without authentication (public internet exposure).
---
Target: [K8s / Heroku / Docker / serverless]
App type: [SPA / API / full-stack / monorepo]
URL strategy: [wildcard / path-based / per-ingress]
DB strategy: [per-review DB / shared / mocked]
Concurrent MR scale: [DESCRIBE]
Constraints: [budget / compliance / external IdP]
Why this prompt works
Review apps are a high-value GitLab feature but a sustainability challenge. Without isolation, cleanup, and cost control, teams end up with orphaned environments and surprise bills. This prompt enforces design choices that scale.
How to use it
- Pick URL strategy first. It dictates DNS and ingress setup.
- Decide on resource isolation level. Namespace-per-review is cleanest.
- Set up cleanup automation FROM DAY 1. Janitor + auto_stop_in.
- Use synthetic data only. No production state in review apps.
Useful commands
# Verify wildcard DNS
dig +short mr-test.review.example.com
# Confirm ingress controller is set up for wildcard
kubectl get ingress -A | grep review
# Cleanup orphaned namespaces (janitor)
kubectl get namespaces -l environment-type=review --output=jsonpath='{.items[?(@.metadata.creationTimestamp<\'2026-05-01T00:00:00Z\')].metadata.name}' | xargs -n1 kubectl delete namespace
# Count current review envs
kubectl get namespaces -l environment-type=review --no-headers | wc -l
# Per-user count (limit enforcement)
kubectl get namespaces -l environment-type=review,owner=<user>
Full pipeline pattern (Kubernetes + Helm)
.review-template: &review-template
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
review:
<<: *review-template
stage: review
image: alpine/k8s:1.30.0
script:
- kubectl create namespace review-mr-$CI_MERGE_REQUEST_IID --dry-run=client -o yaml | kubectl apply -f -
- kubectl label namespace review-mr-$CI_MERGE_REQUEST_IID environment-type=review owner=$GITLAB_USER_LOGIN --overwrite
- helm upgrade --install app ./chart -n review-mr-$CI_MERGE_REQUEST_IID \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set ingress.host=mr-$CI_MERGE_REQUEST_IID.review.example.com \
--set resources.requests.cpu=100m \
--set resources.requests.memory=128Mi \
--wait --timeout 5m
- echo "Review URL: https://mr-$CI_MERGE_REQUEST_IID.review.example.com"
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://mr-$CI_MERGE_REQUEST_IID.review.example.com
on_stop: stop-review
auto_stop_in: 1 week
stop-review:
<<: *review-template
stage: review
image: alpine/k8s:1.30.0
variables:
GIT_STRATEGY: none
script:
- kubectl delete namespace review-mr-$CI_MERGE_REQUEST_IID --ignore-not-found --wait=false
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
when: manual
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
Wildcard ingress (NGINX)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
namespace: review-mr-123
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
ingressClassName: nginx
tls:
- hosts: ["mr-123.review.example.com"]
secretName: app-tls
rules:
- host: mr-123.review.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: app, port: { number: 80 } }
Janitor CronJob (cleanup orphans)
apiVersion: batch/v1
kind: CronJob
metadata:
name: review-app-janitor
namespace: gitlab-runner
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: janitor
containers:
- name: janitor
image: bitnami/kubectl:latest
command:
- sh
- -c
- |
CUTOFF=$(date -u -d '1 week ago' +%Y-%m-%dT%H:%M:%SZ)
kubectl get namespaces -l environment-type=review \
--output=json | \
jq -r --arg cutoff "$CUTOFF" \
'.items[] | select(.metadata.creationTimestamp < $cutoff) | .metadata.name' | \
xargs -r -n1 kubectl delete namespace
restartPolicy: OnFailure
Cost-friendly per-MR DB (shared instance, schema-per-MR)
review-init-db:
stage: review-prep
script:
- psql $SHARED_DB_URL -c "CREATE SCHEMA IF NOT EXISTS review_$CI_MERGE_REQUEST_IID"
- DATABASE_URL="postgres://shared/myapp?options=-c search_path=review_$CI_MERGE_REQUEST_IID" \
alembic upgrade head
artifacts:
reports:
dotenv: db.env # exports DATABASE_URL for next job
Common findings this catches
- Review URL 404 → wildcard DNS / ingress not configured; ingress controller doesn’t allow wildcards.
- Cert error on review URL → cert-manager DNS-01 / HTTP-01 challenge missing for wildcard.
- Namespace stuck Terminating → finalizer issue; check for stuck PVCs/PVs.
- Reviews accumulating despite
auto_stop_in→ stop job has syntax issue orGIT_STRATEGYnot set, can’t checkout. - Database conflicts between concurrent reviews → schema-per-MR not implemented; each review uses same DB tables.
- Production secret leak in review → variable scope incorrect; restrict to
review/*scope.
When to escalate
- Cluster cost spike from review apps — work with finance/SRE on quota and TTL policy.
- Security audit flagging review app data exposure — synthetic data + auth at ingress.
- Concurrent MR explosion at scale — consider sub-pipeline triggers per service rather than per-monorepo deploy.
Related prompts
-
GitLab Environments & Deployments Debug Prompt
Diagnose GitLab environments — stuck deployments, environment scope, `stop_in` cleanup, protected environments, deployment tier confusion.
-
GitLab CI/CD → Kubernetes Deploy Patterns Prompt
Design GitLab CI/CD pipelines that deploy to Kubernetes — kubectl vs Helm vs Kustomize, secrets handling, multi-environment promotion, GitOps comparison.
-
Kubernetes Ingress Troubleshooting Prompt
Diagnose Ingress routing failures, controller misconfiguration, TLS issues, 404/502/503 cascades, and path-vs-host mismatches across NGINX, Traefik, Contour, and HAProxy controllers.