CI CD Pipeline Self-Hosted GitLab: Panduan Praktis

by Marcus Chen
CI CD Pipeline Self-Hosted GitLab: Panduan Praktis

Tagihan GitLab.com-mu naik 40% bulan lalu? Atau mungkin kamu baru sadar bahwa setiap pipeline menit dihitung — bahkan untuk job yang gagal karena typo di .gitlab-ci.yml.

Saya pernah di posisi itu. Proyek freelance kecil, pipeline jalan 50-60 kali sehari, dan di akhir bulan muncul angka yang bikin dahi berkerut. Solusinya bukan pindah ke GitHub Actions (yang punya masalah sendiri), tapi hosting GitLab sendiri di VPS. Total biaya: sekitar $6/bulan untuk VPS 4GB RAM. Itu sudah termasuk runner, registry, dan semua fitur GitLab CE.

Artikel ini bukan overview teoritis. Ini setup yang saya pakai sekarang, dengan konfigurasi nyata, termasuk bagian yang sering bikin orang tersandung.

Kenapa Self-Hosted GitLab Masuk Akal untuk Solo Engineer

GitLab CE (Community Edition) gratis dan fiturnya lebih dari cukup. Kamu dapat CI/CD pipeline, container registry, issue tracker, merge request, dan bahkan Pages — tanpa bayar lisensi. Yang kamu butuhkan cuma server.

Perbandingan kasar:

GitLab.com Free GitLab.com Premium Self-Hosted CE
CI/CD menit/bulan 400 menit 10.000 menit Unlimited
Storage 5 GB 50 GB Sesuai disk
Harga Gratis $29/user/bulan ~$6/bulan (VPS)
Container Registry
Self-managed runner

400 menit gratis habis dalam hitungan hari kalau kamu aktif develop. Premium $29/user/bulan untuk solo engineer itu mahal. Self-hosted dengan VPS murah jauh lebih masuk akal.

Satu caveat: kamu perlu mau urus update, backup, dan maintenance. Kalau kamu tipe yang nggak mau pegang server sama sekali, self-hosted bukan buat kamu. Tapi kalau kamu sudah biasa self-host aplikasi lain, ini nggak jauh berbeda.

Persiapan Server dan Instalasi GitLab CE

Spesifikasi minimum yang saya rekomendasikan: 4GB RAM, 2 vCPU, 40GB SSD. Dengan 2GB RAM GitLab bisa jalan tapi sering swap, dan itu menyiksa. Saya pakai Hetzner CX22 (4GB RAM, 2 vCPU) seharga €3.79/bulan — salah satu yang paling value untuk workload ini.

Instalasinya lewat Docker Compose. Jauh lebih mudah di-update dan di-backup dibanding instalasi langsung di sistem.

Buat file docker-compose.yml:

version: '3.8'

services:
  gitlab:
    image: gitlab/gitlab-ce:16.11.0-ce.0
    container_name: gitlab
    restart: always
    hostname: 'gitlab.domainmu.com'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://gitlab.domainmu.com'
        gitlab_rails['gitlab_shell_ssh_port'] = 2222
        gitlab_rails['time_zone'] = 'Asia/Jakarta'
        # Matikan fitur yang nggak kamu pakai
        gitlab_rails['gitlab_email_enabled'] = true
        gitlab_rails['smtp_enable'] = true
        gitlab_rails['smtp_address'] = 'smtp.mailgun.org'
        gitlab_rails['smtp_port'] = 587
        gitlab_rails['smtp_user_name'] = 'postmaster@mg.domainmu.com'
        gitlab_rails['smtp_password'] = 'password-smtp-kamu'
        gitlab_rails['smtp_domain'] = 'mg.domainmu.com'
        gitlab_rails['smtp_authentication'] = 'plain'
        gitlab_rails['smtp_enable_starttls_auto'] = true
        # Batasi registrasi
        gitlab_rails['gitlab_signup_enabled'] = false
        # Nginx dan SSL via Let's Encrypt
        letsencrypt['enable'] = true
        letsencrypt['contact_emails'] = ['email@domainmu.com']
        nginx['listen_port'] = 80
        nginx['listen_https_port'] = 443
    ports:
      - '80:80'
      - '443:443'
      - '2222:22'
    volumes:
      - gitlab_config:/etc/gitlab
      - gitlab_logs:/var/log/gitlab
      - gitlab_data:/var/opt/gitlab
    shm_size: '256m'

volumes:
  gitlab_config:
  gitlab_logs:
  gitlab_data:

Jalankan dengan:

docker compose up -d

Proses inisialisasi pertama butuh 3-5 menit. Cek progressnya:

docker logs -f gitlab | grep 'gitlab Reconfigured'

Setelah selesai, ambil password root:

docker exec gitlab cat /etc/gitlab/initial_root_password

Password ini hilang setelah 24 jam, jadi langsung ganti di Settings setelah login pertama.

Setup GitLab Runner untuk CI CD Pipeline

GitLab Runner adalah komponen yang benar-benar menjalankan job di pipeline-mu. Tanpa runner, pipeline cuma antri selamanya. Kamu bisa install runner di server yang sama atau server terpisah — saya pakai server yang sama untuk hemat biaya, tapi pisahkan container-nya.

Tambahkan service runner ke docker-compose.yml:

  gitlab-runner:
    image: gitlab/gitlab-runner:v16.11.0
    container_name: gitlab-runner
    restart: always
    volumes:
      - gitlab_runner_config:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - gitlab

volumes:
  gitlab_runner_config:

Setelah container runner jalan, daftarkan runner ke GitLab:

docker exec -it gitlab-runner gitlab-runner register

Kamu akan ditanya beberapa hal:

  • GitLab instance URL: https://gitlab.domainmu.com
  • Registration token: ambil dari GitLab → Admin → CI/CD → Runners
  • Description: docker-runner-01
  • Tags: docker,linux
  • Executor: docker
  • Default Docker image: alpine:latest

Setelah register, edit /etc/gitlab-runner/config.toml (di dalam volume) untuk tambahkan konfigurasi penting:

[[runners]]
  name = "docker-runner-01"
  url = "https://gitlab.domainmu.com"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "alpine:latest"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]
    pull_policy = "if-not-present"
    shm_size = 0
  [runners.cache]
    MaxUploadedArchiveSize = 0

Satu hal penting: pull_policy = "if-not-present" menghemat bandwidth dan waktu dengan tidak pull image Docker setiap kali pipeline jalan kalau image sudah ada di cache lokal.

Menulis .gitlab-ci.yml yang Efisien

Ini bagian yang paling sering diabaikan. Orang setup GitLab, bikin pipeline, tapi konfigurasinya lambat dan boros resource.

Contoh pipeline untuk aplikasi Node.js:

image: node:20-alpine

variables:
  NODE_ENV: test

cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/

stages:
  - install
  - test
  - build
  - deploy

install:deps:
  stage: install
  script:
    - npm ci --prefer-offline
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

test:unit:
  stage: test
  script:
    - npm run test:unit
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'

test:lint:
  stage: test
  script:
    - npm run lint

build:app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 day
  only:
    - main
    - develop

deploy:production:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
  script:
    - ssh $DEPLOY_USER@$DEPLOY_HOST "cd /app && git pull && npm ci --production && pm2 restart app"
  environment:
    name: production
    url: https://appmu.com
  only:
    - main
  when: manual

Beberapa keputusan di konfigurasi ini yang sengaja:

  1. Cache berdasarkan package-lock.json — cache invalide otomatis kalau dependency berubah.
  2. npm ci --prefer-offline — pakai cache npm lokal, nggak fetch dari internet kalau nggak perlu.
  3. deploy:production pakai when: manual — deploy ke production nggak pernah otomatis. Saya harus klik tombol di GitLab UI. Ini kebiasaan yang menyelamatkan nyawa (atau setidaknya uptime).
  4. SSH key lewat CI/CD Variables — simpan SSH_PRIVATE_KEY, DEPLOY_HOST, DEPLOY_USER di GitLab → Settings → CI/CD → Variables, bukan di-hardcode di file.

Backup Otomatis yang Benar-benar Jalan

Ini bagian yang paling sering dilewati sampai disaster terjadi. GitLab CE punya built-in backup tool.

Buat script backup-gitlab.sh:

#!/bin/bash

set -euo pipefail

BACKUP_DIR="/backup/gitlab"
S3_BUCKET="s3://backup-bucket-kamu/gitlab"
DATE=$(date +%Y%m%d_%H%M%S)

# Jalankan backup GitLab
docker exec gitlab gitlab-backup create SKIP=artifacts,registry

# Backup config files (berisi secrets, JANGAN skip ini)
docker exec gitlab tar -czf /tmp/gitlab-config-${DATE}.tar.gz /etc/gitlab
docker cp gitlab:/tmp/gitlab-config-${DATE}.tar.gz ${BACKUP_DIR}/

# Upload ke S3 (pakai rclone atau aws cli)
rclone copy ${BACKUP_DIR}/ ${S3_BUCKET}/ --max-age 7d

# Hapus backup lokal lebih dari 3 hari
find ${BACKUP_DIR} -name "*.tar" -mtime +3 -delete
find ${BACKUP_DIR} -name "*.tar.gz" -mtime +3 -delete

echo "Backup selesai: ${DATE}"

Jadwalkan dengan cron:

0 2 * * * /opt/scripts/backup-gitlab.sh >> /var/log/gitlab-backup.log 2>&1

Satu hal yang sering dilupakan: backup GitLab dan backup config harus versi yang sama. Kalau GitLab-mu versi 16.11 dan kamu restore ke versi 17.x, prosesnya lebih rumit. Catat versi yang kamu pakai dan update secara bertahap.

Kalau kamu sudah setup monitoring untuk self-hosted services, integrasikan alert kalau script backup gagal. Saya tulis soal monitoring stack sederhana di panduan monitoring self-hosted — setup Uptime Kuma cukup untuk mulai.

Update GitLab Tanpa Downtime Berlebihan

GitLab update cukup sering — dan kamu wajib update secara bertahap, nggak boleh loncat versi mayor langsung. Misalnya dari 15.x ke 17.x harus lewat 16.x dulu.

Cek upgrade path di docs.gitlab.com/ee/update/upgrade_paths.html sebelum update.

Proses update dengan Docker Compose:

# Stop dulu
docker compose stop gitlab

# Backup sebelum update (wajib)
docker exec gitlab gitlab-backup create

# Edit docker-compose.yml, ganti versi image
# image: gitlab/gitlab-ce:16.11.0-ce.0 -> gitlab/gitlab-ce:17.0.0-ce.0

# Pull image baru dan restart
docker compose pull gitlab
docker compose up -d gitlab

# Monitor log
docker logs -f gitlab

Proses reconfigure setelah update biasanya 2-4 menit. GitLab akan unavailable selama itu. Untuk solo engineer, downtime 5 menit sekali beberapa bulan bukan masalah besar.

Kalau kamu manage beberapa project dan ingin nol-downtime lebih serius, lihat setup dengan load balancer — tapi itu sudah overkill untuk kebanyakan kasus di sini.

Kesimpulan: Apa yang Harus Kamu Lakukan Besok

Setup ci cd pipeline self-hosted gitlab bukan proyek weekend yang rumit. Dengan Docker Compose, kamu bisa punya instance GitLab CE yang fully functional dalam 30-60 menit — termasuk runner dan pipeline pertama yang jalan.

Investasi waktunya di awal, tapi hasilnya: pipeline unlimited, container registry sendiri, dan tagihan bulanan yang predictable di angka $6-10 — bukan angka yang berubah tergantung seberapa sering kamu push commit.

Langkah konkret untuk besok: spin up VPS 4GB RAM, clone konfigurasi Docker Compose dari artikel ini, dan jalankan. Kalau tersandung di bagian SSL atau runner registration, cek log dulu sebelum panik — 80% masalah ada di sana.

Kalau kamu sudah punya GitLab self-hosted dan mau optimasi lebih lanjut, baca juga cara mengatur secrets dan environment variables untuk multiple project tanpa kebocoran credential.

Satu hal yang nggak perlu kamu tunda: matikan auto-signup di GitLab instance-mu (gitlab_signup_enabled = false) sebelum instance-mu terindeks Google. Itu bukan saran — itu keharusan.