Konvensi penulisan mengurus isi Dockerfile, tetapi satu pertanyaan selalu muncul begitu repository dibuka: berkas ini seharusnya berada di mana? Letak Dockerfile menentukan ukuran context yang dikirim ke daemon, letak compose.yaml menentukan dari mana perintah docker compose dijalankan, dan folder yang tidak dibaca siapa pun berubah menjadi arsitektur implisit yang hanya dipahami penulisnya. Struktur folder adalah keputusan teknis, bukan urusan kerapian. Chapter ini menyajikan tiga pola struktur yang menutup mayoritas kasus — aplikasi tunggal, monorepo multi-service, dan project yang membawa infrastrukturnya sendiri — lalu empat aturan penempatan yang berlaku untuk ketiganya.

Aplikasi Tunggal #

Satu aplikasi, satu Dockerfile, satu compose.yaml — semuanya di akar repository. Pola ini paling sederhana dan sebaiknya tetap dipakai selama repository memang hanya berisi satu aplikasi.

text
myapp/
├── Dockerfile            # membangun image aplikasi
├── .dockerignore         # menjaga context tetap kecil
├── compose.yaml          # stack harian: aplikasi plus database
├── .env.example          # templat variabel, disalin menjadi .env
├── package.json
├── package-lock.json
└── src/
    └── server.js

Dockerfile berada di akar sehingga docker build -t myapp:dev . berjalan tanpa flag tambahan, dan context berukuran hampir sama dengan ukuran repository itu sendiri. Aturan praktisnya: begitu folder src/ mulai memuat lebih dari satu aplikasi yang saling berbeda lifecycle-nya, saatnya pindah ke pola berikutnya.

Monorepo #

Tim yang menumbuhkan beberapa service dalam satu repository memakai pola services: setiap service membawa Dockerfile dan .dockerignore miliknya sendiri, sementara satu compose.yaml di akar menyatukan seluruh stack.

text
toko/
├── compose.yaml              # stack lengkap: web, api, db, cache
├── .env.example
├── shared/
│   └── kontrak-api.json      # skema kontrak yang dipakai lintas service
├── services/
│   ├── web/
│   │   ├── Dockerfile        # membangun service web saja
│   │   ├── .dockerignore     # context web terpisah dari context api
│   │   ├── package.json
│   │   └── src/
│   │       └── server.js
│   └── api/
│       ├── Dockerfile
│       ├── .dockerignore
│       ├── compose.yaml      # stack mini untuk mengembangkan api terpisah
│       ├── go.mod
│       └── main.go

Developer yang mengerjakan api saja tidak wajib menyalakan seluruh stack: compose.yaml lokal di services/api cukup menjalankan api plus database, dan bila butuh stack penuh, satu perintah docker compose up -d dari akar monorepo memuat semuanya. Compose juga menerima path eksplisit dengan flag -f, misalnya docker compose -f services/api/compose.yaml up -d, sehingga skrip CI bisa memilih service mana yang dibangun tanpa berpindah direktori.

Project dan Infrastruktur #

Project yang sudah beroperasi biasanya menambahkan perangkat di sekitar aplikasi: konfigurasi proxy, berkas override produksi, dan skrip operasional. Pola ketiga memisahkan kode aplikasi dari infrastruktur pendukungnya.

text
katalog/
├── compose.yaml              # stack harian developer
├── Dockerfile
├── .dockerignore
├── nginx/
│   └── nginx.conf            # konfigurasi proxy yang di-bind-mount ke container
├── deploy/
│   ├── compose.prod.yaml     # override produksi: restart, limit resource, TLS
│   └── daemon.json           # konfigurasi daemon untuk server produksi
├── scripts/
│   ├── backup.sh             # backup volume database terjadwal
│   └── restore.sh            # dipakai saat disaster drill bulanan
└── src/

Folder deploy/ dan scripts/ berada di luar build context aplikasi berkat .dockerignore, jadi skrip operasional tidak pernah ikut terkirim ke daemon saat build. Pemisahan ini membuat repository tetap satu sumber kebenaran sekaligus tetap ramping sebagai image.

Context Build Monorepo: Dua Opsi #

Dockerfile hanya bisa COPY berkas yang berada di dalam build context, dan keputusan context pada monorepo punya dua kubu. Kedua opsi berikut menghasilkan image yang sama, tetapi konsekuensinya berbeda:

yaml
services:
  web:
    build:
      context: .                        # Opsi A: context akar monorepo
      dockerfile: services/web/Dockerfile
yaml
services:
  web:
    build: ./services/web               # Opsi B: context subfolder service
AspekOpsi A: context akarOpsi B: context subfolder
Ukuran contextbesar; wajib .dockerignore ketat di akarkecil secara bawaan
Akses shared/bisa — COPY shared/ berfungsi langsungtidak bisa — context tidak boleh menjangkau folder di atasnya
.dockerignoresatu berkas mengatur semua servicesatu berkas per service, lebih presisi
Umur cache di CIgoyah — perubahan folder lain ikut mengubah contextstabil — cache service hanya dipengaruhi foldernya sendiri

Opsi A dipilih saat service saling berbagi berkas seperti shared/kontrak-api.json; Opsi B dipilih saat service benar-benar mandiri dan kita ingin cache serta context seminimal mungkin. Section CI/CD Course memakai pola Opsi B untuk service backend toko — seluruh kode service hidup dalam satu folder sehingga context: ./backend cukup — sedangkan struktur shared/ pada diagram di atas adalah contoh tuntutan yang memaksa Opsi A.

Empat Aturan Penempatan #

Empat aturan berikut berlaku untuk ketiga pola struktur di atas, dan urutannya juga urutan pemeriksaan saat code review.

  1. Dockerfile dekat kode yang ia bangun. Developer yang mengubah services/api/main.go harus menemukan services/api/Dockerfile di sebelahnya tanpa mencari — jarak jauh antara kode dan resep buildnya adalah tempat kesalahan build disembunyikan.
  2. Build context sekecil mungkin. Context dikirim utuh ke daemon pada setiap build, jadi node_modules, .git, dan artefak build harus diblokir .dockerignore pada level context yang dipakai.
  3. .env tidak pernah masuk git. Repository hanya boleh memuat .env.example sebagai templat; pengecekan satu baris berikut membuktikannya:
bash
$ git ls-files | grep -E '(^|/)\.env'

Output yang Dihasilkan:

text
.env.example
  1. README adalah runbook, bukan brosur. Minimal empat hal wajib tertulis: cara menjalankan stack, daftar variabel wajib beserta contoh nilainya, cara menjalankan test, dan cara backup-restore data.

Struktur yang rapi membuat kesalahan lebih jarang terjadi, tetapi dunia nyata tetap menyajikan galeri kecelakaan yang sudah akrab: daemon yang mendadak mati, container yang berhenti tanpa pesan, port yang bentrok, disk yang penuh diam-diam. Semua kecelakaan itu punya pola diagnosis yang bisa dihafal, dan chapter berikutnya merangkum sepuluh yang paling sering menjadi Troubleshooting Umum dalam bentuk resep langkah demi langkah.

Referensi #