GitHub Actions automates Linux server deployments via SSH using encrypted secrets for credentials. You configure a YAML workflow triggered by a push to the main branch, execute remote commands, and manage automatic rollback if the operation fails, eliminating manual errors. Deploying manually to 12 servers every week means copying, pasting, and hoping everything works. One Friday afternoon, I uploaded the wrong configuration file to 4 production servers before I realized the mistake. The downtime lasted 40 minutes. GitHub Actions with an approval gate would have stopped the deployment before the file reached the servers. According to the Puppet State of DevOps Report 2024, 45% of production incidents stem from manual configuration errors. I configured my first automated workflow in 20 minutes and never did a manual deployment on those servers again. You will learn to set up secure SSH deployments, configure environment protection rules, and implement instant rollbacks.
Prerequisites for GitHub Actions Deploy
Before you start, you need a Linux server accessible via SSH and a GitHub repository with the code to deploy. On the server, install git and configure the application. I use Node.js for this guide, but the workflow applies to any stack. On your local machine, install git and generate an SSH key dedicated to the deployment.
Workflow Structure and Secrets Management
A workflow is a YAML file inside the .github/workflows/ directory of your repository. GitHub executes it automatically when the event specified in the on block occurs. The jobs block contains the steps executed on a temporary runner, an Ubuntu container that GitHub creates, uses, and destroys in minutes. Each step inside the job executes an action or a shell command.
name: Deploy Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@master
The runner downloads the code with actions/checkout, then uses appleboy/ssh-action to connect to your Linux server and execute the deployment commands.
Never write passwords or private keys in code. The Verizon DBIR 2025 reports that 73% of breaches start from stolen credentials, often hardcoded in configuration files. GitHub solves this problem with secrets. Go to Settings > Secrets and variables > Actions in your repository and create three secrets:
SERVER_IP: the public IP address of your Linux server.SERVER_USER: the SSH user for the deployment, for exampledeployuser.SSH_KEY: the dedicated SSH private key.
Generate the key on your local machine:
ssh-keygen -t ed25519 -C "github-deploy" -f ~/.ssh/github_deploy_key
Copy the content of the github_deploy_key.pub file into the ~/.ssh/authorized_keys file on the Linux server. Copy the content of the github_deploy_key file, which is the private key, into the SSH_KEY secret on GitHub. The runner can now authenticate without a password.
Deployment Strategies and Rollback Mechanisms
The basic workflow connects the runner to the server and executes remote commands. When you push to the main branch, GitHub creates the runner, reads the secrets, connects to the server, and executes the commands defined in script. If a command fails, the workflow stops and GitHub marks the job as an error.
# .github/workflows/deploy.yml
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy Node.js via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/nodeapp
git pull origin main
npm install --production
npm run build
pm2 restart nodeapp --update-env
pm2 save
The runner pulls the code, installs dependencies without development packages, compiles the code, and restarts the process with pm2. The --update-env flag refreshes the environment variables. Organizations that adopt CI/CD reduce deployment times by 60% and production failures by 30%, according to the DORA State of DevOps 2025 report.
For static sites, you do not need git pull on the server. Compile locally in the runner and transfer the files with scp.
jobs:
deploy-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build static site
run: npm install && npm run build
- name: Transfer files via SSH
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
source: "dist/*"
target: "/var/www/html"
The appleboy/scp-action copies the compiled files from the dist folder directly into the webroot of the server, replacing the old files.
If the deployment fails, you must return to the previous version in seconds. Use a strategy based on symlinks. Maintain two folders: current, which is a symlink to the active version, and releases, containing numbered folders for each version.
- name: Rollback on failure
if: failure()
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
ln -sfn /var/www/releases/previous /var/www/current
pm2 restart nodeapp
echo "Rollback executed"
The if: failure() block activates this step only if one of the previous steps fails. The symlink points to the last stable version. The service restarts with the working code.
You can also receive an alert when the deployment finishes. Configure a Telegram bot and use curl in the workflow.
- name: Notify Telegram on success
if: success()
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage \n -d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \n -d text="Deploy completed on ${{ secrets.SERVER_IP }}"
Add TELEGRAM_TOKEN and TELEGRAM_CHAT_ID to your repository secrets. For failures, duplicate the step with if: failure() and change the alert text.
To add an extra layer of control, use environment protection rules. The on: push: branches: [main] block limits the trigger to the main branch. Create an environment called production in Settings > Environments. Add a required reviewer, a colleague who must approve the deployment before the runner starts.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
When you push to main, GitHub stops the workflow and sends a notification to the reviewer. The reviewer clicks Approve. Only then does the runner connect to the server. This approval gate would have stopped that Friday afternoon mistake, preventing the faulty configuration from reaching the 4 servers. You can read more about these protection rules in the official GitHub Actions documentation.
Common Errors and Troubleshooting
Permission denied (publickey): The runner cannot access the server. Verify that the public key is in the authorized_keys file of the remote user. Ensure the private key in the SSH_KEY secret includes the entire content of the file, including the final line.
Host key verification failed: The runner does not recognize the server fingerprint. Add script_stop: true and configure debug: true in appleboy/ssh-action to view the complete SSH connection output.
Secrets not found: The workflow returns errors about variables. Verify that the names of the secrets in Settings match exactly those in the YAML. A different character makes the secret invisible to the runner.
Long commands hanging: SSH closes the connection if a command produces no output for a long time. Add timeout options or divide the commands into separate steps.
Generating SSH keys without a password: Run ssh-keygen -t ed25519 -C "github-deploy" and press Enter when prompted for a password. A key without a passphrase allows the runner to authenticate automatically. Limit this key only to deployment on a user with reduced permissions.
Deploying to multiple servers simultaneously: Use the GitHub Actions matrix strategy to define a list of servers. The runner creates parallel jobs, one for each IP, and executes the deployment on all servers at the same moment.
GitHub Actions pricing for private projects: GitHub offers 2,000 free minutes per month for free accounts and 3,000 for Pro accounts. An SSH deployment workflow takes about 2 minutes. You can run approximately 1,000 deployments per month without additional costs.
Conclusions with Operational Takeaways
- Replace manual deployment with GitHub Actions. The YAML workflow eliminates copy and paste, reducing human errors by 45%.
- Use secrets for SSH credentials. No private keys in the repository, no plaintext passwords in logs.
- Add automatic rollback and approval gates. If the deployment fails, you return to the stable version. If the code is wrong, a colleague stops it before it reaches production.
Read also: Docker Deployment: First Production Container 2026
Read also: Setting Up a Linux VPS from Scratch: The 2026 Checklist
Read also: WireGuard VPN Linux: Configure in 15 Minutes 2026