DEV Community

MysticMc
MysticMc

Posted on

⚙️ My first CI/CD pipeline: From git push to production in 10 minutes

Remember the old days?

You write code. You test it manually (maybe). You open FileZilla. You drag and drop files to a server. You pray.

Then someone yells: "Who deployed on a Friday?!"

Those days are dead.

CI/CD pipelines are the reason modern developers ship code 10x faster with 90% fewer bugs. And the best part? You can set one up in 10 minutes. For free.

Let me show you.


What is CI/CD? (The 30-second explanation)

CI = Continuous Integration – Every time you push code, automatically test it.

CD = Continuous Delivery/Deployment – If tests pass, automatically deploy it.

In practice:

git push origin main
   ↓
[Automatic] Run tests
   ↓
[Automatic] Build the app
   ↓
[Automatic] Deploy to server
   ↓
You get a notification: "✅ Deployment successful"
Enter fullscreen mode Exit fullscreen mode

You never touch a server. You never run scp or FTP. You just push code.


The 10-Minute Challenge

By the end of this guide, you will have:

· A simple Node.js/Express app (or your own project)
· A GitHub repository
· A GitHub Actions workflow that:
· Runs tests automatically
· Deploys to a free hosting service (Render.com – no credit card)

Total time: ~10 minutes. I'll wait.


What You'll Need (All Free)

Tool Why
GitHub account Hosts your code + runs CI/CD
Render.com account Free hosting (similar to Heroku)
A simple app We'll make one in 2 minutes

No credit card required. No servers to manage.


Step 1: The App (2 minutes)

Create a new folder and a simple Express app:

mkdir my-cicd-app
cd my-cicd-app
npm init -y
npm install express
Enter fullscreen mode Exit fullscreen mode

Create index.js:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({ message: 'CI/CD works! 🚀' });
});

app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

// A simple test endpoint
app.get('/add/:a/:b', (req, res) => {
  const a = parseInt(req.params.a);
  const b = parseInt(req.params.b);
  res.json({ result: a + b });
});

app.listen(PORT, () => {
  console.log(`App running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Create a test file test.js (we'll use Node's built-in assert):

const assert = require('assert');

// Simple tests that will run in CI
console.log('Running tests...');

// Test 1: Addition works
function add(a, b) { return a + b; }
assert.strictEqual(add(2, 3), 5);
console.log('✅ Test 1 passed');

// Test 2: String test
assert.strictEqual(typeof 'hello', 'string');
console.log('✅ Test 2 passed');

console.log('🎉 All tests passed!');
Enter fullscreen mode Exit fullscreen mode

Update package.json with a test script:

{
  "scripts": {
    "start": "node index.js",
    "test": "node test.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Test it locally:

npm test          # Should show "All tests passed!"
npm start         # Visit http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

Step 2: Push to GitHub (2 minutes)

Create a repository on GitHub (don't initialize with README – we have our own).

git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/YOUR_USERNAME/my-cicd-app.git
git branch -M main
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the CI/CD Pipeline (3 minutes)

This is where the magic happens. GitHub Actions reads a file in .github/workflows/ and runs it.

Create this folder and file:

mkdir -p .github/workflows
Enter fullscreen mode Exit fullscreen mode

Create .github/workflows/deploy.yml:

name: Deploy to Production

# When should this run?
on:
  push:
    branches: [ main ]  # Every push to main branch
  workflow_dispatch:    # Also allow manual trigger

# What environment variables do we need?
env:
  NODE_VERSION: '18'

# Jobs run in parallel by default
jobs:
  # JOB 1: Test
  test:
    name: Run Tests
    runs-on: ubuntu-latest  # Free Linux runner

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Install dependencies
        run: npm ci  # ci is faster and stricter than install

      - name: Run tests
        run: npm test

  # JOB 2: Deploy (only if tests passed)
  deploy:
    name: Deploy to Render
    runs-on: ubuntu-latest
    needs: test  # Wait for test job to succeed

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Deploy to Render
        uses: johnbeynon/render-deploy-action@v1
        with:
          service-id: ${{ secrets.RENDER_SERVICE_ID }}
          api-key: ${{ secrets.RENDER_API_KEY }}
Enter fullscreen mode Exit fullscreen mode

What this does:

  1. On every git push to main:
  2. Spin up a fresh Linux machine (free)
  3. Run npm test
  4. If tests pass → deploy to Render
  5. If tests fail → stop (no deploy)

Step 4: Hosting on Render (3 minutes)

Render is like the old Heroku (but with a free tier that actually works).

  1. Go to render.com and sign up with GitHub.
  2. Click "New +" → "Web Service"
  3. Connect your GitHub repository (my-cicd-app)
  4. Fill in the settings: · Name: my-cicd-app · Environment: Node · Build Command: npm install · Start Command: npm start · Plan: Free
  5. Click "Create Web Service"

Render will deploy your app once manually. Wait for the green "Live" badge.

You'll get a URL like: https://my-cicd-app.onrender.com

Visit it. You should see your JSON message.


Step 5: Connect GitHub Actions to Render (2 minutes)

Now we need to give GitHub Actions permission to deploy to Render.

Get your Render API Key:

  1. Go to Render Dashboard → Account Settings (top right)
  2. Scroll to "API Keys" → Click "Create API Key"
  3. Name it github-actions → Copy the key (starts with rnd_)

Get your Render Service ID:

  1. Go to your Web Service page on Render
  2. Look at the URL: https://dashboard.render.com/web/srv-abc123def
  3. The srv-abc123def part is your Service ID

Add them as GitHub Secrets:

  1. Go to your GitHub repository → Settings → Secrets and variables → Actions
  2. Click "New repository secret"
  3. Add RENDER_API_KEY → paste your API key
  4. Add RENDER_SERVICE_ID → paste your Service ID

Step 6: Trigger Your First Pipeline (1 minute)

Make a small change to trigger the pipeline:

# Change the message in index.js
res.json({ message: 'My first CI/CD pipeline! 🎉' });

# Commit and push
git add .
git commit -m "Test CI/CD pipeline"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Now go to your GitHub repository → Actions tab.

You'll see a workflow running:

Deploy to Production
  ├── Run Tests (yellow → green)
  └── Deploy to Render (yellow → green)
Enter fullscreen mode Exit fullscreen mode

When both turn green ✅, visit your Render URL. Your new message is live.

You just did CI/CD. 🚀


What Just Happened? (The Diagram)

Your laptop
    │
    │ git push origin main
    ▼
GitHub (Actions)
    │
    │ Spins up runner
    ▼
┌─────────────────────────────────────┐
│  CI Pipeline (Test)                 │
│  ├── npm ci                         │
│  ├── npm test                       │
│  └── ✅ All tests passed            │
└─────────────────────────────────────┘
    │
    │ Tests pass → trigger CD
    ▼
┌─────────────────────────────────────┐
│  CD Pipeline (Deploy)               │
│  ├── Call Render API                │
│  ├── Render pulls code              │
│  ├── npm install                    │
│  ├── npm start                      │
│  └── ✅ Live at your URL            │
└─────────────────────────────────────┘
    │
    ▼
Production server (Render)
Enter fullscreen mode Exit fullscreen mode

Common Variations (Adapt to Your Stack)

For a Python (Django/Flask) app:

# .github/workflows/deploy.yml
- name: Setup Python
  uses: actions/setup-python@v4
  with:
    python-version: '3.11'

- name: Install dependencies
  run: |
    pip install -r requirements.txt
    pip install pytest

- name: Run tests
  run: pytest

- name: Deploy
  run: |
    # Render handles Python automatically
    # Or use: flyctl deploy, railway up, etc.
Enter fullscreen mode Exit fullscreen mode

For a React/Vite static site:

- name: Install dependencies
  run: npm ci

- name: Build
  run: npm run build

- name: Deploy to Netlify
  uses: nwtgck/actions-netlify@v2
  with:
    publish-dir: './dist'
    production-branch: main
  env:
    NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
    NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
Enter fullscreen mode Exit fullscreen mode

For a Docker container:

- name: Build Docker image
  run: docker build -t myapp .

- name: Push to Docker Hub
  run: |
    echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
    docker push myusername/myapp:latest

- name: Deploy to DigitalOcean
  uses: appleboy/ssh-action@v1
  with:
    host: ${{ secrets.HOST }}
    username: ${{ secrets.USERNAME }}
    key: ${{ secrets.SSH_KEY }}
    script: docker pull myusername/myapp && docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Pro Tips (From Someone Who Broke Production)

  1. Always test before deploying (duh)

The needs: test line ensures you never deploy broken code. Add a failing test and watch the pipeline stop.

// Add this to test.js – it will FAIL
assert.strictEqual(1, 2);
console.log('This line never runs');
Enter fullscreen mode Exit fullscreen mode

Push it. Watch the pipeline fail at the test stage. Deploy never happens. You just saved production.

  1. Use npm ci instead of npm install

· npm install can update package-lock.json (unexpected changes)
· npm ci installs exactly what's in package-lock.json (reproducible builds)

Always use npm ci in CI.

  1. Add a "staging" environment

For real projects, deploy to staging first:

on:
  push:
    branches: [ main ]     # Deploys to staging
    branches: [ production ] # Deploys to production
Enter fullscreen mode Exit fullscreen mode
  1. Add notifications

Get a Slack/Discord message when deploy finishes:

- name: Send Slack notification
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "✅ Deployed to production!"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Enter fullscreen mode Exit fullscreen mode
  1. Manual approval for production (for serious teams)
deploy-prod:
  needs: test
  environment: production  # Requires approval in GitHub
  runs-on: ubuntu-latest
  steps:
    - name: Deploy to production
      run: ./deploy.sh
Enter fullscreen mode Exit fullscreen mode

Then go to GitHub → Settings → Environments → Add protection rule.


Troubleshooting (When Things Go Wrong)

Symptom Most likely fix
Workflow doesn't trigger Check branch name: main vs master
Tests fail but work locally Different Node version? Add .nvmrc file
Deploy step fails Wrong Render Service ID or API key
"Module not found" Forgot npm ci step?
"Permission denied" GitHub Actions needs write permissions → Settings → Actions → General → Workflow permissions


What You Just Learned

✅ CI = Automatic testing on every push
✅ CD = Automatic deployment if tests pass
✅ GitHub Actions = Free CI/CD runners
✅ Render = Free hosting with API deploys
✅ Secrets = Store API keys safely

You can now add CI/CD to any project in ~10 minutes.


Next Steps (Level Up)

  1. Add more tests – Unit tests, integration tests
  2. Add a database – Render has free PostgreSQL
  3. Add a staging environment – Deploy to staging first, then production
  4. Add end-to-end tests – Cypress or Playwright
  5. Try other platforms – Vercel, Netlify, Railway, Fly.io

The Ultimate Test

Break something on purpose:

// Add this to index.js – a hidden bug
app.get('/broken', (req, res) => {
  throw new Error('Oops');  // This will crash the server
});
Enter fullscreen mode Exit fullscreen mode

Push it. Watch the pipeline deploy it (tests passed, right?). Then visit /broken and watch it fail.

Now fix it. Push again. Watch the pipeline fix production automatically.

That's the power of CI/CD. You're not afraid to deploy anymore. Because if something breaks, you fix it and push again. No panic. No Friday deploys. Just code.


Your 10-Minute Challenge Recap

· Created an Express app
· Pushed to GitHub
· Created .github/workflows/deploy.yml
· Signed up for Render
· Added API keys as GitHub Secrets
· Made a change and watched the pipeline run

You did it. 🎉

Now go add CI/CD to your real project. Your future self (and your team) will thank you.


What stack do you want to see a CI/CD example for next? Drop a comment (Django, Spring Boot, Next.js, Go, Rust...).

Follow for more DevOps-for-beginners content.

Top comments (0)