GitLab’s Review Apps are a game-changer for collaborative development, allowing you to spin up ephemeral, production-like environments for every merge request.

Let’s see it in action. Imagine a developer submits a merge request for a new feature.

# .gitlab-ci.yml
deploy_staging:
  stage: deploy
  script:
    - echo "Deploying to staging..."
    - helm upgrade --install my-app ./helm/my-app --namespace staging --set image.tag=$CI_COMMIT_SHA --wait
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - main

review_app:
  stage: deploy
  script:
    - echo "Deploying review app for merge request $CI_MERGE_REQUEST_IID..."
    - helm upgrade --install review-app-$CI_MERGE_REQUEST_IID ./helm/my-app --namespace review-apps --set image.tag=$CI_COMMIT_SHA --wait
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://review.example.com/$CI_COMMIT_REF_SLUG
  only:
    - merge_requests

In this snippet, the deploy_staging job deploys the main branch to a permanent staging environment. The review_app job, however, is triggered for every merge request. It uses the CI_MERGE_REQUEST_IID and CI_COMMIT_REF_SLUG predefined variables to create a unique, isolated environment for that specific MR. The environment block tells GitLab where to find the deployed app, creating a link directly in the merge request UI.

The core problem Review Apps solve is the friction in the "review and merge" cycle. Traditionally, testing changes on a merge request meant either deploying to a shared staging environment (which could conflict with other MRs or the main branch) or relying solely on automated tests. Review Apps provide a dedicated, isolated space for each MR, allowing reviewers to interact with the exact changes in a production-like setting before merging.

Internally, Review Apps leverage GitLab’s CI/CD pipeline and Kubernetes (or other deployment targets). The key is the dynamic creation of environments. When a merge request is opened, the review_app job runs. It takes the application’s code, builds it into an artifact (often a Docker image), and then deploys this artifact to a dedicated namespace or sub-domain. The environment definition in .gitlab-ci.yml is crucial here; it tells GitLab how to track these dynamic environments and provides a direct link to them. This link appears right in the merge request, making it incredibly easy for anyone to access and test the changes.

The environment key in the CI job definition is more powerful than just providing a URL. It allows GitLab to manage the lifecycle of these environments. For Review Apps, you’ll often see name: review/$CI_COMMIT_REF_SLUG. The $CI_COMMIT_REF_SLUG variable is generated from the branch or tag name and is unique for each merge request branch. This creates a predictable URL structure, like https://my-app-review-feature-branch-123.example.com. When the merge request is closed, or the branch is deleted, GitLab can automatically clean up these environments, preventing resource sprawl.

A common pattern is to use a Helm chart for deployment. The helm upgrade --install command is idempotent, meaning it can be run multiple times. If the release doesn’t exist, it installs it; if it does, it upgrades it. This is perfect for CI, as the job might run multiple times for a single MR. The --namespace review-apps ensures all review apps are logically grouped, and --set image.tag=$CI_COMMIT_SHA ensures that each deployment uses the specific commit SHA, providing an exact snapshot of the code.

The true magic is how GitLab integrates this into the merge request view. You’ll see a "Deployed" status with a direct link. Clicking it opens the isolated environment for that specific MR. This drastically reduces the "it works on my machine" problem and speeds up the feedback loop.

The underlying mechanism for environment tracking relies on GitLab associating deployment information with specific commits. When a job with an environment definition runs, GitLab records the environment name, URL, and the commit SHA. This data is then displayed in the merge request and commit history. When you navigate to the environment URL, the web server or load balancer is configured to route requests based on the subdomain or path to the correct, isolated application instance running in Kubernetes (or your chosen platform).

Most people configure Review Apps to deploy to Kubernetes using Helm or Kustomize. However, you can adapt this pattern to other deployment targets. For instance, if you’re deploying to a PaaS like Heroku, you might use their CLI to create a new app instance for each MR, setting the environment.url to the Heroku app URL. The core principle remains: dynamically provision an isolated environment for each MR and link to it from the merge request.

The next step in mastering dynamic environments is understanding how to manage their lifecycle, including automatic cleanup of stale or merged-request-closed environments.

Want structured learning?

Take the full Gitlab-ci course →