Gin + Docker Compose untuk Local Development
9 min read

Gin + Docker Compose untuk Local Development

Menyiapkan environment lokal yang konsisten untuk semua engineer adalah tantangan klasik dalam pengembangan backend Golang + Gin. Tanpa containerisasi, masalah yang sering muncul meliputi versi Go yang berbeda antar mesin, dependency database yang tidak sama, konfigurasi environment yang tidak sinkron, sampai masalah klasik “works on my machine” — kode yang berjalan mulus di laptop satu developer tapi error di laptop developer lain. Docker Compose menjawab masalah ini dengan pendekatan yang sederhana, eksplisit, dan mudah diskalakan, karena seluruh definisi service — API, database, cache, dan dependency lainnya — ditulis dalam satu file yang bisa dijalankan dengan satu perintah saja.

Kenapa Docker Compose untuk Local Development?

Docker Compose memberikan empat manfaat utama untuk local development. Pertama, kamu bisa menjalankan API beserta seluruh dependency-nya (database, cache, dan lainnya) hanya dengan satu perintah, tanpa perlu menginstal dan mengonfigurasi masing-masing secara manual. Kedua, environment jadi sama persis untuk semua developer di tim, karena definisinya tertulis eksplisit di file Compose, bukan tergantung apa yang terinstal di mesin masing-masing. Ketiga, konfigurasi local dan production terpisah dengan jelas — file Compose untuk dev tidak akan pernah tercampur dengan konfigurasi yang dipakai di production. Keempat, setup manual berulang di mesin baru jadi tidak diperlukan lagi.

Dampak praktisnya terasa paling jelas saat onboarding engineer baru. Tanpa Docker Compose, engineer baru biasanya perlu menginstal Go versi tertentu, menyiapkan PostgreSQL lokal, mengatur environment variable satu per satu, dan sering berakhir dengan environment yang sedikit berbeda dari rekan setimnya. Dengan Docker Compose, seluruh proses itu berkurang menjadi satu perintah docker compose up.


Contoh Struktur Proyek

Sebelum menulis konfigurasi Compose, ada baiknya menyamakan struktur project terlebih dahulu. Berikut struktur sederhana untuk project Gin yang akan dipakai sebagai acuan di seluruh artikel ini:

.
├── docker-compose.yml
├── Dockerfile
├── .env
├── go.mod
├── go.sum
├── main.go
└── internal/
    ├── handler
    ├── service
    └── repository

Struktur ini memisahkan internal/handler untuk layer HTTP, internal/service untuk business logic, dan internal/repository untuk akses data — pemisahan layer yang umum dipakai di project Gin berskala menengah. docker-compose.yml dan Dockerfile ditaruh di root project supaya context build mencakup seluruh source code tanpa konfigurasi path tambahan.

File .env di root menyimpan environment variable yang dipakai aplikasi saat runtime — misalnya connection string database, secret key, atau flag konfigurasi lainnya. Memisahkan .env dari kode aplikasi punya dua manfaat: pertama, kredensial tidak ikut ter-commit ke version control (asalkan .env masuk ke .gitignore); kedua, nilai environment variable bisa berbeda antara local, staging, dan production tanpa mengubah satu baris kode pun, karena aplikasi cukup membaca dari environment yang tersedia saat container berjalan.


Dockerfile untuk Aplikasi Gin

Untuk local development, Dockerfile yang dibutuhkan cukup sederhana — tidak perlu multi-stage build atau optimasi ukuran image, karena image ini hanya dipakai secara lokal di mesin developer.

FROM golang:1.21

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o app

EXPOSE 8080

CMD ["./app"]

Beberapa hal yang perlu diperhatikan dari Dockerfile ini:

  • Base image memakai official Go image (golang:1.21), bukan varian Alpine yang lebih ringan, karena ukuran image bukan prioritas di stage ini
  • Dependency di-cache lewat go.mod dan go.sum — keduanya disalin dan di-download sebelum source code lain, sehingga Docker bisa memanfaatkan layer cache dan tidak perlu mengunduh ulang dependency setiap kali ada perubahan kode
  • Build binary dilakukan di dalam container, bukan di host, sehingga versi Go yang dipakai untuk build selalu konsisten dengan yang didefinisikan di image
Untuk production, Dockerfile ini biasanya dimodifikasi menjadi multi-stage build agar ukuran image lebih kecil dan lebih aman. Pembahasan detailnya ada di artikel terpisah tentang Dockerfile untuk berbagai stage.

docker-compose.yml untuk Local Development

Berikut contoh konfigurasi Docker Compose paling sederhana untuk menjalankan aplikasi Gin:

version: '3.9'

services:
  api:
    build: .
    container_name: gin-api
    ports:
      - "8080:8080"
    volumes:
      - .:/app
    env_file:
      - .env
    command: ./app

Setiap baris konfigurasi punya peran spesifik. build: . memberitahu Compose untuk membangun image dari Dockerfile yang ada di direktori saat ini. ports: - "8080:8080" memetakan port 8080 di container ke port 8080 di host, sehingga API bisa diakses dari browser atau tool seperti curl di mesin lokal. volumes: - .:/app menyinkronkan seluruh isi direktori project di host ke direktori /app di dalam container — baris inilah yang membuat perubahan kode di editor langsung tercermin di dalam container tanpa rebuild image. env_file: - .env memuat environment variable dari file .env di host, sehingga kredensial dan konfigurasi tidak perlu ditulis langsung di file Compose.

Volume sangat penting di tahap ini. Tanpa baris volumes, perubahan kode hanya akan tersimpan di image hasil build terakhir — kamu harus menjalankan docker compose up --build setiap kali mengubah satu baris kode, yang jelas tidak praktis untuk iterasi cepat.


Menjalankan Aplikasi

Untuk menjalankan aplikasi pertama kali atau setelah mengubah Dockerfile, gunakan flag --build agar Compose membangun ulang image sebelum menjalankan container:

docker compose up --build

Jika image sudah pernah di-build sebelumnya dan tidak ada perubahan pada Dockerfile atau dependency, kamu bisa langsung menjalankan tanpa flag tersebut:

docker compose up

Setelah container berjalan, API akan tersedia di:

http://localhost:8080

Perbedaan antara kedua perintah ini penting untuk dipahami: --build selalu memicu proses build image dari awal (atau menggunakan cache layer kalau tidak ada perubahan), sementara tanpa --build, Compose langsung menjalankan container dari image yang sudah ada sebelumnya — lebih cepat, tapi berisiko menjalankan versi kode yang sudah usang kalau Dockerfile-nya berubah.

Secara default, docker compose up berjalan di foreground — terminal akan menampilkan log dari semua container secara real-time, dan container akan berhenti begitu kamu menekan Ctrl+C. Untuk menjalankan di background, tambahkan flag -d (detached):

docker compose up -d

Saat berjalan di mode detached, kamu tetap bisa memantau log kapan saja dengan:

docker compose logs -f api

Flag -f (follow) membuat log terus mengalir secara real-time, mirip seperti tail -f pada file log biasa. Kombinasi up -d dan logs -f ini sering jadi workflow harian: jalankan semua service di background sekali di pagi hari, lalu pantau log spesifik service yang sedang dikerjakan.


Hot Reload (Opsional tapi Sangat Direkomendasikan)

Rebuild manual setiap kali ada perubahan kode sangat tidak efisien untuk local development, bahkan dengan volume mount sekalipun — proses ./app yang sudah berjalan tidak akan otomatis membaca ulang kode yang berubah. Solusinya adalah tool hot reload seperti air, yang memonitor perubahan file dan otomatis me-restart aplikasi begitu ada file yang disimpan.

Instal air di dalam Dockerfile:

go install github.com/air-verse/air@latest

Lalu ubah bagian command di Docker Compose agar menjalankan air sebagai pengganti binary langsung:

command: air
sequenceDiagram
    participant Editor as Editor (host)
    participant Volume as Volume Mount
    participant Air as air (container)
    participant App as Aplikasi Gin

    Editor->>Volume: Simpan perubahan file .go
    Volume->>Air: File berubah terdeteksi
    Air->>App: Stop proses lama
    Air->>App: go build + restart
    App-->>Editor: Endpoint siap diakses ulang

Dengan kombinasi air dan volume mount, workflow-nya jadi: file berubah di editor, air mendeteksi perubahan melalui volume yang ter-mount, lalu otomatis me-restart aplikasi dengan kode terbaru. Workflow development jadi jauh lebih cepat dibanding harus menjalankan docker compose up --build berulang kali.

air membaca konfigurasi dari file .air.toml di root project. Kalau file ini tidak ada, air akan memakai konfigurasi default yang umumnya sudah cukup untuk project sederhana — tapi tetap disarankan membuat .air.toml sendiri untuk mengatur direktori yang di-exclude (seperti vendor atau tmp) agar proses watcher tidak ikut memonitor file yang tidak relevan.

Contoh Menambahkan Database (PostgreSQL)

Salah satu keuntungan terbesar Docker Compose adalah kemudahan menambah dependency baru. Misalnya, untuk menambahkan PostgreSQL sebagai database, cukup tambahkan satu service baru:

services:
  api:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - db
    env_file:
      - .env

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app_db
    ports:
      - "5432:5432"

Service api sekarang punya depends_on: - db, yang memastikan container db mulai lebih dulu sebelum container api dijalankan. Service db sendiri memakai image resmi postgres:16 dari Docker Hub, tanpa perlu membuat Dockerfile khusus — environment variable POSTGRES_USER, POSTGRES_PASSWORD, dan POSTGRES_DB otomatis dibaca oleh image ini untuk membuat user dan database saat container pertama kali start.

Dengan pola ini, menambah dependency lain seperti Redis untuk cache atau RabbitMQ untuk message queue hanya soal menambah satu service baru lagi ke file Compose yang sama — tidak perlu mengubah cara service lain dikonfigurasi.

depends_on hanya menjamin urutan start container, bukan menjamin database sudah benar-benar siap menerima koneksi. PostgreSQL butuh waktu beberapa detik untuk inisialisasi setelah container start. Kalau aplikasi langsung mencoba koneksi di awal, pertimbangkan menambahkan retry logic di sisi aplikasi atau memakai healthcheck di Compose untuk menunggu database benar-benar ready.

Untuk mengatasi masalah timing ini secara lebih eksplisit, Docker Compose mendukung kombinasi healthcheck dan depends_on dengan kondisi service_healthy:

services:
  api:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    env_file:
      - .env

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app_db
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 5s
      retries: 5

Dengan konfigurasi ini, Compose akan menjalankan pg_isready setiap 5 detik untuk memeriksa apakah PostgreSQL sudah siap menerima koneksi. Container api tidak akan dijalankan sampai db benar-benar berstatus healthy, bukan hanya sekadar sudah running. Pendekatan ini jauh lebih robust dibanding depends_on biasa, terutama untuk database yang butuh waktu inisialisasi lebih lama atau saat menjalankan beberapa service yang saling bergantung sekaligus.


Best Practice untuk Local Development

Beberapa praktik berikut membantu menjaga setup Docker Compose tetap sehat dan tidak menimbulkan masalah di kemudian hari:

  • Gunakan Docker Compose hanya untuk local — jangan dipakai langsung di production tanpa modifikasi
  • Pisahkan file konfigurasi untuk production, baik itu Dockerfile maupun Compose-nya sendiri
  • Jangan commit file .env ke version control, karena biasanya berisi kredensial dan konfigurasi sensitif
  • Gunakan volume agar tidak perlu rebuild terus-menerus setiap ada perubahan kode
  • Jangan over-optimasi Dockerfile untuk dev — multi-stage build dan minimasi layer adalah kebutuhan production, bukan local development

Prinsip yang menyatukan semua poin ini sederhana: local development harus cepat dan nyaman, bukan sempurna. Optimasi yang berguna di production justru bisa menghambat produktivitas kalau diterapkan di tahap yang salah.

Selain kelima poin di atas, ada beberapa kebiasaan kecil yang juga membantu menjaga setup tetap rapi dalam jangka panjang:

KebiasaanAlasan
Sediakan .env.exampleMemberi acuan environment variable apa saja yang dibutuhkan, tanpa membocorkan nilai aslinya
Beri nama service yang deskriptifapi, db, cache lebih mudah dipahami dibanding service1, service2
Bersihkan volume yang tidak terpakaidocker compose down -v menghapus volume sekaligus, berguna saat ingin reset state database lokal
Dokumentasikan port yang dipakaiMencegah konflik port antar project yang berjalan bersamaan di mesin yang sama

.env.example khususnya penting untuk onboarding — file ini berisi nama variable tanpa nilai sensitif (misalnya DB_PASSWORD= kosong atau dengan placeholder), sehingga engineer baru tahu variable apa yang perlu diisi tanpa harus menebak-nebak atau bertanya ke rekan setim.


Ringkasan

  • Docker Compose menyatukan API dan seluruh dependency-nya (database, cache, dll) dalam satu file yang bisa dijalankan dengan satu perintah.
  • Volume mount (volumes: - .:/app) adalah kunci agar perubahan kode di host langsung tercermin di dalam container.
  • Kombinasikan volume mount dengan tool hot reload seperti air agar aplikasi otomatis restart setiap ada perubahan kode.
  • Menambah dependency baru (database, cache, message queue) cukup dengan menambah satu service baru di file Compose yang sama.
  • depends_on hanya mengatur urutan start container, bukan menjamin service sudah siap menerima request.
  • Jangan commit file .env, dan jangan pakai konfigurasi Compose yang sama untuk local dan production.
  • Local development memprioritaskan kecepatan iterasi — hindari optimasi yang sebenarnya hanya relevan untuk production.

Portofolio