Cara Setup Self-Hosted Development Environment

by Marcus Chen
Cara Setup Self-Hosted Development Environment

Cloud Bill Tiba-tiba Dobel — Ini Yang Gue Lakuin

Bulan ketiga freelance, gue buka invoice AWS dan angkanya tiga kali lipat dari bulan sebelumnya. Bukan karena traffic naik — tapi karena gue lupa matiin beberapa service dev yang jalan 24/7. Database staging, beberapa container buat testing, reverse proxy — semua ngabisin duit tanpa menghasilkan apa-apa.

Sejak saat itu gue pindah ke self-hosted development environment. Setup sekali, bayar flat ke provider VPS, dan semua environment dev gue jalan di sana — atau di mesin lokal kalau lagi offline.

Artikel ini adalah cara setup self-hosted development environment yang gue pakai sekarang: Docker Compose sebagai orkestrator, Traefik sebagai reverse proxy, dan beberapa service dasar yang hampir selalu dibutuhkan (database, object storage, CI runner). Bisa dijalankan di VPS Ubuntu 22.04 atau di laptop sendiri.


Kenapa Self-Hosted Lebih Masuk Akal untuk Dev Environment

Bukan soal anti-cloud. Cloud masih masuk akal untuk production. Tapi untuk dev environment, self-hosted menang di beberapa hal:

  • Biaya flat. VPS 2 vCPU / 4 GB RAM di Hetzner atau Contabo sekitar Rp 80–150 ribu/bulan. Bandingkan dengan biaya managed services yang bisa loncat tiba-tiba.
  • Kontrol penuh. Mau install PostgreSQL versi berapa, Redis config seperti apa — semua terserah kamu.
  • Offline-friendly. Kalau setup di mesin lokal, nggak perlu internet buat develop.
  • Latency rendah. Database lokal jauh lebih cepat dari database managed yang jarak servernya ribuan kilometer.

Gotcha yang perlu kamu tahu dari awal: self-hosted artinya kamu yang bertanggung jawab atas backup, update, dan security. Kalau ini buat production, pikirkan matang-matang. Untuk dev environment, risikonya jauh lebih rendah.


Prerequisites: Yang Harus Ada Sebelum Mulai

Pastikan kamu punya:

  • Ubuntu 22.04 (VPS atau lokal) — distro lain bisa, tapi perintahnya mungkin beda sedikit
  • Docker Engine (bukan Docker Desktop) dan Docker Compose v2
  • Domain atau subdomain kalau mau akses via HTTPS (opsional untuk lokal)
  • User non-root dengan akses sudo

Install Docker kalau belum ada:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Verifikasi:

docker --version
# Docker version 25.x.x
docker compose version
# Docker Compose version v2.x.x

Struktur Folder dan File Konfigurasi

Gue pakai struktur ini supaya mudah di-maintain:

~/devenv/
├── docker-compose.yml
├── traefik/
│   ├── traefik.yml
│   └── acme.json          # TLS certs (chmod 600)
├── postgres/
│   └── init.sql
└── .env

Buat foldernya:

mkdir -p ~/devenv/traefik ~/devenv/postgres
cd ~/devenv
touch traefik/acme.json
chmod 600 traefik/acme.json

Setup Traefik sebagai Reverse Proxy

Traefik yang handle routing dan TLS otomatis via Let's Encrypt. Kalau kamu setup di lokal tanpa domain, skip bagian ACME dan pakai HTTP saja.

traefik/traefik.yml:

api:
  dashboard: true
  insecure: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: kamu@example.com
      storage: /acme.json
      httpChallenge:
        entryPoint: web

providers:
  docker:
    exposedByDefault: false
    network: devnet

log:
  level: INFO

Ganti kamu@example.com dengan email kamu yang valid untuk notifikasi sertifikat.


Docker Compose Utama

Ini file docker-compose.yml yang mencakup Traefik, PostgreSQL, Redis, dan MinIO (S3-compatible object storage):

version: "3.9"

networks:
  devnet:
    external: false

volumes:
  postgres_data:
  redis_data:
  minio_data:

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/traefik.yml:/traefik.yml:ro
      - ./traefik/acme.json:/acme.json
    networks:
      - devnet
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.dev.example.com`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$xyz$$hashedpassword"

  postgres:
    image: postgres:16-alpine
    container_name: postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - devnet
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    container_name: redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - devnet
    ports:
      - "6379:6379"

  minio:
    image: minio/minio:latest
    container_name: minio
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
    volumes:
      - minio_data:/data
    networks:
      - devnet
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.minio.rule=Host(`minio.dev.example.com`)"
      - "traefik.http.routers.minio.entrypoints=websecure"
      - "traefik.http.routers.minio.tls.certresolver=letsencrypt"
      - "traefik.http.services.minio.loadbalancer.server.port=9001"

Buat file .env:

POSTGRES_USER=devuser
POSTGRES_PASSWORD=gantiguepassword123
POSTGRES_DB=devdb
REDIS_PASSWORD=redispassword456
MINIO_USER=minioadmin
MINIO_PASSWORD=miniopassword789

Jangan commit file .env ke Git. Tambahkan ke .gitignore:

echo ".env" >> .gitignore
echo "traefik/acme.json" >> .gitignore

Jalankan dan Verifikasi

cd ~/devenv
docker compose up -d

Cek semua container jalan:

docker compose ps

Output yang diharapkan:

NAME        IMAGE                    STATUS
traefik     traefik:v3.0             Up
postgres    postgres:16-alpine       Up
redis       redis:7-alpine           Up
minio       minio/minio:latest       Up

Test koneksi PostgreSQL:

docker exec -it postgres psql -U devuser -d devdb -c "SELECT version();"

Test Redis:

docker exec -it redis redis-cli -a redispassword456 ping
# PONG

Gotcha yang Gue Temuin Waktu Setup

1. acme.json harus permission 600. Kalau Traefik error soal TLS dan kamu lihat log "acme.json has wrong permissions", jalankan:

chmod 600 ~/devenv/traefik/acme.json

2. Port 80/443 sudah dipakai. Di beberapa VPS ada Nginx atau Apache yang sudah running. Cek dulu:

sudo ss -tlnp | grep -E ':80|:443'

Kalau ada, stop dulu service-nya sebelum jalankan Traefik.

3. Docker socket exposure adalah risiko keamanan. Mount /var/run/docker.sock ke Traefik memang praktis, tapi artinya siapa pun yang bisa akses Traefik container bisa kontrol Docker host. Untuk dev environment ini acceptable, tapi jangan lakukan di production tanpa mitigasi tambahan.

4. $$ di label Docker Compose. Kalau kamu pakai htpasswd string di label Traefik, tanda $ harus di-escape jadi $$ di dalam docker-compose.yml. Kalau nggak, Docker Compose akan coba interpret itu sebagai variable substitution.

Generate htpasswd string:

echo $(htpasswd -nB admin) | sed -e s/\\$/\\$\\$/g

5. DNS harus sudah pointing sebelum Let's Encrypt challenge. Kalau domain belum resolve ke IP server kamu, ACME challenge akan gagal dan Traefik akan rate-limit kamu dari Let's Encrypt. Verifikasi dulu:

dig +short traefik.dev.example.com

Pastikan output-nya IP server kamu.


Tambahkan Service Baru ke Environment

Misalnya kamu mau tambahkan Gitea (self-hosted Git) ke environment ini. Tinggal tambahkan service baru di docker-compose.yml dengan label Traefik:

  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=postgres:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=${POSTGRES_USER}
      - GITEA__database__PASSWD=${POSTGRES_PASSWORD}
    volumes:
      - gitea_data:/data
    networks:
      - devnet
    depends_on:
      - postgres
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gitea.rule=Host(`git.dev.example.com`)"
      - "traefik.http.routers.gitea.entrypoints=websecure"
      - "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
      - "traefik.http.services.gitea.loadbalancer.server.port=3000"

Tambahkan juga gitea_data ke section volumes di atas, lalu buat database-nya di PostgreSQL:

docker exec -it postgres psql -U devuser -c "CREATE DATABASE gitea;"

Apply perubahan:

docker compose up -d gitea

Traefik otomatis detect container baru dan setup routing-nya. Nggak perlu restart Traefik.


Yang Gue Lakuin Selanjutnya

Setup ini sudah jalan di VPS Hetzner gue selama lebih dari setahun. Beberapa hal yang gue tambahkan setelah setup awal:

  • Automated backup dengan pg_dump yang di-cron setiap malam dan hasilnya di-upload ke MinIO bucket
  • Watchtower untuk auto-update container image secara berkala
  • Netdata untuk monitoring resource usage
  • Tailscale supaya environment ini hanya bisa diakses dari device yang sudah join network gue — nggak perlu expose port ke public internet

Kalau kamu baru mulai, cukup jalankan setup dasar di atas dulu. Setelah familiar dengan cara kerja Docker Compose dan Traefik, baru tambahkan service lain sesuai kebutuhan project kamu.

Satu hal yang konsisten gue lakukan: setiap kali ada perubahan konfigurasi, langsung commit ke private Git repo. Jadi kalau VPS perlu di-rebuild atau gue pindah provider, setup baru bisa jalan dalam hitungan menit dengan infrastruktur yang sudah terdokumentasi dengan baik.