Deploy with a Single Git Push

Automate your entire deployment pipeline. Push to the "live" branch and watch GitLab CI/CD build, test, deploy to staging, then switch production with <1 second downtime .

๐Ÿš€
One-Command Deploy

git push origin live - that's it!

๐Ÿงช
Staging Verification

Test on staging subdomain before production

โšก
Manual Approval

Review staging, then click "Deploy" in GitLab

<1s
Zero Downtime

Atomic symlink switch to production

How GitLab CI/CD Deployment Works

This automated pipeline builds on the manual blue-green deployment strategy, automating every step from code push to production switch.

Deployment Flow

  1. Trigger: Developer pushes to "live" branch
  2. Build: GitLab Runner builds Nuxt 4 app (pnpm build)
  3. Deploy to Staging: Upload to timestamped release folder, install deps, link shared resources
  4. Staging Health Check: Verify app works on staging subdomain (staging.example.com)
  5. Manual Approval: Developer reviews staging site, clicks "Deploy to Production" in GitLab UI
  6. Production Switch: Atomic symlink update + PM2 restart (<1 second downtime)
  7. Verify & Cleanup: Health check production, remove old releases

Pipeline Stages

Stage 1: Build

Installs dependencies, builds Nuxt 4 app, creates deployment artifact

Stage 2: Deploy Staging

SSHs to server, deploys to timestamped release, starts on staging subdomain

Stage 3: Deploy Production (Manual)

Waits for manual approval, then switches symlink and restarts PM2

Stage 4: Cleanup

Removes old releases (keeps last 3), cleans up temp files

Key Benefit: The staging step lets you verify the deployment on a real server environment (staging.example.com) before making it live. If something's wrong, just don't approve the production deploy!

๐ŸŽฏ Personalize This Guide

Enter your server details below to customize all commands throughout this guide. Your values will appear in highlighted text .

Enter your production domain name
Enter your staging subdomain
Enter application name for PM2
Enter your server username for SSH access
Enter IPv4 address of your server
Port for production application
Port for staging environment

Prerequisites

Before setting up GitLab CI/CD deployment, ensure you have the following in place:

1. Server Setup

2. GitLab Runner

3. GitLab CI/CD Variables

Configure these secret variables in GitLab: Settings โ†’ CI/CD โ†’ Variables

4. NGINX Configuration

Staging subdomain must be configured in NGINX to proxy to staging port:

# /etc/nginx/sites-available/{{deploy.staging || 'staging.example.com'}}
server {
    listen 80;
    server_name {{deploy.staging || 'staging.example.com'}};
    
    location / {
        proxy_pass http://127.0.0.1:{{deploy.stagingport || '3102'}};
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

# Enable and reload
sudo ln -s /etc/nginx/sites-available/{{deploy.staging || 'staging.example.com'}} /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

5. PM2 Ecosystem Configuration

Ensure ecosystem.config.cjs supports both production and staging:

module.exports = {
  apps: [{
    name: '{{deploy.appname || 'my-app'}}',
    script: './.output/server/index.mjs',
    cwd: '/var/www/{{deploy.domain || 'example.com'}}/current',
    instances: 'max',
    exec_mode: 'cluster',
    env_production: {
      NODE_ENV: 'production',
      PORT: {{deploy.prodport || '3101'}}
    },
    env_staging: {
      NODE_ENV: 'production',
      PORT: {{deploy.stagingport || '3102'}}
    }
  }]
};

GitLab CI/CD Pipeline Configuration

Create a .gitlab-ci.yml file in your repository root to define the automated deployment pipeline.

Complete .gitlab-ci.yml

# .gitlab-ci.yml - GitLab CI/CD Zero-Downtime Deployment
# Trigger: Push to "live" branch

stages:
  - build
  - deploy_staging
  - deploy_production
  - cleanup

variables:
  DEPLOY_DIR_BASE: "/var/www/{{deploy.domain || 'example.com'}}"
  APP_NAME: "{{deploy.appname || 'my-app'}}"
  NODE_VERSION: "20.18.1"

# Build Stage: Compile Nuxt 4 application
build:
  stage: build
  image: node:20
  only:
    - live
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
      # Note: .output/ is intentionally excluded from cache to avoid stale builds
      # If code changes don't appear after deployment, the cache may be the issue
  script:
    - echo "๐Ÿ”ง Installing pnpm..."
    - npm install -g pnpm@latest

    - echo "๐Ÿ“ฆ Installing dependencies..."
    - pnpm install --frozen-lockfile

    # Optional: Clean build to avoid cache issues (uncomment if needed)
    # - rm -rf .nuxt .output node_modules/.vite

    - echo "๐Ÿ”จ Building Nuxt 4 application..."
    - NETLIFY_BLOBS=false pnpm build

    # Verify build succeeded
    - test -f .output/server/index.mjs || (echo "โŒ Build failed - server output missing!" && exit 1)
    
    - echo "๐Ÿ“‹ Preparing deployment package..."
    - mkdir -p deploy-package
    - cp -r .output deploy-package/
    - cp package.json pnpm-lock.yaml deploy-package/
    - cp -r prisma deploy-package/
    - cp -r scripts deploy-package/
    - cp ecosystem.config.cjs deploy-package/
    
    - echo "โœ… Build complete!"
  artifacts:
    name: "nuxt-build-${CI_COMMIT_SHORT_SHA}"
    paths:
      - deploy-package/
    expire_in: 1 hour

# Deploy to Staging: Upload and start on staging subdomain
deploy_staging:
  stage: deploy_staging
  only:
    - live
  dependencies:
    - build
  before_script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan $SERVER_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
  script:
    - echo "๐Ÿš€ Deploying to staging environment..."
    - VERSION=$(date +%Y%m%d_%H%M%S)
    - RELEASE_DIR="${DEPLOY_DIR_BASE}/releases/${VERSION}"
    
    - echo "๐Ÿ“ค Uploading build to server..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "mkdir -p ${RELEASE_DIR}"
    - scp -r deploy-package/* ${SERVER_USER}@${SERVER_HOST}:${RELEASE_DIR}/
    
    - echo "โš™๏ธ  Installing dependencies on server..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        cd ${RELEASE_DIR} &&
        source ~/.nvm/nvm.sh &&
        nvm use ${NODE_VERSION} &&
        pnpm install --prod --frozen-lockfile &&
        pnpm dlx prisma generate &&
        pnpm run preflight || true &&
        pnpm run preflight:db || true &&
        mkdir -p logs && chmod 775 logs
      "
    
    - echo "๐Ÿ”— Linking shared resources..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        ln -sf ${DEPLOY_DIR_BASE}/shared/.env ${RELEASE_DIR}/.env &&
        ln -sf ${DEPLOY_DIR_BASE}/shared/db.sqlite ${RELEASE_DIR}/prisma/db.sqlite
      "
    
    - echo "๐Ÿ—„๏ธ  Syncing database schema..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        cd ${RELEASE_DIR} &&
        source ~/.nvm/nvm.sh &&
        nvm use ${NODE_VERSION} &&
        pnpm dlx prisma db push || echo 'โš ๏ธ  Schema already in sync'
      "
    
    - echo "๐Ÿ”„ Updating staging symlink..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        ln -sfn ${RELEASE_DIR} ${DEPLOY_DIR_BASE}/staging
      "
    
    - echo "โ™ป๏ธ  Starting/restarting staging PM2 process..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        cd ${DEPLOY_DIR_BASE}/staging &&
        pm2 delete ${APP_NAME}-staging || true &&
        pm2 start ecosystem.config.cjs --name ${APP_NAME}-staging --env staging &&
        pm2 save
      "
    
    - echo "๐Ÿงช Running staging health check..."
    - sleep 5
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        curl -f -I http://127.0.0.1:${STAGING_PORT}/ || (echo 'โŒ Staging health check failed!' && exit 1)
      "
    
    - echo "โœ… Staging deployment complete!"
    - echo "๐Ÿ“ Version: ${VERSION}"
    - echo "๐ŸŒ Staging URL: https://{{deploy.staging || 'staging.example.com'}}"
    - echo ""
    - echo "๐Ÿ‘‰ Review staging site, then approve 'Deploy to Production' job in GitLab UI"
  environment:
    name: staging
    url: https://{{deploy.staging || 'staging.example.com'}}

# Deploy to Production: Manual approval required
deploy_production:
  stage: deploy_production
  only:
    - live
  when: manual
  dependencies: []
  before_script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan $SERVER_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
  script:
    - echo "โšก Deploying to production..."
    - echo "This will switch the production symlink and restart PM2"
    
    - echo "๐Ÿ”„ Reading staging release path..."
    - STAGING_PATH=$(ssh ${SERVER_USER}@${SERVER_HOST} "readlink ${DEPLOY_DIR_BASE}/staging")
    - echo "Staging release: ${STAGING_PATH}"
    
    - echo "โšก ATOMIC SWITCH: Updating production symlink..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        ln -sfn ${STAGING_PATH} ${DEPLOY_DIR_BASE}/current
      "
    
    - echo "โ™ป๏ธ  Graceful PM2 restart (follows symlink, <1s downtime)..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        pm2 restart ${APP_NAME} --update-env &&
        pm2 save
      "
    
    - echo "โœ”๏ธ  Verifying production deployment..."
    - sleep 3
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        curl -f -I http://127.0.0.1:${PROD_PORT}/ || (echo 'โŒ Production health check failed!' && exit 1)
      "
    
    - CURRENT_VERSION=$(ssh ${SERVER_USER}@${SERVER_HOST} "readlink ${DEPLOY_DIR_BASE}/current | sed 's|.*/||'")
    - echo "โœ… Production deployment complete!"
    - echo "๐Ÿ“ฆ Version: ${CURRENT_VERSION}"
    - echo "๐ŸŒ Production URL: https://{{deploy.domain || 'example.com'}}"
  environment:
    name: production
    url: https://{{deploy.domain || 'example.com'}}

# Cleanup: Remove old releases
cleanup:
  stage: cleanup
  only:
    - live
  when: on_success
  dependencies: []
  before_script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan $SERVER_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
  script:
    - echo "๐Ÿงน Cleaning old releases..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        cd ${DEPLOY_DIR_BASE}/releases &&
        KEEP_COUNT=3 &&
        RELEASE_COUNT=\$(ls -1 | wc -l) &&
        if [ \$RELEASE_COUNT -gt \$KEEP_COUNT ]; then
          ls -t | tail -n +\$((KEEP_COUNT + 1)) | xargs -r rm -rf &&
          echo 'โœ… Cleaned old releases (kept last '\$KEEP_COUNT')'
        else
          echo 'โœ… No cleanup needed (only '\$RELEASE_COUNT' releases)'
        fi
      "
    
    - ACTIVE_COUNT=$(ssh ${SERVER_USER}@${SERVER_HOST} "ls -1 ${DEPLOY_DIR_BASE}/releases | wc -l")
    - echo "๐Ÿ“Š Active releases: ${ACTIVE_COUNT}"
๐Ÿ’ก Key Features:
  • Only triggers on "live" branch pushes
  • Builds once, deploys to staging automatically
  • Requires manual approval before production switch
  • Shares the same release between staging and production
  • Atomic symlink switch ensures zero downtime
  • Automatic cleanup keeps last 3 releases

Setting Up GitLab Runner

For this pipeline to work, you need a GitLab Runner configured on your server (or use GitLab's shared runners with SSH access).

Option 1: Install GitLab Runner on Your Server (Recommended)

# SSH into your server
ssh {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}
# Install GitLab Runner
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install gitlab-runner

# Register runner
sudo gitlab-runner register

# Follow prompts:
# - GitLab instance URL: https://gitlab.com (or your instance)
# - Registration token: Get from GitLab โ†’ Settings โ†’ CI/CD โ†’ Runners
# - Description: production-deploy
# - Tags: production,deployment
# - Executor: shell

# Start runner
sudo gitlab-runner start

# Verify
sudo gitlab-runner status

Option 2: Use GitLab Shared Runners with SSH

If using GitLab's shared runners, the pipeline will SSH into your server. Ensure:

Configure GitLab CI/CD Variables

In GitLab: Settings โ†’ CI/CD โ†’ Variables โ†’ Add Variable

Variable Value Protected Masked
SSH_PRIVATE_KEY Your private SSH key (entire content) โœ“ โœ—
SERVER_USER {{deploy.sudouser || 'deploy'}} โœ“ โœ—
SERVER_HOST {{deploy.ipaddress || deploy.domain || '192.168.1.100'}} โœ“ โœ—
PROD_PORT {{deploy.prodport || '3101'}} โœ“ โœ—
STAGING_PORT {{deploy.stagingport || '3102'}} โœ“ โœ—
Security: Mark variables as "Protected" to only expose them to protected branches (like "live"). Never commit secrets to your repository!

Deployment Workflow

Once everything is configured, deploying is as simple as pushing to the "live" branch.

Step 1: Create "live" Branch (One-Time)

# In your local repository
git checkout main
git checkout -b live
git push -u origin live

# Protect the branch in GitLab
# Settings โ†’ Repository โ†’ Protected Branches โ†’ Add "live"

Step 2: Deploy Your Changes

# Make your changes on main or feature branch
git checkout main
# ... make changes, commit ...

# Merge to live branch to trigger deployment
git checkout live
git merge main
git push origin live

# GitLab CI/CD pipeline will automatically:
# 1. Build the app
# 2. Deploy to staging
# 3. Wait for your approval

Step 3: Review Staging

  1. Go to GitLab โ†’ CI/CD โ†’ Pipelines
  2. Click on the running pipeline
  3. Wait for "deploy_staging" job to complete
  4. Visit https:// {{deploy.staging || 'staging.example.com'}}
  5. Test your changes thoroughly

Step 4: Deploy to Production

  1. In the pipeline view, find the "deploy_production" job
  2. Click the "Play" button (โ–ถ) to approve deployment
  3. Watch the logs as it switches production symlink and restarts PM2
  4. Verify at https:// {{deploy.domain || 'example.com'}}
That's it! Your changes are now live with less than 1 second of downtime. The entire process is automated, traceable, and safe.

Rollback Strategy

If something goes wrong in production, you have multiple rollback options:

Option 1: Re-deploy Previous Version via GitLab

# Revert the merge commit
git checkout live
git revert HEAD
git push origin live

# Or reset to previous commit
git reset --hard HEAD~1
git push -f origin live

# Pipeline will deploy the previous version

Option 2: Manual Symlink Rollback (Instant)

# SSH into server
ssh {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}
# List available releases
ls -lt /var/www/{{deploy.domain || 'example.com'}}/releases/

# Get previous version
CURRENT=$(readlink /var/www/{{deploy.domain || 'example.com'}}/current | sed 's|.*/||')
PREVIOUS=$(ls -t /var/www/{{deploy.domain || 'example.com'}}/releases/ | grep -v "^${CURRENT}$" | head -1)

echo "Current: $CURRENT"
echo "Rolling back to: $PREVIOUS"

# Atomic rollback
ln -sfn "/var/www/{{deploy.domain || 'example.com'}}/releases/$PREVIOUS" /var/www/{{deploy.domain || 'example.com'}}/current
pm2 restart {{deploy.appname || 'my-app'}} --update-env

# Verify
curl -I http://127.0.0.1:{{deploy.prodport || '3101'}}/
echo "โœ… Rolled back to $PREVIOUS"

Option 3: Re-run Old Pipeline

  1. Go to GitLab โ†’ CI/CD โ†’ Pipelines
  2. Find the successful pipeline you want to re-deploy
  3. Click "Retry" (๐Ÿ”„)
  4. Approve the production deployment
Rollback Time: Manual symlink rollback takes 5-10 seconds. GitLab re-deployment takes 3-5 minutes (full build). Choose based on urgency!

Advanced: Database Migrations in CI/CD

Handling database migrations requires extra care in automated deployments.

Strategy 1: Auto-Migrate in Pipeline (Simple)

The example pipeline already includes pnpm dlx prisma db push in the staging deploy. This works for development but can be risky in production.

Strategy 2: Manual Migration Approval (Safer)

Add a separate manual migration stage:

# Add to .gitlab-ci.yml after deploy_staging
migrate_database:
  stage: deploy_staging
  only:
    - live
  when: manual
  allow_failure: false
  before_script:
    # ... same SSH setup ...
  script:
    - echo "๐Ÿ—„๏ธ  Applying database migrations..."
    - STAGING_PATH=$(ssh ${SERVER_USER}@${SERVER_HOST} "readlink ${DEPLOY_DIR_BASE}/staging")
    - ssh ${SERVER_USER}@${SERVER_HOST} "
        cd ${STAGING_PATH} &&
        source ~/.nvm/nvm.sh &&
        nvm use ${NODE_VERSION} &&
        
        # Backup database first
        cp ${DEPLOY_DIR_BASE}/shared/db.sqlite ${DEPLOY_DIR_BASE}/shared/db.sqlite.backup-\$(date +%Y%m%d_%H%M%S) &&
        
        # Apply migrations
        pnpm dlx prisma migrate deploy &&
        pnpm dlx prisma generate
      "
    - echo "โœ… Database migrated successfully"

Strategy 3: Backward-Compatible Migrations (Best)

Design migrations that work with both old and new code:

Production Safety: Always backup database before migrations! The example includes automatic backup in the migration job.

Monitoring & Notifications

Stay informed about your deployments with GitLab's built-in notifications and integrations.

Email Notifications

GitLab automatically emails you when pipelines fail. Configure in: User Settings โ†’ Notifications

Slack Integration

# Add to .gitlab-ci.yml
notify_slack:
  stage: cleanup
  only:
    - live
  when: on_success
  script:
    - |
      curl -X POST -H 'Content-type: application/json' \
      --data "{\"text\":\"โœ… Deployment successful! Version: ${CI_COMMIT_SHORT_SHA}\"}" \
      $SLACK_WEBHOOK_URL

GitLab Environments

The pipeline already defines "staging" and "production" environments. View deployment history in: GitLab โ†’ Deployments โ†’ Environments

PM2 Monitoring

Monitor your application post-deployment:

# Add health monitoring job
monitor_production:
  stage: cleanup
  only:
    - live
  when: on_success
  script:
    - echo "๐Ÿ“Š Checking PM2 status..."
    - ssh ${SERVER_USER}@${SERVER_HOST} "pm2 list"
    - ssh ${SERVER_USER}@${SERVER_HOST} "pm2 logs ${APP_NAME} --lines 50 --nostream"

Troubleshooting

๐Ÿšจ Code Changes Not Appearing After Deployment

Symptom: Pipeline completes successfully but your code changes aren't visible on production.

Causes and Solutions:

1. GitLab CI/CD Cache Issue

# In .gitlab-ci.yml, temporarily disable cache for one build:
build:
  stage: build
  cache: {}  # Disable cache
  # ... rest of config

# Or clear cache in GitLab UI:
# CI/CD โ†’ Pipelines โ†’ Clear Runner Caches

# After successful deployment, re-enable cache

2. Service Worker Caching (if PWA was enabled)

# Users need to clear service workers:
# Chrome/Edge: F12 โ†’ Application โ†’ Service Workers โ†’ Unregister
# Firefox: about:serviceworkers โ†’ Unregister

# Or disable PWA module entirely in nuxt.config.ts:
modules: [
  "@nuxt/eslint",
  # ... other modules
  // "@vite-pwa/nuxt", // DISABLED
],

Pipeline Fails at Build Stage

"Cannot access 'sessionHooks' before initialization"

Error: ReferenceError: Cannot access 'sessionHooks' before initialization
# Ensure you have the patch-sessionhooks module in your project
# File: modules/patch-sessionhooks.ts
# This module fixes export order issues with nuxt-auth-utils

# If error persists, rebuild with clean cache:
# In .gitlab-ci.yml, add before build:
- rm -rf .nuxt .output node_modules/.vite

PWA Build Errors

Error: Cannot read properties of undefined (reading 'Symbol(ProxyTarget)')
# Disable PWA module if not needed
# In nuxt.config.ts:
modules: [
  "@nuxt/eslint",
  "@nuxt/image",
  "@nuxt/content",
  "@nuxt/ui",
  "./modules/patch-sessionhooks",
  // "@vite-pwa/nuxt", // DISABLED - causing build issues
],

# Commit and push to trigger new build

Empty .output/server Folder

# Build failed silently - check GitLab build logs carefully
# Common causes:
# - PWA module errors (disable if not needed)
# - TypeScript errors (add typecheck stage before build)
# - Missing dependencies
# - Environment variables not set

# Add build verification to pipeline:
- test -f .output/server/index.mjs || exit 1

General Build Failures

# Check build logs in GitLab UI
# Common issues:
# - Missing dependencies in package.json
# - TypeScript errors
# - Environment variables not set

# Test build locally first:
pnpm install
pnpm build

# Add linting and type checking stages:
lint:
  stage: build
  script:
    - pnpm install
    - pnpm lint
    - pnpm typecheck

SSH Connection Issues

# Verify SSH key is correct
# In GitLab variables, ensure SSH_PRIVATE_KEY has:
# - Full key including -----BEGIN/END----- lines
# - No extra spaces or newlines

# Test SSH access manually:
ssh -i ~/.ssh/your_key {{deploy.sudouser || 'username'}}@{{deploy.ipaddress || 'your-server'}}

# Check server's authorized_keys
cat ~/.ssh/authorized_keys

Staging Health Check Fails

# SSH into server and check staging process
ssh {{deploy.sudouser || 'username'}}@{{deploy.ipaddress || 'your-server'}}

# Check PM2 logs
pm2 logs {{deploy.appname || 'my-app'}}-staging --lines 100

# Test staging port manually
curl -I http://127.0.0.1:{{deploy.stagingport || '3102'}}/

# Common issues:
# - Port already in use
# - Missing .env file
# - Database connection issues
# - PM2 process crashed

Production Switch Fails

# Check current symlink
ssh {{deploy.sudouser || 'username'}}@{{deploy.ipaddress || 'your-server'}}
readlink /var/www/{{deploy.domain || 'example.com'}}/current

# Check PM2 status
pm2 list
pm2 logs {{deploy.appname || 'my-app'}} --lines 50

# Verify PM2 is using symlink path
pm2 describe {{deploy.appname || 'my-app'}} | grep cwd
# Should show: /var/www/{{deploy.domain || 'example.com'}}/current

GitLab Runner Not Picking Up Jobs

# Check runner status
sudo gitlab-runner status

# Verify runner is registered
sudo gitlab-runner list

# Check runner tags match .gitlab-ci.yml
# In GitLab: Settings โ†’ CI/CD โ†’ Runners

# Restart runner
sudo gitlab-runner restart

Common Questions

Why do PM2 logs show "CSP Disabled"?

This is normal and expected. Content Security Policy (CSP) is an optional security feature disabled by default. To enable it, add CSP_ENABLED=true to your shared/.env file on the server and redeploy.

What if code changes don't appear after deployment?

This usually means GitLab's cache is stale. Solutions:

Should I use GitLab shared runners or install my own?

Shared Runners: Easier setup, but requires SSH from GitLab to your server.
Self-hosted Runner: More control, no external SSH needed, but requires runner installation on your server.

For production deployments, self-hosted runners on the same server are recommended for security and speed.

Best Practices

Branch Protection

Testing Before Deployment

Deployment Timing

Resource Management

Security

Why GitLab CI/CD Wins

For Developers

  • One Command: git push origin live - that's it!
  • Staging Review: Test on real server before production
  • Safe Approval: Manual gate before production switch
  • Full Visibility: See every deployment step in GitLab UI
  • Easy Rollback: Revert commit or re-run old pipeline

For Operations

  • Consistency: Every deployment follows same process
  • Audit Trail: Full history of who deployed what when
  • Zero Downtime: <1 second production switch
  • Automated Cleanup: Old releases removed automatically
  • Monitoring: Health checks at every stage
Bottom Line: GitLab CI/CD transforms deployment from manual, error-prone SSH sessions into a reliable, auditable, one-command process. Your team deploys faster, safer, and with confidence.

Next Steps

  1. โœ… Set up server with blue-green directory structure (see manual deployment guide )
  2. โœ… Install and configure GitLab Runner on your server
  3. โœ… Add staging subdomain to NGINX configuration
  4. โœ… Configure CI/CD variables in GitLab
  5. โœ… Create .gitlab-ci.yml in your repository
  6. โœ… Create and protect "live" branch
  7. โœ… Test with a small change: merge to live, verify staging, approve production
  8. โœ… Set up Slack notifications (optional)
  9. โœ… Document your deployment process for team members
Pro Tip: Start with manual deployments to understand the process, then gradually automate. GitLab CI/CD adds automation on top of the solid blue-green foundation.