How do GitLab environments and review apps work, and how do you configure dynamic environments for merge requests?
Quick Answer
GitLab environments track where code is deployed (staging, production) and display deployment history in the UI. Review apps are dynamic, per-merge-request environments that spin up automatically when an MR is opened and tear down when it is merged or closed, giving reviewers a live preview of changes.
Detailed Answer
Think of environments and review apps like a theater company's rehearsal process. The main stage (production) is where the final show runs. The rehearsal hall (staging) is where the full cast practices before opening night. Review apps are like individual practice rooms where each actor can rehearse their solo scenes independently. When an actor finishes rehearsing, their practice room is freed up for someone else. The director (reviewer) can walk into any practice room to watch a specific actor's performance before deciding if it is ready for the main stage.
GitLab environments are declared within job definitions using the environment keyword. When a job with an environment runs successfully, GitLab records a deployment entry linking the commit SHA, the pipeline, and the environment name. The Environments page in the GitLab UI shows all environments, their current deployment status, the last deployed commit, and links to the deployed URL. This gives teams full traceability of what code is running where. Environments also support manual actions like rollbacks: GitLab keeps a history of all deployments to an environment, and you can redeploy any previous version directly from the UI. The environment keyword supports two key properties: name (the environment identifier) and url (the deployed application URL, displayed as a clickable link in the MR).
Internally, when a job with an environment completes, GitLab creates a Deployment record in its database. This record references the environment, the project, the pipeline, the commit SHA, and the deployment status. GitLab maintains an ordered list of deployments per environment, enabling the rollback feature. For dynamic environments (like review apps), GitLab uses variable interpolation in the environment name. When you set name: review/$CI_COMMIT_REF_SLUG, each branch gets its own environment. The on_stop action links a deployment job to a cleanup job: when the environment is stopped (manually or via auto_stop_in), GitLab triggers the stop job, which typically runs terraform destroy or kubectl delete to tear down the infrastructure. GitLab tracks the environment lifecycle through states: available (deployed and accessible), stopping (cleanup in progress), and stopped (resources removed).
In production, a team building a customer-portal application would configure three environment tiers. Feature branches get review apps deployed to ephemeral Kubernetes namespaces like review-feature-oauth-login, giving designers and product managers a live URL to test changes before code review. The staging environment receives deployments from the main branch after all tests pass, serving as the integration testing ground. Production is deployed manually or through a scheduled pipeline with approval gates. The review app configuration uses the Kubernetes executor to create namespaces dynamically, deploys the application with Helm using branch-specific values, and exposes it via an Ingress with a wildcard DNS record like *.review.portal.acme.com. Auto-stop is configured to shut down review apps after 48 hours of inactivity to save cluster resources.
A common gotcha is forgetting to configure the on_stop action for dynamic environments. Without it, review environments accumulate indefinitely, consuming cluster resources and making the Environments page unmanageable. Every review app deployment job should have a corresponding stop job with action: stop and the same environment name. Another pitfall is using CI_COMMIT_REF_NAME instead of CI_COMMIT_REF_SLUG for environment names; the slug version is URL-safe and lowercased, while the raw branch name might contain characters that break DNS or Kubernetes naming conventions.