Skip to content
Deployment Automation — CI/CD Pipelines, Git Hooks, Docker, Ansible, Rolling Updates, and Rollbacks

Deployment Automation — CI/CD Pipelines, Git Hooks, Docker, Ansible, Rolling Updates, and Rollbacks

DodaTech Updated Jun 20, 2026 11 min read

Manual deployments are error-prone, slow, and stressful. Automation ensures every deployment follows the same reliable process — build, test, deploy, and verify. This guide covers CI/CD pipeline design with GitHub Actions and GitLab CI, Git hooks for pre-deployment validation, Docker container deployment strategies, Ansible playbooks for server provisioning, zero-downtime rolling updates, and automated rollback when things go wrong.

What You’ll Learn

You’ll design CI/CD pipelines that build, test, and deploy automatically, use Git hooks to prevent bad commits from reaching production, deploy Docker containers with zero downtime, provision servers with Ansible playbooks, implement rolling updates with health checks, and build automated rollback strategies. DodaZIP and Durga Antivirus Pro use automated deployment pipelines for continuous delivery of updates and security patches.

Deployment Automation Path

    flowchart LR
  A[Version Control] --> B[CI/CD Pipeline]
  B --> C[Git Hooks]
  C --> D[Docker Deploy]
  D --> E[Ansible Provisioning]
  E --> F[Rolling Updates]
  F --> G[Rollbacks]
  G --> H[Deployment Automation<br/>You are here]
  style H fill:#f90,color:#fff
  

CI/CD Pipeline with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]
  workflow_dispatch:  # manual trigger

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
      - run: npm run lint

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/

      - name: Deploy to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          source: "dist/"
          target: "/var/www/app/releases/${{ github.sha }}"

      - name: Activate new release
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /var/www/app
            ln -sfn releases/${{ github.sha }} current
            systemctl reload nginx
            curl -f http://localhost/health || exit 1
            echo "Deploy successful: ${{ github.sha }}"

GitLab CI Equivalent

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test
    - npm run lint

build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add openssh-client curl
  script:
    - scp -r dist/ user@server:/var/www/app/releases/$CI_COMMIT_SHA/
    - ssh user@server "
        cd /var/www/app &&
        ln -sfn releases/$CI_COMMIT_SHA current &&
        systemctl reload nginx &&
        curl -f http://localhost/health
      "
  only:
    - main

Git Hooks for Pre-Deployment Checks

#!/bin/bash
# .git/hooks/pre-push — Prevent pushing to main without tests

BRANCH=$(git symbolic-ref HEAD 2>/dev/null | sed 's|refs/heads/||')

if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "production" ]; then
    echo "→ Running pre-push checks for $BRANCH..."

    # Run tests
    npm test 2>/dev/null || {
        echo "✗ Tests failed. Push aborted."
        exit 1
    }

    # Check for TODO/FIXME
    if git diff --cached | grep -q "TODO\|FIXME"; then
        echo "⚠ Warning: Unresolved TODO/FIXME in staged changes"
        read -p "Continue pushing? (y/N) " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            exit 1
        fi
    fi

    # Ensure CHANGELOG is updated
    if ! git diff --cached --name-only | grep -q "CHANGELOG"; then
        echo "⚠ Warning: CHANGELOG not updated"
        read -p "Continue pushing? (y/N) " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            exit 1
        fi
    fi

    echo "✓ Pre-push checks passed"
fi
#!/bin/bash
# .git/hooks/post-merge — Auto-deploy after pull on production

CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
if [ "$CURRENT_BRANCH" = "production" ]; then
    echo "→ Production branch updated. Running deploy..."

    # Rebuild
    npm ci && npm run build

    # Reload service
    systemctl reload nginx

    # Verify
    if curl -f http://localhost/health; then
        echo "✓ Auto-deploy successful"
    else
        echo "✗ Health check failed — check logs"
        exit 1
    fi
fi
# Enable hooks project-wide (shared with team)
git config core.hooksPath .githooks

Docker Deployment

# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget -qO- http://localhost/health || exit 1
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    image: registry.example.com/app:${DEPLOY_TAG:-latest}
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: any

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app

Rolling Update with Docker

#!/bin/bash
# docker-deploy.sh — Zero-downtime Docker deployment

set -euo pipefail

REGISTRY="registry.example.com"
SERVICE="app"
TAG="${1:-$(git rev-parse --short HEAD)}"
STACK_NAME="production"

echo "=== Deploying $SERVICE:$TAG ==="

# 1. Build and push
echo "[1/4] Building image..."
docker build -t "$REGISTRY/$SERVICE:$TAG" .
docker push "$REGISTRY/$SERVICE:$TAG"

# 2. Deploy with rolling update
echo "[2/4] Deploying..."
docker service update \
    --image "$REGISTRY/$SERVICE:$TAG" \
    --update-parallelism 1 \
    --update-delay 10s \
    --update-order start-first \
    --update-failure-action rollback \
    "${STACK_NAME}_${SERVICE}"

# 3. Monitor deployment
echo "[3/4] Monitoring rollout..."
sleep 5
docker service ps "${STACK_NAME}_${SERVICE}" \
    --format "table {{.Name}}\t{{.CurrentState}}\t{{.Error}}"

# 4. Verify health
echo "[4/4] Verifying health..."
for i in $(seq 1 10); do
    if curl -sf http://localhost/health > /dev/null 2>&1; then
        echo "✓ Deployment successful: $TAG"
        exit 0
    fi
    echo "  Waiting... ($i/10)"
    sleep 3
done

echo "✗ Health check failed — initiating rollback..."
docker service rollback "${STACK_NAME}_${SERVICE}"
exit 1

Ansible Provisioning

# playbooks/deploy.yml
---
- name: Provision and deploy web application
  hosts: web-servers
  become: yes
  vars:
    app_name: dodatech-app
    app_directory: /var/www/{{ app_name }}
    node_version: "20.x"

  tasks:
    - name: Install system dependencies
      apt:
        name:
          - nginx
          - certbot
          - nodejs
          - git
        state: present
        update_cache: yes

    - name: Create app directory
      file:
        path: "{{ app_directory }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'

    - name: Deploy application code
      synchronize:
        src: ./dist/
        dest: "{{ app_directory }}/current/"
        delete: yes
      notify: reload nginx

    - name: Configure NGINX site
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ app_name }}
      notify: reload nginx

    - name: Enable site
      file:
        src: /etc/nginx/sites-available/{{ app_name }}
        dest: /etc/nginx/sites-enabled/{{ app_name }}
        state: link
      notify: reload nginx

    - name: Set up SSL
      command: >
        certbot --nginx -d {{ inventory_hostname }}
        --non-interactive --agree-tos -m admin@example.com
      when: ssl_enabled | default(true)
      notify: reload nginx

  handlers:
    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded

Rolling Update Strategies

# NGINX — active-passive (blue-green) with health checks
upstream app {
    # Blue (current)
    server 10.0.1.10:3000 weight=1;
    # Green (staging)
    # server 10.0.1.11:3000 weight=0;

    # Health check
    zone backend 64k;
    health_check interval=5s fails=3 passes=2 uri=/health;
}

server {
    location / {
        proxy_pass http://app;
        proxy_set_header Host $host;
    }
}

Blue-Green Deploy Script

#!/bin/bash
# blue-green-deploy.sh

GREEN="10.0.1.11"
BLUE="10.0.1.10"

echo "=== Blue-Green Deploy ==="
echo "Current active: BLUE ($BLUE)"

# Step 1: Deploy to green
echo "[1/4] Deploying to GREEN ($GREEN)..."
ssh deploy@$GREEN 'bash -s' < deploy-app.sh

# Step 2: Test green
echo "[2/4] Testing GREEN..."
for i in $(seq 1 5); do
    if curl -sf "http://$GREEN/health" > /dev/null; then
        echo "  GREEN health check passed"
        break
    fi
    echo "  Waiting... ($i/5)"
    sleep 3
done

# Step 3: Switch traffic
echo "[3/4] Switching traffic to GREEN..."
ssh load-balancer 'sed -i "s/server '"$BLUE"':3000;/# server '"$BLUE"':3000;/" /etc/nginx/conf.d/app.conf && sed -i "s/# server '"$GREEN"':3000;/server '"$GREEN"':3000;/" /etc/nginx/conf.d/app.conf && nginx -t && systemctl reload nginx'

# Step 4: Verify
echo "[4/4] Verifying..."
if curl -sf http://example.com/health; then
    echo "✓ Blue-green switch successful"
    echo "  Active: GREEN ($GREEN)"
    echo "  Idle: BLUE ($BLUE)"
else
    echo "✗ Switch failed — rolling back"
    # ... rollback logic
fi

Rollback Strategies

#!/bin/bash
# rollback.sh — Automated rollback to previous release

RELEASE_DIR="/var/www/app/releases"
CURRENT_LINK="/var/www/app/current"
HEALTH_URL="http://localhost/health"

get_previous_release() {
    ls -t "$RELEASE_DIR" | sed -n '2p'
}

rollback() {
    local target_release="$1"
    echo "=== Rolling back to: $target_release ==="

    # Switch symlink
    ln -sfn "$RELEASE_DIR/$target_release" "$CURRENT_LINK"

    # Reload service
    systemctl reload nginx

    # Verify
    if curl -sf "$HEALTH_URL"; then
        echo "✓ Rollback to $target_release successful"
        return 0
    else
        echo "✗ Rollback failed — rolling forward..."
        ln -sfn "$(ls -t "$RELEASE_DIR" | sed -n '1p')" "$CURRENT_LINK"
        systemctl reload nginx
        return 1
    fi
}

# Auto-rollback on deploy failure
if ! curl -sf "$HEALTH_URL"; then
    previous=$(get_previous_release)
    if [ -n "$previous" ]; then
        echo "Deploy failed — auto-rolling back to $previous"
        rollback "$previous"
    else
        echo "No previous release found — manual intervention required"
        exit 1
    fi
fi

Common Errors

1. Pipeline Fails Silently

CI/CD steps must check exit codes. Missing set -e in shell scripts means failures are ignored. Always use exit 1 on failure and configure pipeline to fail on any non-zero exit.

2. Rolling Update Kills All Instances

Without --update-parallelism 1 and --update-order start-first, Docker replaces all containers simultaneously, causing downtime. Always roll one instance at a time.

3. Database Migrations Without Rollback Plan

Irreversible migrations (DROP COLUMN) break rollback. Always make migrations reversible. Use down migrations: migrate -database $DB -path migrations down 1.

4. Secrets in Git History

Committing .env or API keys exposes credentials. Use .gitignore and git-secrets scanning. Rotate any keys that have been committed. Tools like git-filter-repo can purge history.

5. Health Check Not Representative

A health check that only returns 200 without testing the database or cache can pass while the app is broken. Health checks must verify actual functionality.

6. Canary Release Without Monitoring

Deploying to a subset of users without monitoring response times, error rates, and business metrics means you won’t detect problems until all users are affected.

7. Infrequent Deployments

Deploying rarely (monthly) means each release is large and risky. Frequent small deployments reduce risk — each change is smaller, rollback is easier, and the team gets faster feedback.

Practice Questions

1. What is the difference between blue-green and rolling deployment? Blue-green: two identical environments, switch traffic instantly. Rolling: gradually replace instances one by one. Blue-green has faster switchover; rolling uses less resources.

2. How do you perform a zero-downtime database migration? Use expand-migrate-contract: add new columns (expand), deploy code that uses both old and new columns, backfill data (migrate), deploy code that uses only new columns, remove old columns (contract).

3. What is the purpose of health checks in deployment? Health checks verify the new instance is serving traffic correctly before routing users to it. Failed health checks trigger automatic rollback, preventing broken deployments from reaching users.

4. How do you handle secrets in CI/CD pipelines? Use the CI/CD platform’s secret store (GitHub Secrets, GitLab CI Variables). Inject at runtime via environment variables. Never commit secrets to repositories.

5. Challenge: Design a deployment pipeline for a microservices architecture with 10 services that must be deployed in dependency order with coordinated rollbacks. Answer: Use a deployment orchestration tool (ArgoCD, Spinnaker, or a custom pipeline). Define a DAG of service dependencies. Deploy in topological order: databases first, then services with no dependents, then services that depend on them. On any failure, roll back the current and all downstream services.

Mini Project: Complete Deploy Pipeline

#!/bin/bash
# complete-deploy.sh — End-to-end deployment

set -euo pipefail

APP_NAME="${1:-myapp}"
TAG="${2:-$(git rev-parse --short HEAD)}"
SERVERS=("web1.example.com" "web2.example.com")

echo "=== Complete Deploy: $APP_NAME @ $TAG ==="

# 1. Pre-deploy checks
echo "[1/7] Pre-deploy checks..."
npm ci && npm test && npm run build
echo "  ✓ Tests passed, build generated"

# 2. Tag release in git
echo "[2/7] Tagging release..."
git tag "v$TAG" && git push origin "v$TAG"

# 3. Build Docker image
echo "[3/7] Building Docker image..."
docker build -t "registry.example.com/$APP_NAME:$TAG" .
docker push "registry.example.com/$APP_NAME:$TAG"

# 4. Run DB migrations
echo "[4/7] Running migrations..."
ssh deploy@${SERVERS[0]} "cd /var/www/$APP_NAME && npm run migrate up"

# 5. Deploy to servers
echo "[5/7] Deploying..."
for SERVER in "${SERVERS[@]}"; do
    echo "  Deploying to $SERVER..."
    ssh deploy@$SERVER "
        docker pull registry.example.com/$APP_NAME:$TAG
        docker service update --image registry.example.com/$APP_NAME:$TAG ${APP_NAME}_app
    "
    echo "  ✓ $SERVER deployed"
done

# 6. Verify health
echo "[6/7] Verifying health..."
for SERVER in "${SERVERS[@]}"; do
    for i in $(seq 1 10); do
        if curl -sf "https://$SERVER/health"; then
            echo "  ✓ $SERVER healthy"
            break
        fi
        sleep 3
    done
done

# 7. Post-deploy
echo "[7/7] Post-deploy tasks..."
# Notify monitoring, update changelog, etc.
echo "  ✓ Deployment complete: $TAG"
echo "  Dashboard: https://status.example.com/deployments"

FAQ

What is the best CI/CD platform?
GitHub Actions is excellent for GitHub-hosted code. GitLab CI is tightly integrated with GitLab. Jenkins offers maximum flexibility. Choose based on your git hosting and team expertise.
How do I handle environment-specific configuration?
Use environment variables (not config files in the repo). Inject them at deploy time from a secrets manager (Vault, AWS Secrets Manager, GitHub Secrets).
What is the difference between canary and blue-green deployment?
Canary: route a small percentage of traffic to the new version, monitor, then ramp up. Blue-green: two full environments, switch all traffic at once. Canary is safer; blue-green is simpler.
How often should I deploy?
Aim for daily or multiple times per day. Each deployment should be small — a single feature or fix. Frequent small deployments reduce risk and make rollback straightforward.
Should I deploy on Fridays?
Avoid Friday deployments unless you have 24/7 on-call support. If something breaks, weekend recovery is stressful. Deploy early in the week during business hours.
How do I handle secrets rotation?
Use a secrets manager (HashiCorp Vault, AWS Secrets Manager) with automatic rotation. Applications should reload secrets on SIGHUP or poll for changes. Rotate credentials on every deploy if possible.

What’s Next

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro