Kushal Timilsina
5 min read

Configuring Automated Deployments with GitHub Actions

A practical guide to setting up GitHub Actions CI/CD pipelines for automated building, testing, and deployment of applications to Linux servers.

github-actionsci-cddevopsautomationdockerdeployment

Why Automate Deployments

Manual deployments are fragile, inconsistent, and time-consuming. A developer SSH-ing into a server, pulling changes, and restarting services is a routine that scales poorly and invites human error. A CI/CD pipeline automates this workflow: every push to a repository triggers a repeatable, auditable process that builds, tests, and deploys your application.

GitHub Actions is a strong choice for automation because it integrates natively with GitHub repositories, offers generous free-tier minutes, and supports self-hosted runners for private infrastructure. This guide walks through configuring a production-grade deployment pipeline.

Core Concepts

A GitHub Actions workflow is a YAML file stored in .github/workflows/ that defines one or more jobs triggered by events such as push, pull_request, or workflow_dispatch.

Event (e.g., push to main)
  └── Job 1: Build
        ├── Check out code
        ├── Install dependencies
        ├── Run tests
        └── Build Docker image
  └── Job 2: Deploy
        ├── Authenticate to server
        ├── Copy artefacts
        ├── Restart services
        └── Verify health

A Basic Build and Test Workflow

Every pipeline should start with building and testing your code. Here is a minimal Node.js example:

# .github/workflows/ci.yml
name: CI

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

jobs:
  build:
    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 test
      - run: npm run build

This workflow triggers on every push and pull request, ensuring broken code never reaches the main branch unnoticed.

Building and Publishing Docker Images

Once tests pass, the next step is to package the application. Docker images should be built, tagged, and pushed to a container registry:

docker-build:
  needs: build
  runs-on: ubuntu-latest

  steps:
    - uses: actions/checkout@v4

    - name: Log in to GitHub Container Registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        push: true
        tags: |
          ghcr.io/${{ github.repository }}:latest
          ghcr.io/${{ github.repository }}:${{ github.sha }}

Tagging with both latest and the commit SHA gives you the option to deploy a specific version or roll back to a known-good state.

Deploying to a Linux Server

For deployment to a VPS or bare-metal server, there are several approaches. I recommend SSH-based deployment with a lightweight script runner:

Approach 1: SSH Deploy with Appleboy

deploy:
  needs: docker-build
  runs-on: ubuntu-latest

  steps:
    - name: Deploy via SSH
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.DEPLOY_HOST }}
        username: ${{ secrets.DEPLOY_USER }}
        key: ${{ secrets.DEPLOY_KEY }}
        script: |
          cd /opt/applications/my-app
          docker compose pull
          docker compose up -d --remove-orphans
          docker image prune -f

This action SSHes into the server, pulls the latest Docker images, restarts services, and cleans up unused images.

Approach 2: Self-Hosted Runner

For environments where SSH access is restricted or you need tighter integration, register a self-hosted runner on your server:

# On the target server
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux.tar.gz \
  -L https://github.com/actions/runner/releases/download/v2.320.0/actions-runner-linux-x64-2.320.0.tar.gz
tar xzf actions-runner-linux.tar.gz
./config.sh --url https://github.com/your-org/your-repo --token <registration-token>
sudo ./svc.sh install && sudo ./svc.sh start

Then reference it in your workflow:

jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: docker compose -f docker-compose.prod.yml up -d --build

Environment-Specific Configuration

A robust pipeline handles multiple environments (staging, production) through GitHub Environments and secrets:

deploy-staging:
  environment: staging
  runs-on: ubuntu-latest
  steps:
    - run: echo "Deploying to staging"
    - run: ./deploy.sh ${{ vars.STAGING_HOST }}

deploy-production:
  environment: production
  needs: deploy-staging
  runs-on: ubuntu-latest
  steps:
    - run: echo "Deploying to production"
    - run: ./deploy.sh ${{ vars.PROD_HOST }}

GitHub Environments support approval gates, secret isolation, and deployment branch policies — critical for production safety.

Monitoring Pipeline Health

A deployment is not complete until you verify it succeeded. Add health-check steps:

- name: Health Check
  run: |
    for i in {1..30}; do
      curl -sf http://${{ secrets.DEPLOY_HOST }}/health && break
      sleep 5
    done

This polls the application's health endpoint with a timeout. If the service does not respond within 30 retries (2.5 minutes), the pipeline fails and a rollback should be triggered.

Secrets Management

Never hardcode credentials in workflow files. GitHub provides:

  • Repository secrets — Available to all workflows in the repository
  • Environment secrets — Scoped to a specific environment
  • Organization secrets — Shared across repositories
  • OpenID Connect — For cloud provider authentication without static keys

For AWS deployments, use OIDC instead of long-lived access keys:

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
    aws-region: ap-south-1

Common Pitfalls

  1. Missing cache configuration — Without dependency caching, every build downloads all packages from scratch. Use actions/cache or the built-in caching in setup actions to reduce build times by 60–80%.

  2. Overly permissive secrets — Repository secrets are accessible to all workflows, including those triggered by pull requests from forks. Use environment secrets for sensitive production credentials.

  3. No rollback strategy — A failed deployment should not leave the application in an undefined state. Docker Compose with --remove-orphans and previous image tags enables quick rollbacks.

  4. Ignoring workflow run concurrency — Multiple pushes in quick succession can cause conflicting deployments. Set concurrency: deploy-production in your workflow to cancel in-progress runs.

Conclusion

GitHub Actions provides a powerful, integrated CI/CD platform that works well for small teams and enterprise organisations alike. Start with a simple build-and-test workflow, add Docker image publishing, then layer in SSH-based deployment. Each stage adds reliability and repeatability to your release process, reducing the time between committing code and shipping it to production.