CI/CD Pipeline Setup

9 min read
Updated Jun 19, 2025

CI/CD Pipeline Setup

Step-by-step guide to setting up production-ready CI/CD pipelines from scratch with security, testing, and deployment automation.

Pipeline Architecture

A well-architected CI/CD pipeline provides fast feedback, ensures quality, and enables safe, frequent deployments.

Core Components

  • Version Control: Git-based source control
  • CI Server: Build and test automation
  • Artifact Repository: Store build artifacts
  • Container Registry: Docker image storage
  • Deployment Platform: Target infrastructure
  • Monitoring: Pipeline and application metrics

Initial Setup

Repository Structure

project-root/
├── .github/workflows/      # GitHub Actions
├── .gitlab-ci.yml         # GitLab CI
├── Jenkinsfile           # Jenkins Pipeline
├── src/                  # Application source
├── tests/                # Test suites
├── scripts/              # Build/deploy scripts
├── k8s/                  # Kubernetes manifests
├── terraform/            # Infrastructure code
├── docker/               # Dockerfiles
├── docs/                 # Documentation
└── .gitignore

Branch Strategy

# Git Flow Configuration
git flow init

# Feature branch
git flow feature start new-feature
# Work on feature
git flow feature finish new-feature

# Release branch
git flow release start 1.0.0
# Prepare release
git flow release finish 1.0.0

# Hotfix
git flow hotfix start critical-fix
# Fix issue
git flow hotfix finish critical-fix

Jenkins Setup

Jenkins Installation

# Docker Compose for Jenkins
version: '3.8'
services:
  jenkins:
    image: jenkins/jenkins:lts-jdk11
    privileged: true
    user: root
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - JENKINS_OPTS="--prefix=/jenkins"
    restart: unless-stopped

  jenkins-agent:
    image: jenkins/inbound-agent
    privileged: true
    user: root
    environment:
      - JENKINS_URL=http://jenkins:8080/jenkins
      - JENKINS_AGENT_NAME=docker-agent
      - JENKINS_SECRET=${JENKINS_SECRET}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

volumes:
  jenkins_home:

Jenkins Configuration as Code

# jenkins.yaml
jenkins:
  systemMessage: "Jenkins CI/CD Server"
  securityRealm:
    local:
      allowsSignup: false
      users:
        - id: "admin"
          password: "${ADMIN_PASSWORD}"
  authorizationStrategy:
    globalMatrix:
      permissions:
        - "Overall/Read:anonymous"
        - "Overall/Administer:admin"
  
  clouds:
    - kubernetes:
        name: "kubernetes"
        serverUrl: "https://kubernetes.default"
        namespace: "jenkins"
        jenkinsUrl: "http://jenkins:8080"
        
credentials:
  system:
    domainCredentials:
      - credentials:
          - string:
              scope: GLOBAL
              id: "github-token"
              secret: "${GITHUB_TOKEN}"
          - usernamePassword:
              scope: GLOBAL
              id: "docker-hub"
              username: "${DOCKER_USERNAME}"
              password: "${DOCKER_PASSWORD}"

unclassified:
  location:
    url: "https://jenkins.example.com/"
  gitscm:
    globalConfigName: "Jenkins"
    globalConfigEmail: "[email protected]"

GitLab CI Setup

GitLab Runner Installation

# Install GitLab Runner on Kubernetes
helm repo add gitlab https://charts.gitlab.io
helm repo update

# Create values file
cat > gitlab-runner-values.yaml << EOF
gitlabUrl: https://gitlab.com/
runnerRegistrationToken: "YOUR_REGISTRATION_TOKEN"
concurrent: 10
checkInterval: 30

rbac:
  create: true
  serviceAccountName: gitlab-runner

runners:
  config: |
    [[runners]]
      [runners.kubernetes]
        image = "ubuntu:20.04"
        privileged = true
        [[runners.kubernetes.volumes.pvc]]
          name = "cache"
          mount_path = "/cache"
      [runners.cache]
        Type = "s3"
        Shared = true
        [runners.cache.s3]
          ServerAddress = "s3.amazonaws.com"
          BucketName = "gitlab-runner-cache"
          BucketLocation = "us-east-1"
EOF

# Install runner
helm install gitlab-runner gitlab/gitlab-runner \
  -f gitlab-runner-values.yaml \
  --namespace gitlab-runner \
  --create-namespace

Pipeline Templates

# .gitlab-ci.yml with reusable components
include:
  - project: 'devops/ci-templates'
    ref: main
    file: 
      - '/templates/security.yml'
      - '/templates/docker.yml'
      - '/templates/deploy.yml'

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_BUILDKIT: 1

.default_rules:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
      when: never
    - if: '$CI_COMMIT_BRANCH'

stages:
  - build
  - test
  - security
  - package
  - deploy

build:app:
  extends: .default_rules
  stage: build
  image: node:18
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
    policy: pull-push
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 day

GitHub Actions Setup

Self-Hosted Runners

# Kubernetes deployment for GitHub Actions runner
apiVersion: v1
kind: Namespace
metadata:
  name: actions-runner-system

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: github-runner
  namespace: actions-runner-system
spec:
  replicas: 3
  selector:
    matchLabels:
      app: github-runner
  template:
    metadata:
      labels:
        app: github-runner
    spec:
      containers:
      - name: runner
        image: summerwind/actions-runner:latest
        env:
        - name: RUNNER_NAME_PREFIX
          value: "k8s-runner"
        - name: RUNNER_TOKEN
          valueFrom:
            secretKeyRef:
              name: github-runner
              key: runner-token
        - name: RUNNER_REPOSITORY_URL
          value: "https://github.com/myorg/myrepo"
        - name: RUNNER_LABELS
          value: "self-hosted,linux,x64,kubernetes"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Reusable Workflows

# .github/workflows/reusable-build.yml
name: Reusable Build Workflow

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '18'
      environment:
        required: true
        type: string
    secrets:
      npm-token:
        required: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
    
    - name: Configure npm
      run: |
        echo "//registry.npmjs.org/:_authToken=${{ secrets.npm-token }}" > ~/.npmrc
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build
      run: npm run build:${{ inputs.environment }}
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build-${{ inputs.environment }}
        path: dist/
        retention-days: 7

# Usage in main workflow
name: CI/CD
on: [push, pull_request]

jobs:
  build-staging:
    uses: ./.github/workflows/reusable-build.yml
    with:
      environment: staging
    secrets:
      npm-token: ${{ secrets.NPM_TOKEN }}

Testing Pipeline

Test Stages Configuration

# Complete test pipeline
test:
  stage: test
  parallel:
    matrix:
      - TEST_SUITE: unit
        TEST_COMMAND: "npm run test:unit"
      - TEST_SUITE: integration
        TEST_COMMAND: "npm run test:integration"
        SERVICES: ["postgres:14", "redis:7"]
      - TEST_SUITE: e2e
        TEST_COMMAND: "npm run test:e2e"
        BROWSER: ["chrome", "firefox", "safari"]
  
  image: node:18
  services: $SERVICES
  
  before_script:
    - npm ci
    - |
      if [[ "$TEST_SUITE" == "e2e" ]]; then
        apt-get update && apt-get install -y chromium firefox-esr
        npm run build
        npm run preview &
        npx wait-on http://localhost:4173
      fi
  
  script:
    - $TEST_COMMAND
  
  artifacts:
    when: always
    reports:
      junit: test-results/junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - test-results/
      - coverage/
    expire_in: 30 days

Performance Testing

# k6 performance test integration
performance:
  stage: test
  image: loadimpact/k6:latest
  script:
    - k6 run --out json=performance.json tests/performance/load-test.js
  artifacts:
    reports:
      performance: performance.json
    expose_as: 'performance-report'
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

# k6 test script
import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
  stages: [
    { duration: '5m', target: 100 },
    { duration: '10m', target: 100 },
    { duration: '5m', target: 200 },
    { duration: '10m', target: 200 },
    { duration: '5m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed: ['rate<0.1'],
  },
};

export default function () {
  let response = http.get('https://staging.example.com/api/health');
  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
  sleep(1);
}

Security Pipeline

Security Scanning Integration

# Security scanning job
security:scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    # Scan dependencies
    - trivy fs --security-checks vuln --severity HIGH,CRITICAL .
    
    # Scan container image
    - trivy image --severity HIGH,CRITICAL ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
    
    # Generate reports
    - trivy fs --format template --template "@/contrib/gitlab.tpl" -o gl-dependency-scanning-report.json .
    - trivy image --format template --template "@/contrib/gitlab.tpl" -o gl-container-scanning-report.json ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
  
  artifacts:
    reports:
      dependency_scanning: gl-dependency-scanning-report.json
      container_scanning: gl-container-scanning-report.json

# SAST with multiple tools
security:sast:
  stage: security
  image: returntocorp/semgrep:latest
  script:
    - semgrep ci --json --output=semgrep-results.json
    - semgrep --config=auto --severity=ERROR --json --output=sast-report.json .
  artifacts:
    reports:
      sast: sast-report.json

Artifact Management

Nexus Repository Setup

# Docker compose for Nexus
version: '3.8'
services:
  nexus:
    image: sonatype/nexus3:latest
    ports:
      - "8081:8081"
      - "8082:8082"  # Docker registry
    volumes:
      - nexus-data:/nexus-data
    environment:
      - NEXUS_CONTEXT=nexus
    restart: unless-stopped

volumes:
  nexus-data:

# Configure in CI/CD
publish:artifacts:
  stage: package
  script:
    - mvn deploy -DaltDeploymentRepository=nexus::default::https://nexus.example.com/repository/maven-releases/
  only:
    - tags

Multi-Environment Deployment

Environment Configuration

# environments/staging/values.yaml
replicaCount: 2
image:
  repository: myapp
  pullPolicy: Always
  tag: ""  # Overridden by CI/CD

ingress:
  enabled: true
  hosts:
    - host: staging.example.com
      paths:
        - path: /
          pathType: Prefix

resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"

# environments/production/values.yaml
replicaCount: 5
image:
  repository: myapp
  pullPolicy: IfNotPresent
  tag: ""  # Overridden by CI/CD

ingress:
  enabled: true
  hosts:
    - host: example.com
      paths:
        - path: /
          pathType: Prefix

resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "1Gi"
    cpu: "1000m"

autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70

Progressive Deployment

# Argo Rollouts configuration
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: myapp-rollout
spec:
  replicas: 5
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: {duration: 10m}
      - setWeight: 40
      - pause: {duration: 10m}
      - setWeight: 60
      - pause: {duration: 10m}
      - setWeight: 80
      - pause: {duration: 10m}
      analysis:
        templates:
        - templateName: success-rate
        startingStep: 2
        args:
        - name: service-name
          value: myapp-canary
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080

Pipeline Monitoring

Metrics Collection

# Prometheus metrics for CI/CD
- job_name: 'jenkins'
  static_configs:
  - targets: ['jenkins:8080']
  metrics_path: '/prometheus'

- job_name: 'gitlab-runner'
  static_configs:
  - targets: ['gitlab-runner:9252']

# Grafana dashboard queries
# Build success rate
sum(rate(jenkins_job_build_result_total{result="SUCCESS"}[1h])) / sum(rate(jenkins_job_build_result_total[1h]))

# Average build duration
avg(jenkins_job_build_duration_seconds)

# Deployment frequency
sum(increase(deployments_total[1d]))

Troubleshooting Guide

Common Issues

Issue Cause Solution
Build timeout Resource constraints Increase runner resources
Flaky tests Race conditions Add retries, fix async issues
Deploy fails Permission issues Check service account roles
Slow pipeline No caching Implement dependency caching

Best Practices Checklist

  • □ Version control everything including pipeline code
  • □ Use pipeline as code (Jenkinsfile, .gitlab-ci.yml)
  • □ Implement comprehensive testing
  • □ Automate security scanning
  • □ Use artifact repositories
  • □ Implement proper secret management
  • □ Monitor pipeline metrics
  • □ Document pipeline processes
  • □ Regular pipeline maintenance
Note: This documentation is provided for reference purposes only. It reflects general best practices and industry-aligned guidelines, and any examples, claims, or recommendations are intended as illustrative—not definitive or binding.