CI/CD Pipeline untuk Solo Developer di 2024

by Marcus Chen
CI/CD Pipeline untuk Solo Developer di 2024

Kamu kerja sendirian, deploy manual tiap kali ada perubahan, dan pernah push langsung ke production sambil deg-degan? Gue juga pernah di situ. Setiap kali mau deploy, ritualnya sama: SSH ke server, git pull, npm install, restart service, terus nunggu sambil berdoa nggak ada yang error.

Sampai suatu malam gue push hotfix jam 11 malam dan lupa jalanin database migration. Production down. Pelanggan marah. Gue panik.

Sejak itu gue setup CI/CD pipeline yang proper — dan ternyata sebagai solo developer, kamu nggak butuh setup enterprise yang kompleks. Cukup yang simpel, reliable, dan bisa jalan otomatis setiap kali kamu push code.

Kenapa CI/CD Penting Buat Solo Developer?

Sebagian orang mikir CI/CD itu buat tim besar. Salah besar. Justru solo developer yang paling butuh ini karena:

  • Nggak ada code review partner — automated test jadi safety net kamu
  • Deploy manual itu error-prone — satu langkah kelewat bisa fatal
  • Kamu punya banyak project — otomasi berarti fokus ke code, bukan ops
  • Waktu kamu terbatas — pipeline yang jalan sendiri = lebih banyak waktu coding

Intinya: CI/CD pipeline untuk solo developer bukan overkill, ini survival kit.

Stack yang Gue Pakai (dan Kenapa)

Untuk setup ini gue pakai:

  • GitHub Actions — gratis untuk public repo, 2000 menit/bulan untuk private repo
  • Docker — biar environment di CI sama persis dengan production
  • VPS dengan SSH — gue pakai Hetzner, tapi VPS Vultr vs DigitalOcean untuk pengguna Indonesia juga oke
  • Node.js app sebagai contoh, tapi konsepnya sama untuk stack lain

Kalau kamu pakai GitLab, konsepnya mirip, tinggal ganti syntax YAML-nya.

Setup GitHub Actions dari Nol

Buat file .github/workflows/deploy.yml di root project kamu:

name: CI/CD Pipeline

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

env:
  NODE_VERSION: '20'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        options: >
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

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

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

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
          NODE_ENV: test

  build-and-push:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'

    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}

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

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=raw,value=latest

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /opt/myapp
            echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker pull ghcr.io/${{ github.repository }}:latest
            docker compose up -d --no-deps --build app
            docker image prune -f

Pipeline ini punya tiga job yang jalan berurutan: test → build → deploy. Kalau test gagal, build nggak jalan. Kalau build gagal, deploy nggak jalan. Simple, tapi efektif.

Dockerfile yang Proper untuk Production

Jangan pakai Dockerfile asal-asalan. Ini yang gue pakai dengan multi-stage build:

# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Runner
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# Security: jangan run sebagai root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodeuser

COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

USER nodeuser

EXPOSE 3000

CMD ["node", "dist/index.js"]

Multi-stage build ini bikin image production lebih kecil karena dev dependencies nggak ikut masuk.

Setup Secrets di GitHub

Pergi ke Settings → Secrets and variables → Actions di repo kamu, lalu tambah:

  • SERVER_HOST — IP atau domain VPS kamu
  • SERVER_USER — username SSH (misal: deploy)
  • SERVER_SSH_KEY — private key SSH (isi dengan output cat ~/.ssh/id_ed25519)

Untuk generate SSH key khusus deploy:

# Di local machine kamu
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_key

# Copy public key ke server
ssh-copy-id -i ~/.ssh/deploy_key.pub user@your-server-ip

# Isi SERVER_SSH_KEY dengan private key ini
cat ~/.ssh/deploy_key

Jangan pakai SSH key yang sama dengan yang kamu pakai sehari-hari. Buat key terpisah khusus untuk deployment. Kalau key-nya bocor, kamu tinggal revoke satu key itu aja.

Docker Compose di Server

Di server kamu, buat file /opt/myapp/docker-compose.yml:

version: '3.8'

services:
  app:
    image: ghcr.io/username/myapp:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
    depends_on:
      - postgres
    networks:
      - app-network

  postgres:
    image: postgres:15-alpine
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - certbot_data:/etc/letsencrypt
    depends_on:
      - app
    networks:
      - app-network

volumes:
  postgres_data:
  certbot_data:

networks:
  app-network:
    driver: bridge

Buat file .env di /opt/myapp/ untuk environment variables yang sensitif. File ini jangan di-commit ke git.

Gotcha yang Pernah Gue Kena

1. Docker layer cache di GitHub Actions

Pertama kali setup, setiap build butuh 8-10 menit karena Docker harus download semua layer dari awal. Fix-nya ada di workflow di atas — perhatikan bagian cache-from dan cache-to di step build:

cache-from: type=gha
cache-to: type=gha,mode=max

Ini pakai GitHub Actions cache. Build time turun dari 10 menit ke 2-3 menit.

2. Database migration saat deploy

Ini yang bikin gue down waktu itu. Solusinya: jalankan migration sebagai bagian dari deploy script, tapi dengan careful:

# Di bagian script SSH deploy, tambah sebelum docker compose up:
docker compose run --rm app node dist/migrate.js
docker compose up -d --no-deps app

Atau kalau pakai Prisma:

docker compose run --rm app npx prisma migrate deploy

3. Zero-downtime deploy

Pakai --no-deps flag di docker compose up biar service lain (postgres, nginx) nggak restart juga. Container lama akan diganti container baru dengan graceful shutdown.

4. Secrets jangan di-hardcode di workflow

Pernah lihat orang taruh API key langsung di YAML? Jangan. Semua yang sensitif masuk ke GitHub Secrets. Bahkan nama database pun lebih aman di Secrets daripada di YAML yang ter-commit ke repo.

Monitoring Sederhana Biar Tahu Kalau Deploy Gagal

Tambah notifikasi ke Telegram biar kamu langsung tahu kalau pipeline gagal:

  notify:
    name: Send Notification
    runs-on: ubuntu-latest
    needs: [test, build-and-push, deploy]
    if: always()

    steps:
      - name: Notify Telegram
        uses: appleboy/telegram-action@master
        with:
          to: ${{ secrets.TELEGRAM_CHAT_ID }}
          token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          message: |
            ${{ job.status == 'success' && '✅' || '❌' }} Deploy ${{ job.status }}
            Repo: ${{ github.repository }}
            Branch: ${{ github.ref_name }}
            Commit: ${{ github.sha }}
            By: ${{ github.actor }}

Setup Telegram bot-nya: chat dengan @BotFather di Telegram, buat bot baru, copy token-nya ke GitHub Secrets.

Langkah Lanjutan

Ini yang gue lakuin setelah pipeline dasar jalan:

  1. Tambah staging environment — buat branch staging yang deploy ke server terpisah (bisa VPS murah Hetzner €3/bulan)
  2. Setup health check — endpoint /health yang dicek setelah deploy, kalau gagal auto-rollback
  3. Artifact versioning — tag Docker image dengan git SHA biar bisa rollback ke versi spesifik
  4. Dependabot — aktifkan di GitHub biar dependency update otomatis dibuatkan PR-nya

Untuk rollback manual kalau butuh:

# Di server, rollback ke image versi sebelumnya
docker compose down
docker tag ghcr.io/username/myapp:sha-abc123 ghcr.io/username/myapp:latest
docker compose up -d

CI/CD pipeline untuk solo developer yang gue describe di sini udah gue pakai di 4 project berbeda selama hampir setahun. Total waktu setup pertama kali sekitar 2-3 jam, tapi setelah itu deploy jadi semudah git push. Nggak ada lagi ritual SSH jam 11 malam sambil berdoa. Dengan API design patterns that scale yang proper, infrastructure kamu juga siap handle growth.

Start dari yang simpel dulu — test dan deploy otomatis. Setelah itu baru tambah fitur lain sesuai kebutuhan.