Langsung ke konten
KamusNgoding
Terapan Github-actions 5 menit baca

Membangun CI/CD Pipeline untuk Aplikasi Node.js dengan GitHub Actions: Tutorial Step-by-Step

#github-actions #ci/cd #nodejs #deployment #applied

Membangun CI/CD Pipeline untuk Aplikasi Node.js dengan GitHub Actions: Tutorial Step-by-Step

Pendahuluan

Bayangkan kamu sedang membangun backend API untuk aplikasi e-commerce — setiap kali ada perubahan kode, kamu harus manual menjalankan tes, build, lalu upload ke server. Proses itu bukan hanya lambat, tapi juga rawan kesalahan manusia. Itulah masalah yang diselesaikan oleh CI/CD pipeline.

Continuous Integration (CI) adalah praktik menggabungkan kode secara otomatis dan memverifikasi hasilnya setiap kali ada push. Continuous Deployment (CD) melanjutkannya dengan men-deploy kode yang sudah terverifikasi ke server secara otomatis.

Jika kamu ingin membangun layanan seperti Tokopedia atau Bukalapak, konsep CI/CD adalah fondasi yang membuat ratusan developer bisa bekerja bersama tanpa saling menghalangi. Dalam tutorial ini, kita akan membangun pipeline CI/CD lengkap untuk aplikasi Node.js menggunakan GitHub Actions.


Prasyarat dan Pengenalan Workflow GitHub Actions

Sebelum mulai, pastikan kamu sudah menyiapkan:

  • Akun GitHub dengan repository Node.js
  • Node.js v18+ terinstall di lokal
  • Aplikasi Node.js dengan test suite (kita pakai Jest)
  • Server VPS atau akses SSH ke server deployment (untuk bagian CD)
  • Pemahaman dasar tentang package.json dan npm scripts

Struktur project Node.js yang akan kita gunakan:

my-node-app/
├── src/
│   ├── app.js
│   └── routes/
│       └── users.js
├── tests/
│   └── users.test.js
├── package.json
└── .github/
    └── workflows/
        └── ci-cd.yml

File package.json dengan scripts yang diperlukan:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js",
    "test": "jest --coverage",
    "lint": "eslint src/ --ext .js",
    "build": "echo 'Build step for Node.js'"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "eslint": "^8.0.0",
    "supertest": "^6.3.0",
    "nodemon": "^3.0.0"
  }
}

Implementasi Continuous Integration: Build dan Test Otomatis

Buat file .github/workflows/ci-cd.yml. Kita mulai dengan bagian CI dulu — menjalankan lint dan test otomatis setiap ada push atau pull request:

name: CI/CD Pipeline Node.js

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  NODE_VERSION: '20'

jobs:
  # =====================
  # JOB 1: Continuous Integration
  # =====================
  ci:
    name: Build & Test
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18, 20]

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

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

      - name: Install dependencies
        run: npm ci

      - name: Jalankan ESLint
        run: npm run lint

      - name: Jalankan unit tests
        run: npm test
        env:
          NODE_ENV: test

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        if: matrix.node-version == 20
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7

Beberapa poin penting di sini:

  • npm ci digunakan sebagai pengganti npm install karena lebih deterministik — ia membaca package-lock.json dan tidak memodifikasinya
  • matrix strategy memungkinkan kita menguji di Node.js 18 dan 20 secara paralel
  • cache: 'npm' menyimpan cache node_modules antar run sehingga pipeline lebih cepat

Kalau kamu sudah familiar dengan Mastering ES6 Modules: Cara Mengelola Kode JavaScript Secara Terstruktur, pastikan test suite kamu juga menggunakan module system yang konsisten agar tidak ada error saat CI berjalan.


Implementasi Continuous Deployment: Deploy Otomatis ke Server

Setelah CI berhasil, kita tambahkan job CD yang akan berjalan hanya jika CI sukses dan hanya untuk branch main:

  # =====================
  # JOB 2: Continuous Deployment
  # =====================
  cd:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: ci                    # Tunggu job CI selesai
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    environment:
      name: production
      url: https://myapp.example.com

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

      - name: Setup SSH key
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Tambahkan server ke known_hosts
        run: |
          ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy ke server via SSH
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'ENDSSH'
            set -e
            cd /var/www/my-node-app
            git pull origin main
            npm ci --production
            pm2 reload ecosystem.config.js --env production
            echo "Deploy berhasil pada $(date)"
          ENDSSH

      - name: Verifikasi deployment
        run: |
          sleep 10
          curl -f https://myapp.example.com/health || exit 1

      - name: Kirim notifikasi sukses
        if: success()
        run: |
          echo "Deploy sukses ke production!"
          # Bisa ditambah webhook Slack/Discord di sini

      - name: Kirim notifikasi gagal
        if: failure()
        run: |
          echo "Deploy GAGAL! Periksa logs di GitHub Actions."

Secrets yang perlu kamu tambahkan di Settings > Secrets and variables > Actions repositorimu:

Secret NameIsi
SSH_PRIVATE_KEYPrivate key SSH (isi ~/.ssh/id_rsa)
SERVER_HOSTIP atau domain server, misal 203.0.113.10
SERVER_USERUsername SSH, misal ubuntu atau deploy

File ecosystem.config.js di server untuk PM2:

module.exports = {
  apps: [{
    name: 'my-node-app',
    script: 'src/app.js',
    instances: 'max',
    exec_mode: 'cluster',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    }
  }]
};

Contoh Kasus Nyata: Docker Build dan Push

Berikut workflow lengkap yang juga menangani Docker build — pola yang umum digunakan pada aplikasi backend berskala:

  # =====================
  # JOB 3: Build & Push Docker Image (Opsional)
  # =====================
  docker:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: ci
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Login ke Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build dan push image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            myusername/my-node-app:latest
            myusername/my-node-app:${{ github.sha }}

Dockerfile untuk Node.js:

FROM node:20-alpine AS base
WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --production

# Copy source
COPY src/ ./src/

# Non-root user untuk keamanan
USER node

EXPOSE 3000
CMD ["node", "src/app.js"]

Pola yang sama bisa kamu terapkan saat membangun backend dengan bahasa lain. Misalnya, jika kamu mengikuti tutorial Membangun RESTful API Sederhana dengan Go: Tutorial Step-by-Step, strukturnya hampir identik — hanya bagian build dan runtime command yang berbeda.


Troubleshooting: Error yang Sering Muncul

npm ci: npm error: Missing package-lock.json

Penyebab: File package-lock.json tidak di-commit ke repository, padahal npm ci membutuhkannya secara wajib.

Solusi:

# Jalankan di lokal, lalu commit hasilnya
npm install
git add package-lock.json
git commit -m "chore: add package-lock.json"
git push

SSH: Host key verification failed saat deploy

Penyebab: Server belum ditambahkan ke known_hosts sebelum koneksi SSH dilakukan, atau step ssh-keyscan dijalankan setelah step ssh command.

Solusi:

# Pastikan urutan steps sudah benar di workflow
- name: Tambahkan server ke known_hosts
  run: ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts

- name: Deploy via SSH
  run: ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} "cd /app && git pull"

Jest: Jest encountered an unexpected token saat CI

Penyebab: Jest belum dikonfigurasi untuk menangani ES Modules, sementara source code menggunakan sintaks import/export.

Solusi:

# Install dependensi Babel untuk Jest
npm install --save-dev @babel/core @babel/preset-env babel-jest
// package.json — tambahkan konfigurasi Jest
{
  "jest": {
    "transform": {
      "^.+\\.js$": "babel-jest"
    }
  }
}
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }]
  ]
};

PM2: Error: Script not found setelah pm2 reload

Penyebab: Path script di ecosystem.config.js tidak sesuai dengan struktur direktori di server, atau file tidak ter-copy saat git pull.

Solusi:

# Di server, verifikasi path file ada
ls -la /var/www/my-node-app/src/app.js

# Restart PM2 dengan path absolut jika perlu
pm2 start /var/www/my-node-app/src/app.js --name my-node-app
pm2 save

Pertanyaan yang Sering Diajukan

Apa perbedaan npm install dan npm ci di GitHub Actions?

npm ci dirancang khusus untuk lingkungan CI/CD. Ia selalu menghapus node_modules dan menginstall ulang dari nol berdasarkan package-lock.json, sehingga hasilnya 100% konsisten. npm install bisa memodifikasi package-lock.json dan lebih lambat karena tidak menjamin reproduksi yang identik.

Bagaimana cara menyimpan environment variable yang berbeda untuk staging dan production?

Gunakan fitur Environments di GitHub Actions. Buat dua environment (staging dan production) di Settings repositori, lalu tambahkan secrets yang berbeda di masing-masing. Di workflow, gunakan environment: production atau environment: staging di setiap job agar secrets yang tepat digunakan secara otomatis.

Apakah CI/CD pipeline ini aman untuk menyimpan SSH key?

Ya, selama kamu menggunakan GitHub Secrets (bukan hardcode di file workflow). Secrets dienkripsi dan hanya bisa diakses oleh workflow saat runtime — nilainya tidak pernah muncul di logs. Untuk keamanan tambahan, buat SSH key khusus untuk deployment dengan akses terbatas, bukan menggunakan key pribadi kamu.

Bagaimana cara men-trigger pipeline hanya untuk folder tertentu di monorepo?

Gunakan filter paths di bagian on:

on:
  push:
    branches: [ main ]
    paths:
      - 'packages/backend/**'
      - '.github/workflows/backend-ci.yml'

Dengan ini, pipeline hanya berjalan jika ada perubahan di folder packages/backend/, sehingga tidak membuang waktu build untuk perubahan yang tidak relevan.

Berapa lama waktu ideal untuk satu CI pipeline berjalan?

Idealnya di bawah 5 menit untuk developer experience yang baik. Jika lebih lama, identifikasi bottleneck: biasanya install dependencies (solusi: aktifkan caching dengan cache: 'npm') atau test yang terlalu banyak (solusi: jalankan test secara paralel dengan Jest flag --maxWorkers=4).


Kesimpulan

Kita telah berhasil membangun pipeline CI/CD lengkap untuk aplikasi Node.js: mulai dari menjalankan lint dan test otomatis saat ada push, hingga deployment otomatis ke server via SSH setelah semua checks hijau. Pipeline ini juga bisa diperluas dengan Docker build, notifikasi Slack, atau deployment ke platform cloud seperti Railway atau Render.

Poin-poin kunci yang perlu diingat:

  • Gunakan npm ci (bukan npm install) untuk konsistensi di CI
  • Simpan semua kredensial di GitHub Secrets, bukan di kode
  • Gunakan needs: untuk memastikan CD hanya berjalan setelah CI sukses
  • Tambahkan health check setelah deploy untuk verifikasi otomatis

Selamat mencoba membangun pipeline CI/CD pertamamu! Dengan otomatisasi ini, kamu bisa fokus menulis fitur baru tanpa khawatir tentang proses deployment yang membosankan. Jika ada pertanyaan atau menemui kendala, jangan ragu untuk eksplorasi artikel-artikel lainnya di KamusNgoding — komunitas developer Indonesia selalu siap belajar bersama!

Artikel Terkait