Automate Server Deployments with GitHub Actions & SSH



This content originally appeared on DEV Community and was authored by Amol Mali

🚀 Automate Server Deployments with GitHub Actions & SSH

Modern development teams want code changes to move from GitHub to production without friction. In this guide, we’ll extend our CI/CD pipeline to automatically deploy to a server using SSH—no manual steps required.

This builds on the previous blog where we created a pipeline to build and push Docker images using GitHub Actions.

🧰 Prerequisites

Before getting started, ensure you have:

  • A GitHub account
  • A Linux server with SSH access (e.g. DigitalOcean, AWS EC2, VPS)
  • A Docker Hub account (if using Docker)
  • SSH key pair added to your server’s ~/.ssh/authorized_keys
  • GitHub repository secrets:
    • SSH_HOST # e.g., 192.168.1.100
    • SSH_USER # e.g., ubuntu
    • SSH_PRIVATE_KEY # No passphrase
    • SSH_KNOWN_HOSTS # Generated via ssh-keyscan <server-ip>

🔐 Security Tips

  • Generate SSH keys without passphrases specifically for GitHub Actions
  • Only add the private key to GitHub Secrets
  • Use ssh-keyscan your-server-ip to safely populate SSH_KNOWN_HOSTS

🔒 Avoid using root—prefer a user with limited sudo access if needed.

⚙ Why Use GitHub Actions for Deployment?

By adding SSH deployment to your existing pipeline, you:

  • Avoid manual scp, git pull, or docker run commands
  • Trigger deployments automatically on code changes
  • Keep all deployment logic under version control

🔄 CI/CD Workflow with SSH Deployment

Here’s how the full pipeline works:

  1. Code is pushed to the main branch
  2. GitHub Actions:
    • Builds the Python app
    • Pushes a Docker image to Docker Hub
    • SSHs into the server and deploys the latest Docker image

📋 Full GitHub Actions Workflow

name: CI/CD Pipeline

on:
push:
  branches:
    - main

jobs:
build:
  runs-on: ubuntu-latest

  steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'

    - name: Install dependencies
      run: |
        pip install -r requirements.txt

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

  steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Log in to Docker Hub
      run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin

    - name: Build and Push Docker Image
      run: |
        docker build -t ${{ secrets.DOCKER_USERNAME }}/python-cicd:${{ github.sha }} .
        docker push ${{ secrets.DOCKER_USERNAME }}/python-cicd:${{ github.sha }}

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

  steps:
    - name: Deploy to Server via SSH
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
        script: |
          docker pull ${{ secrets.DOCKER_USERNAME }}/python-cicd:${{ github.sha }}
          docker stop app || true
          docker rm app || true
          docker run -d --name app -p 3000:3000 ${{ secrets.DOCKER_USERNAME }}/python-cicd:${{ github.sha }}

✅ Testing & Troubleshooting

  • Use -v or -vvv in SSH scripts for verbose logs
  • Ensure the server user is in the docker group
  • Use act to test GitHub Actions locally

📈 Bonus: Make It Even Better

Here’s how you can upgrade your deployment pipeline:

🔄 Add Rollback Logic

If a deployment fails, you can roll back to a previous image by storing the old commit SHA or Docker tag and restarting the container:

docker run -d --name app -p 3000:3000 your-docker-user/python-cicd:previous-commit-sha

🌐 Blue-Green Deployments with Nginx

Avoid downtime by running two versions of your app (e.g., app-blue and app-green) and switching traffic using Nginx.

Example Nginx Config:

upstream backend {
    server 127.0.0.1:3001;  # app-blue
    # server 127.0.0.1:3002;  # app-green
}

server {
    listen 80;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

To switch versions:

  • Deploy app-green on port 3002
  • Update Nginx to point to 3002
  • Reload Nginx: sudo systemctl reload nginx
  • Remove old container if needed

In your GitHub Action SSH script:

docker run -d --name app-green -p 3002:3000 your-docker-user/python-cicd:${{ github.sha }}
sudo sed -i 's/3001/3002/' /etc/nginx/sites-available/default
sudo systemctl reload nginx
docker stop app-blue || true
docker rm app-blue || true

This enables zero-downtime deploys.

🔔 Send Deployment Alerts to Slack

Keep your team updated automatically using a Slack webhook.

  1. Create a Slack Incoming Webhook
  2. Add SLACK_WEBHOOK as a GitHub Secret
  3. Use this step in your GitHub Actions:
- name: Notify Slack
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    fields: repo,message,commit,author
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

You’ll get instant alerts on success, failure, or cancellation of deployments.

💬 Final Thoughts

Automating deployments with GitHub Actions and SSH is a game-changer for solo developers and teams alike. Once configured, every git push to your main branch becomes a full-cycle delivery—from source code to a live service.


This content originally appeared on DEV Community and was authored by Amol Mali