Skip to main content
Back to Category

Automated Deployment with GitHub Actions: CI/CD Pipeline Setup

Build CI/CD pipelines with GitHub Actions: Docker build and push, SSH server deployment, test automation, secrets management, and workflow optimization.

Read time: 18 min DevOps & Automation
github-actionsci/cddeploymentdevopsautomationdockerssh

Table of Contents

Automated Deployment with GitHub Actions: CI/CD Pipeline Setup

GitHub Actions is a powerful CI/CD platform that automatically tests, builds, and deploys code changes to your server. In this guide we'll build a production-ready pipeline from scratch.

What is GitHub Actions?

GitHub Actions is an automation platform integrated into GitHub repositories:

  • Reacts to events like push, PR, and schedule
  • Runs parallel and sequential workflows
  • 2,000 minutes/month free (unlimited for public repos)
  • Extensible with thousands of ready-made actions

GitHub Actions workflow files are stored in YAML format in the .github/workflows/ directory.

Basic Workflow Structure

hljs yaml
# .github/workflows/deploy.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: echo "Deploy steps here"

on / jobs / steps Structure

Triggers (on)

hljs yaml
on:
  # Push to branch
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'package.json'

  # Pull request
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

  # Scheduled run (cron)
  schedule:
    - cron: '0 2 * * *'  # Every night at 02:00

  # Manual trigger
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

  # When another workflow completes
  workflow_run:
    workflows: ["Build"]
    types: [completed]

Job Configuration

hljs yaml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      NODE_ENV: test
    timeout-minutes: 30
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

Docker Build and Push

Push to Docker Hub

hljs yaml
jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: username/myapp
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=sha-

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

GitHub Container Registry (GHCR)

hljs yaml
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push (GHCR)
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

SSH Deployment to Server

Using appleboy/ssh-action

hljs yaml
  deploy:
    needs: docker
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SERVER_PORT }}
          script: |
            cd /root/myapp
            git pull origin main
            docker compose pull
            docker compose up -d --remove-orphans
            docker image prune -f
            echo "Deploy completed: $(date)"

Deployment with Docker Compose

hljs yaml
      - name: Docker Compose deploy
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            # Pull new image
            docker pull ghcr.io/${{ github.repository }}:latest

            # Zero-downtime deploy
            docker compose -f /root/app/docker-compose.yml up -d \
              --no-deps \
              --build \
              --remove-orphans

            # Clean old images
            docker image prune -af --filter "until=24h"

            # Health check
            sleep 10
            curl -f http://localhost:3000/health || exit 1

Secrets Management

GitHub Secrets securely stores sensitive information:

hljs yaml
steps:
  - name: Application configuration
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      JWT_SECRET: ${{ secrets.JWT_SECRET }}
      API_KEY: ${{ secrets.API_KEY }}
    run: |
      echo "Configuration loaded"
      # Secrets are never printed to logs, automatically masked

Required secrets:

  • SERVER_HOST: Server IP address
  • SERVER_USER: SSH username
  • SSH_PRIVATE_KEY: SSH private key
  • DOCKERHUB_USERNAME: Docker Hub username
  • DOCKERHUB_TOKEN: Docker Hub access token

Cache Usage

hljs yaml
      - name: Cache Node.js dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: npm ci (cached)
        run: npm ci

      # Docker layer cache
      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          cache-from: type=gha
          cache-to: type=gha,mode=max

Matrix Builds

hljs yaml
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        node: [18, 20]
        os: [ubuntu-latest, macos-latest]
        include:
          - node: 20
            os: ubuntu-latest
            coverage: true
        exclude:
          - node: 18
            os: macos-latest
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test
      - name: Coverage report
        if: matrix.coverage
        run: npm run coverage

Environment Protection Rules

hljs yaml
jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - name: Production deploy
        run: echo "Deploying to production"

In GitHub under Settings > Environments > production:

  • Required reviewers (require approval)
  • Wait timer (waiting period)
  • Deployment branches (main only)

Full CI/CD Pipeline Example

hljs yaml
name: Full CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage
      - name: Upload coverage
        uses: codecov/codecov-action@v4

  build:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
      - uses: docker/build-push-action@v5
        with:
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
            docker compose -f /root/app/docker-compose.yml up -d
            docker image prune -f

Conclusion

By setting up a fully automated CI/CD pipeline with GitHub Actions, you can improve code quality, reduce deployment errors, and speed up your development process. With secrets management, cache optimization, and environment protection rules, you get a production-grade pipeline.