Why automate deployment
The manual flow — build locally, scp the archive, SSH in, unzip, restart — is slow, error-prone and untraceable. Handing deployment to CI/CD (such as GitHub Actions) means every push automatically runs tests, builds, uploads and restarts, with full logs and easy rollback. This article demonstrates auto-deploying an app to a DMIT cloud server using GitHub Actions + rsync.
Choosing an approach
| Approach | Fits | Traits |
|---|---|---|
| rsync + SSH | Most web apps | Simple, incremental transfer, no extra server components |
| Git pull + hook | Small projects, internal tools | Server pulls code; keys live on the server |
| Docker image + Compose | Containerised apps | Consistent environments; pairs with Docker Deployment |
Setup: dedicated deploy user and key
Never use root from CI. Create a restricted deploy user:
# on the server
adduser --disabled-password deploy
mkdir -p /home/deploy/.ssh
# write the public key into authorized_keys (see below)
chmod 700 /home/deploy/.ssh
chown -R deploy:deploy /home/deploy/.ssh
# app directory ownership
chown -R deploy:deploy /var/www/myapp
In GitHub → Settings → Secrets add: SSH_PRIVATE_KEY, SSH_HOST, SSH_USER. The private key exists only as a CI secret, never in the repo.
GitHub Actions workflow
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm ci && npm run build
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Rsync to server
run: rsync -avz --delete ./dist/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/myapp/
- name: Restart service
run: ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} 'sudo systemctl restart myapp'
ssh-keyscan records the host fingerprint on first connect; for production, pin the fingerprint in known_hosts to prevent MITM.
Security and rollback
- Least-privilege sudo: allow deploy to run only one command:
# /etc/sudoers.d/deploy deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp - Atomic releases: rsync into
/var/www/myapp-releases/<timestamp>/, flip thecurrentsymlink — one command rolls back - Keep the last 5 releases, prune older ones
- Rotate keys periodically; if leaked, deleting the public key on the server revokes access instantly
Common issues
Permission denied (publickey)?
Check server-side ~/.ssh permissions (700/600), authorized_keys ownership, and that the Actions secret contains the full key including header/footer lines.
rsync hangs or times out?
Confirm the firewall allows port 22 from CI runners (their IPs are not fixed), and review your baseline in the Security Checklist.
Deployed but service unchanged?
Verify the restart step targets the right service name; confirm via systemctl status myapp and journalctl -u myapp -f (more in Troubleshooting Guide).
Related articles
Docker Deployment Guide, Security Checklist, First Deploy Checklist.
Data checked: 2026-08-18. Actions syntax follows official GitHub documentation.