Devcontainer Konfigurasi Best Practices 2024

by Marcus Chen
Devcontainer Konfigurasi Best Practices 2024

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:

  1. node_modules sebagai named volume — Ini trick yang sering dilupain. Kalau kamu mount seluruh workspace dari host, node_modules yang diinstall di dalam container bisa tertimpa oleh node_modules dari host (yang mungkin punya binary berbeda karena OS berbeda). Dengan named volume terpisah, node_modules di container terisolasi.

  2. volumes: ..:/workspace:cached — Flag cached bikin 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:

  1. Audit remoteUser di semua devcontainer yang kamu punya. Kalau masih root, ganti sekarang.
  2. Cek apakah ada secret yang di-hardcode di devcontainer.json atau docker-compose.yml. Pindahkan ke localEnv atau .env file.
  3. Tambahkan postCreateCommand yang otomatis install dependencies supaya developer baru nggak perlu baca README panjang.
  4. Pin versi image — ganti semua latest dengan versi spesifik.
  5. Test rebuild dari scratch dengan Dev Containers: Rebuild Container Without Cache dari 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?"