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 viassh-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 populateSSH_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
, ordocker 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:
- Code is pushed to the
main
branch - 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.
- Create a Slack Incoming Webhook
- Add
SLACK_WEBHOOK
as a GitHub Secret - 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