Deploying to production isn't just running a script — it needs a trail, control, and human approval. This episode covers declaring environments, deployment history in the GitLab UI, manual approval gates with when manual, protected environments, and Review Apps that live and die automatically with merge requests.

In the previous episode 13 we covered how pipelines authenticate to the cloud with OIDC without storing permanent credentials. Now the question shifts: where are those temporary credentials used to deploy? That's where the environment concept comes in.
Imagine monitoring a building management team without a clear key system: anyone can enter, anytime, with no records. When there's a problem on floor 3, nobody knows when the last change was made or by whom. Software deployment faces the same problem. Without a tracking system, "who deployed what version where" is a mystery — and when production is down, that mystery is very expensive.
GitLab solves this with the Environment and Deployment concepts: every target receiving build results (staging, production, review app) is declared explicitly, and every deployment execution is recorded as a traceable deployment entry in the UI. This episode covers declaring environments, manual approval gates for production, protected environments, and Review Apps — dynamic environments that live and die with the merge request lifecycle.
An Environment is a target where the application is deployed — it could be staging, production, or a per-branch environment. When a job declares environment, GitLab links the pipeline result to that environment and records it as a new deployment.
deploy-staging:
stage: deploy
environment:
name: staging
url: https://staging.myapp.com
script:
- echo "Deploy ke staging"The two most important attributes:
name — the environment identity. Common convention: staging, production, or review/<branch-name>.url — the application address. This value makes GitLab display a button to open the environment directly from the pipeline and merge request.Once this job succeeds, a deployment is recorded — GitLab knows which version (based on commit SHA) is currently active in that environment.
All recorded deployments can be traced via the Operate → Environments (or Deployments) menu in the project sidebar. This page shows:
Rollback isn't just a convenience feature — it's one of the strongest reasons to always declare environment on every deployment job. When production has a problem, the team simply clicks rollback to the last healthy deployment instead of rerunning the pipeline from scratch.
when: manualDeploying to production ideally isn't an automatic decision. GitLab provides an approval gate with the when: manual keyword: the job doesn't run until a human clicks the play button in the UI.
deploy-production:
stage: deploy
environment:
name: production
url: https://myapp.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
script:
- echo "Deploy ke produksi"The rules + when: manual combination is the recommended modern pattern: the job only appears in pipelines on the main branch, and only runs when a human clicks it. A manual job holds the pipeline at that stage — the next stage doesn't run until this job is executed or skipped.
Warning
when: manual without extra protection can be abused by anyone who can trigger a pipeline. A manual approval gate alone only delays execution, it doesn't restrict who may execute. To restrict execution to specific people, combine it with a protected environment (next).
Protected environments add an access-control layer to environments. When enabled, GitLab only allows users with specific roles — for example Maintainers or Release Managers — to execute deployment jobs to that environment. Environments can also be restricted to specific branches, for example main only.
deploy-production:
stage: deploy
environment:
name: production
url: https://myapp.com
protected: true
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manualThe who can deploy configuration is done in Settings → CI/CD → Protected environments: choose the environment (e.g. production), then set which roles may deploy and which may approve. With this, the approval gate isn't just a button — it can only be clicked by authorized people.
One of the most productive features in GitLab is Review Apps: every merge request automatically gets a temporary environment (spin-up) for the feature version of the app, so reviewers can try the change for real before approving the MR.
deploy-review:
stage: deploy
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.myapp.com
on_stop: stop-review
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- echo "Deploy review app untuk branch ini"
stop-review:
stage: deploy
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
script:
- echo "Hentikan review app"Notice the patterns:
$CI_COMMIT_REF_SLUG — a slug of the branch name, so every branch gets a unique environment.deploy-review job uses on_stop: stop-review — pointing to the job tasked with stopping the environment.stop-review job uses action: stop, which marks the environment as stopped when run.Tip
A stopped environment doesn't automatically delete its cloud resources — that's the job of the stop-review job's script. Best practice is bundling the teardown action (e.g. deleting a VM, pod, or domain) in the job with action: stop, so finished MRs don't leave idle environments racking up costs.
| Mistake | Symptom | Solution |
|---|---|---|
Not declaring environment | No deployment history, no rollback | Always add environment to deploy jobs |
when: manual without a protected environment | Anyone can execute production | Combine with a protected environment |
| Wrong or empty environment URL | "view app" button doesn't appear | Fill url with the correct app address |
| Review apps never stopped | Cloud costs balloon | Use on_stop + an action: stop job |
| Rollback used without verification | Old bugs return to production | Verify the target commit before rolling back |
Manual environment without rules | Job waits for a click on every pipeline | Restrict with branch-specific rules: if |
In this episode we covered the environment concept as declared deployment targets with environment: name and url; tracking deployment history through the Deployments menu and the rollback feature in the GitLab UI; manual approval gates with when: manual that hold deployments until a human clicks; protected environments restricting execution to specific roles; and Review Apps with on_stop and action: stop for dynamic environments that live and die with merge requests.
The core of this episode: an untracked deployment is a risk; a tracked deployment is an asset. With managed environments, teams know when, by whom, and where every application version was shipped — and can pull it back at any time.
In the next episode 15 we close PHASE 6 with the most hands-on work: Continuous Deployment to Linux Servers via SSH and Ansible — setting up SSH keys, ssh-agent and ssh-keyscan in the pipeline, syncing files with rsync, and automating full deployment with Ansible playbooks. See you there!