M
Mr Sugiarto
Developer
06 Sep 2026 5 min read

Aplikasi kita sudah lengkap secara fitur. Part ini yang terakhir berisi kode — kita bungkus jadi Docker image supaya bisa dijalankan konsisten di mesin manapun, dari laptop sampai VPS production.

Dockerfile Multi-Stage

Build Go menghasilkan satu binary statis — kita manfaatkan itu supaya image production tidak perlu membawa seluruh toolchain Go, cukup binary hasil compile-nya saja. Buat Dockerfile:

FROM golang:1.25-alpine AS builder

WORKDIR /app

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

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/blogfiber .

FROM alpine:3.20

RUN apk add --no-cache ca-certificates && \
    addgroup -S blogfiber && adduser -S blogfiber -G blogfiber

WORKDIR /app

COPY --from=builder /app/bin/blogfiber .

RUN mkdir -p ./public/uploads && chown -R blogfiber:blogfiber /app

USER blogfiber

EXPOSE 3000

CMD ["./blogfiber"]

Dua stage di sini:

  • builder — pakai image golang:1.25-alpine (ada compiler Go), download dependency, compile jadi binary statis (CGO_ENABLED=0 supaya tidak bergantung ke library C sistem operasi).
  • Stage final — pakai alpine:3.20 yang jauh lebih kecil (tidak ada compiler Go sama sekali), cuma menyalin binary hasil build dari stage builder. Hasilnya image production yang ringan.

Perhatikan juga addgroup/adduser dan USER blogfiber di baris-baris terakhir. Tanpa ini, container berjalan sebagai root secara default — kalau suatu saat ada celah keamanan di aplikasi yang berhasil dieksploitasi, penyerang otomatis punya akses root di dalam container. Menjalankan proses sebagai user biasa (non-root) adalah lapisan pertahanan tambahan yang murah: kalaupun aplikasi berhasil dieksploitasi, dampaknya terbatas pada hak akses user blogfiber, bukan root.

Buat juga .dockerignore supaya file yang tidak perlu (termasuk .env yang berisi kredensial lokal) tidak ikut ter-copy ke dalam image:

.git
.env
.env.test
tmp
public/uploads/*
!public/uploads/.gitkeep
*.md

Docker Compose: App + MySQL

Buat docker-compose.yml untuk menjalankan aplikasi bersama database-nya dalam satu perintah:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DB_HOST: mysql
      DB_PORT: "3306"
      DB_USERNAME: blogfiber
      DB_PASSWORD: ${DB_PASSWORD}
      DB_DATABASE: blogfiber_dasar
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      mysql:
        condition: service_healthy
    volumes:
      - uploads:/app/public/uploads

  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: blogfiber_dasar
      MYSQL_USER: blogfiber
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      # Tanpa kredensial: mysqladmin ping mengembalikan exit 0 begitu server
      # merespons, bahkan kalau otentikasinya sendiri ditolak - cukup untuk
      # liveness check tanpa perlu menaruh password di argumen command.
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  mysql_data:
  uploads:

Beberapa hal penting:

  • DB_HOST: mysql — di dalam jaringan Docker Compose, service lain dipanggil pakai nama service-nya (bukan 127.0.0.1), Docker otomatis menyediakan DNS internal untuk ini.
  • depends_on dengan condition: service_healthy memastikan container app baru start setelah MySQL benar-benar siap menerima koneksi (bukan cuma "container-nya sudah jalan", karena MySQL butuh waktu inisialisasi sebelum bisa dipakai).
  • Volume mysql_data dan uploads membuat data database dan file upload persisten — tidak hilang setiap kali container di-restart atau di-rebuild.
  • Aplikasi tersambung ke database sebagai user blogfiber (dibuat otomatis oleh image MySQL lewat MYSQL_USER/MYSQL_PASSWORD, dengan hak akses cuma ke database blogfiber_dasar), bukan sebagai root. MYSQL_ROOT_PASSWORD tetap perlu diisi (MySQL mewajibkannya), tapi dipisah dari password aplikasi — root cuma dipakai untuk keperluan admin manual (backup, dst), bukan oleh aplikasi sehari-hari. Kalau kredensial aplikasi bocor, dampaknya terbatas ke satu database itu saja, bukan seluruh server MySQL.
  • Healthcheck mysqladmin ping sengaja tidak menyertakan -u/-p — bukan karena tidak butuh autentikasi, tapi karena mysqladmin ping tetap mengembalikan exit code sukses selama server MySQL merespons, walau otentikasinya sendiri ditolak. Ini menghindari menaruh password sebagai argumen command line, yang kalau tidak, bisa terlihat oleh siapapun yang menjalankan ps aux di dalam container.

Environment Variable untuk Docker

Buat template .env.docker.example:

MYSQL_ROOT_PASSWORD=ganti-dengan-password-root-yang-berbeda-dan-kuat
DB_PASSWORD=ganti-dengan-password-user-blogfiber-yang-kuat
JWT_SECRET=ganti-dengan-secret-yang-panjang-dan-acak-minimal-32-karakter

Copy jadi .env.docker (bukan .env biasa — sengaja dipisah supaya tidak tertukar dengan .env yang dipakai untuk development lokal via go run), isi dengan nilai sungguhan, dan tambahkan ke .gitignore.

Menjalankan

docker compose --env-file .env.docker up --build -d

Cek status dan log:

docker compose ps
docker compose logs -f app

Test API-nya seperti biasa — dari luar container, semuanya tetap diakses lewat http://localhost:3000:

curl http://localhost:3000/
curl -X POST http://localhost:3000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Docker Test","email":"docker@test.local","password":"rahasia123"}'

Migration jalan otomatis setiap kali container app start (persis seperti saat go run biasa), jadi tabel akan otomatis terbentuk begitu MySQL siap.

Deploy ke VPS

Panduan lengkap ada di DEPLOYMENT.md di root project, ringkasannya:

  1. Install Docker & Docker Compose di VPS.
  2. Clone/upload project, siapkan .env.docker dengan kredensial production (jangan pernah pakai nilai contoh).
  3. docker compose --env-file .env.docker up --build -d.
  4. Taruh di belakang reverse proxy (Nginx) untuk domain + HTTPS lewat Certbot.
  5. Untuk update: git pull lalu docker compose up --build -d lagi — migration otomatis ter-apply.

Aplikasi kita sekarang siap dijalankan secara konsisten di mesin manapun, dari laptop untuk development sampai VPS untuk production, tanpa perlu instalasi Go/MySQL manual di server. Part terakhir (Part 20) adalah rangkuman penutup seri ini.

M
Mr Sugiarto

Developer

Bagian dari Series: GoFiber Dasar - Belajar Fundamental Lewat REST API Blog

Belajar konsep-konsep dasar GoFiber (routing, handler, GORM, MySQL, JWT auth) dengan membangun REST API blog sederhana dari nol - lengkap dengan auten...

Lihat Series Lengkap