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_dumpyang 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.