CI/CD in 30 Minutes: Deploying to a VM Using GitHub Actions

DevOps & Cloud Engineer — building scalable, automated, and intelligent systems. Developer of sorts | Automator | Innovator
Search for a command to run...

DevOps & Cloud Engineer — building scalable, automated, and intelligent systems. Developer of sorts | Automator | Innovator
Artificial Intelligence has become one of the biggest talking points in software engineering. Every week, a new tool promises to automate deployments, fix production incidents, or replace developers a

You spin up a Kubernetes workload. You connect to PostgreSQL. You add sslmode=require because security matters. And then PostgreSQL replies with: server does not support SSL, but SSL was required Wai

When you first approach Tencent Cloud, it feels familiar if you come from AWS or Azure. There are VPCs, Kubernetes clusters, load balancers, object storage, IAM, everything you would expect. But once

When I first automated Docker builds and deployments, I thought the hard part would be writing the YAML. It was not. The real challenges were versioning, preventing accidental rollbacks, handling envi

For a long time, Docker Compose felt like the perfect solution. Simple YAML, fast local setup, predictable behavior. For a single service or even a small stack, it works beautifully. But at some point, reality catches up. As the application grew, tra...

CodeOps Studies
40 posts
Simple write-ups on day to day code or devops experiments, tests etc.
When I first heard the term CI/CD, it sounded like something only large companies use with complicated pipelines and enterprise tools. But CI/CD is simply this:
Whenever you push code, your app automatically builds, tests, and deploys.
No manual SSH into servers.
No git pull every time you change something.
In this post, we’ll set up a very clean CI/CD workflow using:
GitHub Actions (free)
A Linux VM on any cloud (can be DigitalOcean, AWS EC2, Proxmox VM, anything)
A simple application running on the VM
You should already have:
A VM with Linux (Ubuntu recommended)
Your app running (Node, Python, doesn’t matter)
SSH access working
Let’s assume your app directory on the server is:
/var/www/myapp
and your app runs using something like:
systemctl restart myapp
For the above, we often setup our service as a systemd service, perhaps, we will talk about deploying an app with systemd in a different article!
On your local machine (not the server):
ssh-keygen -t ed25519 -C "github-deploy-key"
This creates:
~/.ssh/id_ed25519 (private key)
~/.ssh/id_ed25519.pub (public key)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip
Test:
ssh user@server_ip
Go to your GitHub repo → Settings → Secrets → Actions → New Secret
| Secret Name | Value |
SSH_PRIVATE_KEY | contents of id_ed25519 |
SERVER_HOST | your server IP |
SERVER_USER | your server username |
Create file:
.github/workflows/deploy.yml
name: Deploy to VM
on:
push:
branches: ["main"]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
- name: Deploy to server
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} "
cd /var/www/myapp &&
git pull &&
npm install --production &&
pm2 restart myapp
"
Replace
npm installyour app’s commands if needed.
Now try:
git add .
git commit -m "Test CI/CD"
git push origin main
Open GitHub → Actions Tab → You’ll see deployment logs in real time.
Once it says: Success, refresh your app, it’s live.
This is continuous deployment in its simplest, cleanest form.
This setup:
Removes the “dev to server” friction
Makes deployments repeatable and reliable
Forces clean code structure
Helps you work like teams do in production
Even if you’re a solo developer today: your future teammates will thank you.
You don’t need Kubernetes, Helm, ArgoCD, or GitOps to start doing DevOps.
Sometimes, a 40-line GitHub Action is all you need.