Building Clean Staging to Production Promotion with GitLab Pipelines
AI generated
CI/CD
.yml
GitLab · Environments · Staging · Production · Magento
Staging to Production Promotion
Built Cleanly with GitLab Pipelines

Anyone who builds the same thing on production as on staging is not actually testing what they tested on staging. Promoting a validated artifact, instead of rebuilding it, is the foundation of reproducible deployments. GitLab environments, manual approvals and correct variable scopes make this process team ready and auditable.

13 min read Environments · Artifact Promotion · Manual Approval · Rollback GitLab CI/CD · Magento 2 · Zero Downtime

1. The Promotion Concept: Promoting an Artifact Instead of Rebuilding

There is a fundamental mistake in many CI/CD setups: staging and production build the same source code commit, but the build process runs independently twice. Composer versions can differ if a new package version was released between the two builds. npm builds can produce different output files if Node.js or a build tool was updated in the meantime. The result: what was tested on staging is not identical to what lands on production, even though it is technically the same Git hash.

The promotion concept solves this problem with a simple principle: build once, and move the resulting artifact from staging to production. The deploy job on production uses the exact same package that was successfully deployed and verified on staging. No second composer install, no second npm build, no second setup:di:compile. The artifact is the single source of truth, and it gets promoted in a controlled way instead of being regenerated.

In GitLab this concept can be implemented with artifacts, the package registry, or a simple package file that is passed between jobs and stages. Which method you choose depends on artifact size and pipeline runtime. What matters is the principle: build once, deploy many times.

2. GitLab Environments: Cleanly Separating Staging and Production

GitLab environments are more than labels in the pipeline view. They define the context of a deployment and make it possible to manage variables, deployments and approvals per environment. Under Deployments > Environments you can see every active deployment, the current state of each environment and the most recent successful releases. For staging and production, two separate environments are created: staging and production.

Every deploy job in .gitlab-ci.yml gets an environment: keyword. GitLab automatically links the job to the corresponding environment and shows the deployment status in the UI. For production it is also worth using environment: action: start together with protected environments, which are configured in the project settings under Settings > CI/CD > Protected Environments. That is where you define which roles (Maintainer, Owner) are allowed to trigger a deployment job on the production environment.

stages:
  - build
  - test
  - package
  - deploy_staging
  - verify_staging
  - deploy_production
  - verify_production

variables:
  GIT_STRATEGY: fetch
  ARTIFACT_NAME: "magento-release-${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}.tar.gz"

# Build once, reuse artifact across all deploy stages
build:magento:
  stage: build
  image: php:8.4-cli
  script:
    - composer install --no-dev --prefer-dist --no-interaction --quiet
    - npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
    - npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind
    - bin/magento setup:di:compile
    # Package everything into a single tarball for promotion
    - tar -czf "$ARTIFACT_NAME" \
        vendor/ generated/ pub/static/ \
        app/code/ app/design/ app/etc/config.php
  artifacts:
    paths:
      - "*.tar.gz"
    expire_in: 7 days
  only:
    - main
    - tags

3. Configuring Variable Scopes for Staging and Production

GitLab allows you to give variables an environment scope. A variable DEPLOY_HOST with the scope staging holds the staging hostname, and the same variable with the scope production holds the production hostname. The deploy job automatically picks the right value based on its environment: keyword. That means the .gitlab-ci.yml file contains no environment specific hostnames or paths at all; everything lives in the variables.

Setting separate scopes for staging and production is not optional, it is a basic requirement for a safe promotion process. If staging and production share the same unscoped DEPLOY_HOST variable, every job deploys to the same server, or worse, the production job accidentally deploys to staging. With environment scoped variables this risk is structurally ruled out. The GitLab documentation describes variables without a scope as a wildcard (*) that applies to all environments. That is the default case and should always be replaced with specific scopes for environment dependent configuration.

4. Manual Approvals and Approval Gates in GitLab

Production deployments should never start automatically right after a staging deploy. The step from staging to production is a deliberate decision that must be confirmed by an authorized person. GitLab provides when: manual at the job level for this. A manual job appears in the pipeline view as a play button and does not run automatically. Only once someone with the necessary permissions clicks that button does the job start.

For more formal approvals, GitLab Premium offers protected environments with required approvals: several people have to approve the deployment job before it starts. Even without Premium, you can build a two stage pattern: the staging verify job has to complete successfully (needs: [verify:staging]), and the production deploy job is manual. This structurally guarantees that production is only offered once staging is green, and only runs after manual confirmation. That is the simplest and most effective approval gate without a Premium license.

deploy:staging:
  stage: deploy_staging
  before_script:
    - eval $(ssh-agent -s)
    - ssh-add "$SSH_PRIVATE_KEY"
    - mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
  script:
    # Transfer artifact and run deploy script on staging server
    - scp "$ARTIFACT_NAME" "$DEPLOY_USER@$DEPLOY_HOST:/tmp/"
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "bash -s $ARTIFACT_NAME" < scripts/deploy.sh
  environment:
    name: staging
    url: https://staging.mironsoft.de
  needs: [build:magento]

verify:staging:
  stage: verify_staging
  script:
    - curl --fail --max-time 10 "https://staging.mironsoft.de/health"
    - curl --fail --max-time 10 "https://staging.mironsoft.de/"
  environment:
    name: staging
  needs: [deploy:staging]

deploy:production:
  stage: deploy_production
  before_script:
    - eval $(ssh-agent -s)
    - ssh-add "$SSH_PRIVATE_KEY"
    - mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
  script:
    # Promote the same artifact, no rebuild on production
    - scp "$ARTIFACT_NAME" "$DEPLOY_USER@$DEPLOY_HOST:/tmp/"
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "bash -s $ARTIFACT_NAME" < scripts/deploy.sh
  environment:
    name: production
    url: https://mironsoft.de
  needs: [verify:staging]
  when: manual
  only:
    - tags

5. Passing and Reusing Artifacts Between Stages

GitLab artifacts are files that a job produces and that subsequent jobs in the same pipeline run can download. With needs: [build:magento], a deploy job makes sure it receives the artifact from the build job. This also works across stages: the production deploy job in stage 6 can download the artifact from stage 1 directly, without it having to be passed through every intermediate stage.

For large artifacts, a full Magento package with vendor, generated and static files can easily reach several hundred megabytes, transferring the artifact through the GitLab API is sometimes slower than a direct SCP transfer to the server. One alternative: store the artifact from the build job on external storage (S3, GCS or NFS) and download it from there in the deploy job. The artifact URL then serves as the reference that is shared between jobs. The advantage: the artifact stays available even after GitLab's artifact retention period expires and can still be used for later rollbacks.

6. Staging Verification as a Prerequisite for Production

A staging verify job is more than a smoke test, it is the quality gate between staging and production. Whatever was verified on staging gets promoted to production; whatever fails blocks the promotion. For Magento projects, the staging verify step should cover at least the following checks: HTTP status code of the frontend, the health endpoint, the Magento cache status via SSH, and a sample of critical pages (home page, category, product, checkout entry point).

Anyone who wants more confidence should add automated browser tests with Playwright or Puppeteer against the staging URL as part of the verify job. These tests run after the deploy to staging and must be fully green before the production deploy job becomes available as a manual trigger. This pattern guarantees that the person clicking the production deploy button is not deciding based on hope, but based on verified test results.

7. Rebuilding on Production vs. Promotion Compared

The comparison below shows where the practical differences lie between rebuilding directly on production and true artifact promotion.

Criterion Rebuild on Production Artifact Promotion Recommendation
Reproducibility Depends on package versions at build time Identical artifact as on staging Promotion
Test validity What was tested does not match what gets deployed What was green on staging ships to production Promotion
Deploy duration Long build step on production Only transfer and extraction Promotion is faster
Rollback basis Which version exactly was it? Artifact with tag reference, traceable at any time Promotion is auditable
Server load Build process on the production server Only extraction and symlink switch Promotion is gentler

The most common objection to the promotion model is that the artifact is too large for GitLab's artifact retention. The solution is not to abandon promotion, it is to store the artifact on external storage and only pass along the reference. That is an infrastructure problem you can solve; trading reproducibility for convenience is not something you can undo later.

8. Magento Specific Steps During Promotion

Once the artifact has been extracted on the production server, Magento specific steps follow that can differ from staging. The env.php and config.php files from the shared directories are linked in via symlink, not overwritten from the artifact. The reason: env.php contains database specific credentials and environment dependent configuration that is different on production than on staging.

For static content there are two approaches during promotion: either the static content already compiled on staging is taken over from the artifact, which is fast but requires theme paths that are identical across both environments, or setup:static-content:deploy runs after the symlink switch on production. With Hyva themes and a Tailwind build, the artifact is the better choice because the CSS output depends on the build process and should not be reproduced by the production server. The Tailwind build runs in the pipeline's build job, and the result becomes part of the artifact.

# Magento-specific steps executed on the target server after artifact deployment
# These run via SSH, not in the GitLab runner itself

# scripts/deploy.sh (called on remote server)
# Usage: bash deploy.sh <artifact_name>

# Example of the remote deployment sequence:
# 1. Extract artifact into new release directory
# 2. Link shared files (env.php, pub/media, var/log, var/session)
# 3. Run setup:upgrade if schema changes are expected
# 4. Flush cache, static content already in artifact from build stage
# 5. Switch symlink atomically
# 6. Run post-deploy verify commands

verify:production:
  stage: verify_production
  before_script:
    - eval $(ssh-agent -s)
    - ssh-add "$SSH_PRIVATE_KEY"
    - mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
  script:
    # HTTP checks
    - curl --fail --max-time 15 "https://mironsoft.de/health"
    - curl --fail --max-time 15 "https://mironsoft.de/"
    # Magento status via SSH
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" \
        "cd $DEPLOY_PATH/current && bin/magento cache:status && bin/magento --version"
    # Check no maintenance mode is active
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" \
        "test ! -f $DEPLOY_PATH/current/var/.maintenance.flag"
  environment:
    name: production
  needs: [deploy:production]
  only:
    - tags

9. Rollback Strategy After a Failed Promotion

If the production verify job fails after promotion, a rollback has to happen immediately. The rollback strategy is based on the release directory structure: the previous release directory is still present, and switching the symlink back takes milliseconds. A rollback:production job with when: manual is prepared in the pipeline and can be triggered right away.

After the rollback comes root cause analysis: what was different about the promoted artifact compared to staging? Common causes are database schema differences (migration applied on staging but not on production), environment dependent configuration that was not reflected in env.php, or caching states (Redis, Varnish) that differ between production and staging. Once the cause is identified, you can fix the staging environment, verify it again, and start a new promotion, this time with better verify coverage.

10. Summary

Staging to production promotion is not a feature you bolt on eventually when there is time; it is the foundation for reproducible, testable and auditable deployments. The principle is simple: build once, verify the result on staging, then promote the exact same package to production. GitLab supports this process with environments, environment scoped variables, manual jobs and artifact passing between stages.

The most important decision is the deliberate separation of build and deploy: no composer install on the production server, no npm build, no setup:di:compile. Everything that can be reproduced deterministically in the build job belongs in the build job, and the resulting artifact is what lands on production. What was tested on staging is what arrives on production. That is the core of continuous delivery for Magento projects.

Staging to Production Promotion: The Essentials at a Glance

Core Principle

Build once, deploy and verify the artifact on staging, then promote the exact same package to production. No rebuild.

Manual Approval

Production deploy job with when: manual and needs: [verify:staging]. Production is only available once staging is green.

Environment Scopes

DEPLOY_HOST, DEPLOY_USER and DEPLOY_PATH as scoped variables. Staging and production automatically get the right values.

Rollback Readiness

rollback:production prepared as a manual job. Ready to trigger instantly after a failed verify, no improvising during an incident.

11. FAQ: Staging to Production Promotion in GitLab

1Why not rebuild on production?
Composer and npm builds are not fully deterministic. Package versions can change between two runs, so the package deployed to production is not identical to the one tested on staging.
2What is a GitLab environment?
A named deployment context with its own deployment history, URL and variable scopes. Lets you see the current state of each environment in the GitLab UI and track it over time.
3Prevent production from deploying before staging?
needs: [verify:staging] on the production job. GitLab only runs it once the staging verify was successful. With when: manual, manual confirmation is required as well.
4How large can a GitLab artifact be?
Up to 1 GB per job on gitlab.com. For very large artifacts, external storage with a reference URL is recommended instead of a direct GitLab artifact transfer.
5Same deploy job for staging and production?
Yes, if the differences come from environment scoped variables. Both jobs use the same script section but get different variable values based on the environment: name.
6when: manual vs. protected environments?
when: manual allows anyone with pipeline rights to start it. Protected environments (Premium) require approval from specific roles. The stronger solution for formal approvals.
7Does setup:static-content:deploy have to run on production?
Not if the static content is already included in the build artifact. Recommended for Hyva with Tailwind: the Tailwind build and SCD run in the CI build job, and the result is part of the artifact.
8How long should artifacts be kept?
At least as long as it makes sense to roll back to a release: seven to thirty days is common. External storage allows for longer retention independent of GitLab.
9Test the promotion before it reaches production?
Practice the promotion process on a production-like environment. Anyone who only tests on staging may not be checking the same server characteristics as production.
10Different database states on staging and production?
Account for database migrations explicitly in the deploy process. setup:upgrade runs after the artifact deploy. Expand-contract patterns for backward compatible schema changes prevent downtime.