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 kamuSERVER_USER— username SSH (misal:deploy)SERVER_SSH_KEY— private key SSH (isi dengan outputcat ~/.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:
- Tambah staging environment — buat branch
stagingyang deploy ke server terpisah (bisa VPS murah Hetzner €3/bulan) - Setup health check — endpoint
/healthyang dicek setelah deploy, kalau gagal auto-rollback - Artifact versioning — tag Docker image dengan git SHA biar bisa rollback ke versi spesifik
- 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.