GitLab CI/CD for ReactJS App | Multi-Environment Deployment (Dev, QA, Stage, Prod)
This chapter covers the Continuous Deployment (CD) phase of a ReactJS application pipeline in GitLab CI/CD. It explores how to structure multi-environment deployments across Dev, QA, Staging, and Production, utilize common reusable deployment jobs, securely transfer application artifacts via SSH, and manage deployments triggered by branch updates versus version tags.
1. Why Do We Need Multi-Environment CD?
In the previous CI pipeline, our build processes compiled the React application, executed tests, and published release snapshots or tags. However, those assets remain sitting in artifact repositories or GitLab registries.
To make those changes live for users, we need a Continuous Deployment (CD) stage that pushes compiled code to designated servers across multiple isolated tiers (Non-Production: Dev, QA, Staging; Production: Prod) without manual intervention.
2. Core Mental Model & Deployment Architecture
To deploy a React JS application to target EC2 instances, we use dedicated Pipeline Agents running inside specific availability zones with proper network access.
Real-Life Analogy: The Centralized Courier Dispatcher
Think of the GitLab CI pipeline agent as a central courier dispatch office. When a new product package (the React build
.ziparchive) is ready, the dispatcher doesn't personally drive to every single customer's house. Instead, they hand the package to local delivery drivers assigned to specific neighborhoods (Dev/QA Subnets or Prod Subnets). Each driver follows the exact same delivery protocol (unzip, clear old files, copy new files, restart Nginx), but uses different neighborhood addresses (Target IPs and directories).
Deployment Flow Architecture
3. Naive vs. Better Deployment Approach
Writing separate scripts and configurations for every single environment creates immense code duplication and maintenance nightmares.
4. Step-by-Step Evolution of the Deployment Script
To execute deployments cleanly, the workflow is split into two parts:
- The GitLab CI Job Definition (handling artifact downloads and SSH handoffs).
- The Target Server Script (
deploy.sh) (handling local file replacement and service reloads).
The Server-Side Deployment Script (deploy.sh)
Placed directly on all target front-end EC2 instances, this script manages local file extraction and service refreshing:
#!/bin/bash
# deploy.sh - Executed locally on target EC2 instances
VERSION=$1
TARGET_DIR=$2
TEMP_DIR="/tmp/frontend_build"
echo "Starting deployment for version: $VERSION to $TARGET_DIR"
# Step 1: Unzip Artifact in Temp Directory
unzip -o "$TEMP_DIR/app.zip" -d "$TEMP_DIR"
# Step 2: Clean Target Directory and Replace Files
rm -rf "${TARGET_DIR:?}"/*
cp -r "$TEMP_DIR"/* "$TARGET_DIR/"
# Step 3: Refresh Nginx Service
systemctl daemon-reload
systemctl restart nginx
# Step 4: Cleanup Temp Artifacts
rm -rf "$TEMP_DIR/app.zip"
echo "Deployment completed successfully!"
5. Reusable Common Deployment Job in GitLab CI
Instead of repeating code, we define a reusable deployment workflow file (e.g., deploy-common.yml) that accepts variables for target hosts, paths, and environment tags.
# .deploy_template.yml
.deploy_job_template:
script:
- echo "Downloading artifact version $ARTIFACT_VERSION..."
# Artifact download logic executed by pipeline agent
- echo "Connecting to target host $TARGET_HOST via SSH..."
- ssh -i $SSH_PRIVATE_KEY ubuntu@$TARGET_HOST "mkdir -p /tmp/frontend_build"
- echo "Transferring zip archive to target EC2..."
- scp -i $SSH_PRIVATE_KEY app.zip ubuntu@$TARGET_HOST:/tmp/frontend_build/app.zip
- echo "Triggering remote deploy.sh script..."
- ssh -i $SSH_PRIVATE_KEY ubuntu@$TARGET_HOST "bash /home/ubuntu/deploy.sh $ARTIFACT_VERSION $TARGET_DIR"
6. Multi-Environment Pipeline Integration
We wire the common template into our main .gitlab-ci.yml file, mapping specific hostnames, target directories, and stages for Dev, QA, Staging, and Production.
include:
- local: '/ci-templates/deploy-common.yml'
stages:
- build
- non-prod
- prod
variables:
DEV_HOST: "192.168.1.50"
DEV_DIR: "/var/www/html/dev"
QA_HOST: "192.168.1.51"
QA_DIR: "/var/www/html/qa"
PROD_HOST: "10.0.1.10"
PROD_DIR: "/var/www/html/prod"
dev_deploy:
extends: .deploy_job_template
stage: non-prod
variables:
TARGET_HOST: $DEV_HOST
TARGET_DIR: $DEV_DIR
ARTIFACT_VERSION: $CI_COMMIT_REF_NAME
tags:
- non-prod-agent
qa_deploy:
extends: .deploy_job_template
stage: non-prod
variables:
TARGET_HOST: $QA_HOST
TARGET_DIR: $QA_DIR
ARTIFACT_VERSION: $CI_COMMIT_REF_NAME
tags:
- non-prod-agent
prod_deploy:
extends: .deploy_job_template
stage: prod
variables:
TARGET_HOST: $PROD_HOST
TARGET_DIR: $PROD_DIR
ARTIFACT_VERSION: $CI_COMMIT_TAG
rules:
- if: '$CI_COMMIT_TAG'
tags:
- prod-agent
7. Branch Deployments vs. Tagged Production Releases
Understanding when pipelines trigger deployments helps differentiate standard development cycles from official releases:
- Non-Prod Environments (Dev / QA): Automatically deploy on code changes merged into development branches, allowing rapid verification of UI upgrades (e.g., updated React homepage layouts).
- Production Environments: Restricted by design (
rules: - if: '$CI_COMMIT_TAG'). Production deployments never run on raw branch commits; they execute exclusively when an official release tag is cut.
8. Real-World Execution & Verification Walkthrough
- Initial State Check: The production server currently displays the legacy placeholder React homepage (
<h1>Basic App</h1>). - Pushing Changes: Code modifications enhancing the React homepage are pushed and merged into the main branch.
- Automated Non-Prod Deployment:
- The pipeline executes snapshot creation and immediately triggers
dev_deployandqa_deploy. - Refreshing the Dev and QA URLs instantly displays the updated modern homepage.
- The pipeline executes snapshot creation and immediately triggers
- Staging & Production Gate:
- Staging is verified via a manual trigger.
- Once verified, cutting a release tag triggers the
prod_deploypipeline job. - Refreshing the production URL confirms the successful live rollout of the updated frontend.
Summary and Key Takeaways
- Multi-Environment Architecture: Separates deployment targets into non-production tiers (Dev, QA, Staging) and production tiers using dedicated network agents.
- DRY Principle with Templates: Uses common job templates (
.deploy_job_template) to prevent duplicate YAML configurations across environments. - Secure Remote Execution: Leverages SSH and SCP protocols to transfer build
.zipartifacts securely from pipeline agents to target EC2 instances. - Server-Side Shell Automation: Employs a dedicated
deploy.shscript on target servers to handle extraction, directory cleanup, file replacement, and Nginx reloading. - Tag-Gated Production Safety: Ensures production environments deploy strictly from version tags (
$CI_COMMIT_TAG) rather than raw, unverified branch commits. - End-to-End Visibility: Integrates environment mapping and status checks directly into the GitLab CI/CD UI dashboard for instant verification.