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

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
    

## 1\. **The Setup**

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:

```json
/var/www/myapp
```

and your app runs using something like:

```json
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!

---

## 2\. **Generate SSH Key for GitHub Action**

On your **local machine** (not the server):

```json
ssh-keygen -t ed25519 -C "github-deploy-key"
```

This creates:

```json
~/.ssh/id_ed25519      (private key)
~/.ssh/id_ed25519.pub  (public key)
```

### Copy the **public key** to your VM:

```json
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip
```

Test:

```
ssh user@server_ip
```

## 3\. **Add Private Key to GitHub Secrets**

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 |

---

## 4\. **Add CI/CD Workflow**

Create file:

```json
.github/workflows/deploy.yml
```

```json
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 install` your app’s commands if needed.

---

## 5\. **Push to Main → Deployment Happens**

Now try:

```json
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.

---

## Why This Matters!

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.**

---

## Closing Thoughts

You don’t need Kubernetes, Helm, ArgoCD, or GitOps to start doing DevOps.

Sometimes, a **40-line GitHub Action** is all you need.
