Masalahnya: "Works on My Machine"
Kamu pernah push code, CI merah, tapi di lokal hijau semua? Atau onboarding developer baru butuh setengah hari cuma buat install dependency yang benar? Itu bukan masalah skill — itu masalah environment yang nggak konsisten.
Devcontainer hadir buat solve masalah ini. Tapi kalau konfigurasinya asal-asalan, kamu cuma mindahin masalah dari "works on my machine" ke "works in my devcontainer". Artikel ini bahas devcontainer konfigurasi best practices yang gue pakai di proyek-proyek solo maupun kolaborasi.
Apa Itu Devcontainer dan Kenapa Konfigurasinya Penting
Devcontainer adalah spec dari Microsoft yang mendefinisikan development environment lewat file .devcontainer/devcontainer.json. VS Code, GitHub Codespaces, dan Cursor semuanya support ini. Intinya: environment kamu jalan di dalam container Docker, bukan langsung di host machine.
Kenapa konfigurasinya krusial? Karena devcontainer yang buruk punya masalah nyata:
- Build lambat — image rebuild tiap kali buka project karena layer cache-nya nggak efisien.
- Permission error — file yang dibuat di dalam container punya owner
root, susah diedit dari host. - Extension VS Code hilang — tiap developer harus install ulang extension secara manual.
- Secret bocor — env var di-hardcode langsung di
devcontainer.json.
Mari kita benerin satu per satu.
Struktur Folder yang Benar
Jangan taruh semua config di satu file. Pisahkan concerns-nya:
.devcontainer/
├── devcontainer.json # Konfigurasi utama
├── Dockerfile # Custom image (kalau perlu)
└── docker-compose.yml # Kalau butuh multi-service
Kalau project kamu cuma butuh satu service (misal: backend Node.js tanpa database), pakai image langsung di devcontainer.json. Kalau butuh PostgreSQL, Redis, atau service lain, pakai docker-compose.yml.
devcontainer.json yang Solid
Ini template yang gue pakai untuk project Node.js/TypeScript:
{
"name": "Node.js Dev",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"remoteUser": "node",
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-next",
"bradlc.vscode-tailwindcss"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"terminal.integrated.defaultProfile.linux": "bash"
}
}
},
"forwardPorts": [3000, 5432],
"postCreateCommand": "npm install",
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,consistency=cached"
]
}
Beberapa hal yang perlu diperhatikan:
remoteUser: "node" — Ini penting banget. Jangan jalankan container sebagai root. Image resmi Node.js sudah punya user node dengan UID 1000. Kalau kamu pakai image lain, buat non-root user di Dockerfile.
features — Ini cara yang lebih clean untuk install tooling tambahan dibanding nulis RUN apt-get install di Dockerfile. Devcontainer Features punya versioning dan caching yang lebih baik.
mounts untuk SSH — Dengan mount SSH key dari host, kamu bisa git push dari dalam container tanpa copy-paste key. Ini lebih aman daripada hardcode credential.
Dockerfile yang Cache-Friendly
Ini gotcha yang paling sering bikin build lambat: urutan layer yang salah.
# BURUK — npm install jalan ulang tiap ada perubahan source code
FROM node:20-bookworm-slim
WORKDIR /workspace
COPY . .
RUN npm install
# BAGUS — npm install hanya jalan ulang kalau package.json berubah
FROM node:20-bookworm-slim
# Install system deps dulu (layer ini jarang berubah)
RUN apt-get update && apt-get install -y \
git \
curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
# Copy package files dulu, BARU install
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Source code di-copy terakhir
COPY . .
# Switch ke non-root user
USER node
Aturannya simpel: layer yang paling jarang berubah harus paling atas. System dependencies > package manifest > source code.
Gotcha yang gue kena: pakai npm install bukan npm ci di Dockerfile. npm ci lebih deterministik karena strict terhadap package-lock.json dan lebih cepat di CI/CD environment.
Handle Secrets dengan Benar
Ini yang paling sering salah. Jangan pernah taruh secret di devcontainer.json:
// JANGAN LAKUKAN INI
"containerEnv": {
"DATABASE_URL": "postgresql://user:password123@localhost:5432/mydb",
"API_KEY": "sk-1234567890abcdef"
}
Cara yang benar: pakai localEnv untuk forward env var dari host machine:
"containerEnv": {
"DATABASE_URL": "${localEnv:DATABASE_URL}",
"API_KEY": "${localEnv:API_KEY}"
}
Terus di host machine kamu, set env var-nya di ~/.bashrc atau ~/.zshrc:
export DATABASE_URL="postgresql://user:password123@localhost:5432/mydb"
export API_KEY="sk-1234567890abcdef"
Alternatif yang lebih rapi: pakai file .env di root project (yang sudah di-.gitignore) dan load lewat postCreateCommand:
"postCreateCommand": "cp .env.example .env && npm install"
Multi-Service dengan Docker Compose
Kalau project kamu butuh database atau cache, jangan install PostgreSQL langsung di container devcontainer. Pisahkan dengan docker-compose.yml:
# .devcontainer/docker-compose.yml
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
- node_modules:/workspace/node_modules
command: sleep infinity
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/devdb
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: devdb
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
node_modules:
postgres_data:
redis_data:
Perhatikan dua hal penting di sini:
-
node_modulessebagai named volume — Ini trick yang sering dilupain. Kalau kamu mount seluruh workspace dari host,node_modulesyang diinstall di dalam container bisa tertimpa olehnode_modulesdari host (yang mungkin punya binary berbeda karena OS berbeda). Dengan named volume terpisah,node_modulesdi container terisolasi. -
volumes: ..:/workspace:cached— Flagcachedbikin file sync dari host ke container lebih cepat di macOS. Di Linux ini nggak berpengaruh, tapi nggak ada ruginya ditambahkan.
Update devcontainer.json untuk pakai compose file ini:
{
"name": "Node.js + PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"remoteUser": "node",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg"
]
}
},
"forwardPorts": [3000],
"postCreateCommand": "npm ci && npx prisma migrate dev"
}
postCreateCommand vs postStartCommand
Banyak yang bingung kapan pakai yang mana:
postCreateCommand— Jalan sekali saat container pertama kali dibuat. Cocok untuk:npm install, database migration, generate SSL cert lokal.postStartCommand— Jalan setiap kali container start. Cocok untuk: start background service, sync config.postAttachCommand— Jalan setiap kali VS Code attach ke container. Cocok untuk: print welcome message, check env vars.
Contoh penggunaan yang masuk akal:
"postCreateCommand": "npm ci && npx prisma generate && npx prisma migrate deploy",
"postStartCommand": "echo 'Container started. Run npm run dev to start the server.'"
Gotcha: kalau postCreateCommand gagal di tengah jalan, container tetap terbuat tapi state-nya setengah jalan. Tambahkan set -e kalau kamu pakai shell script:
"postCreateCommand": "bash -c 'set -e && npm ci && npx prisma migrate deploy'"
Optimasi untuk Rebuild yang Lebih Cepat
Beberapa tips yang bikin rebuild devcontainer nggak bikin frustrasi:
Pakai image yang spesifik, bukan latest:
# BURUK
FROM node:latest
# BAGUS
FROM node:20.11.1-bookworm-slim
Image latest bisa berubah kapan saja dan bikin build nggak reproducible.
Manfaatkan Dev Container Features daripada manual install:
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.11"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
}
Features ini di-cache secara terpisah, jadi kalau kamu tambah satu feature baru, nggak perlu rebuild semua layer dari awal.
Pisahkan dev dependencies dari runtime:
Kalau kamu juga pakai devcontainer untuk staging/production build (jarang, tapi ada yang begini), pastikan Dockerfile punya multi-stage build. Untuk pure dev environment, ini less critical, tapi tetap good practice.
Apa yang Gue Lakuin Sekarang
Setup devcontainer konfigurasi best practices ini bukan one-time thing. Gue maintain file .devcontainer/ di setiap project dan treat dia seperti production code — ada review kalau ada yang mau ubah, ada changelog kalau ada breaking change (misal: upgrade Node.js version).
Beberapa langkah konkret yang bisa kamu mulai sekarang:
- Audit
remoteUserdi semua devcontainer yang kamu punya. Kalau masihroot, ganti sekarang. - Cek apakah ada secret yang di-hardcode di
devcontainer.jsonataudocker-compose.yml. Pindahkan kelocalEnvatau.envfile. - Tambahkan
postCreateCommandyang otomatis install dependencies supaya developer baru nggak perlu baca README panjang. - Pin versi image — ganti semua
latestdengan versi spesifik. - Test rebuild dari scratch dengan
Dev Containers: Rebuild Container Without Cachedari Command Palette VS Code. Kalau build-nya lebih dari 5 menit, ada yang perlu dioptimasi di layer ordering.
Devcontainer yang dikonfigurasi dengan benar harusnya bikin onboarding developer baru jadi: clone repo → buka di VS Code → klik "Reopen in Container" → langsung bisa coding. Nggak ada lagi "eh, kamu pakai Node versi berapa?"