Banyak deployment Docker gagal bukan saat image dibuat, tetapi ketika container harus hidup di server, menerima traffic HTTPS, menyimpan data, dan di-update tanpa downtime yang tidak terencana. Docker menyelesaikan packaging aplikasi. Ia tidak otomatis menyelesaikan network, secret, backup, observability, atau rollback.
Artikel ini memakai satu pola yang realistis: aplikasi web berjalan di Docker Compose, database tidak dibuka ke internet, Caddy menjadi reverse proxy HTTPS, dan deployment dilakukan dari GitHub Actions ke VPS. Polanya bisa disesuaikan untuk GitLab CI, GitLab Runner, atau platform lain.
Arsitektur target
Alur request yang dituju:
Client HTTPS
|
v
Caddy :80/:443
|
v
app container :3000
|
+--> postgres container :5432 (private Docker network)
+--> redis container :6379 (private Docker network, optional)
Hanya Caddy yang membuka port publik 80 dan 443. Port aplikasi, PostgreSQL, dan Redis cukup berada di network Docker. Ini mengurangi attack surface dan mencegah client luar mengakses database secara langsung.
Untuk production kecil, Docker Compose cukup. Kubernetes baru memberi manfaat ketika kebutuhan scheduling, autoscaling, multi-node, dan service discovery memang sudah ada. Memakai Kubernetes terlalu awal menambah control plane, RBAC, monitoring, dan biaya operasi.
Prasyarat server
Gunakan VPS Linux dengan resource sesuai aplikasi. Untuk aplikasi Node.js kecil, 2 vCPU dan 4 GB RAM bisa menjadi titik awal, tetapi database, build, dan traffic menentukan kebutuhan sebenarnya.
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl git ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
SSH sebaiknya memakai key, bukan password. Jangan menutup port 22 sebelum koneksi key baru teruji. Firewall bukan pengganti authentication; ia hanya membatasi jalur masuk.
Install Docker Engine
Pada Ubuntu, gunakan repository resmi Docker agar versi dan package berasal dari sumber yang bisa diaudit:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world
Menambahkan user ke group docker memang membuat command tidak perlu sudo, tetapi group tersebut secara praktis memberikan hak setara root. Pada server multi-user, pertimbangkan tetap memakai sudo docker atau batasi siapa yang mendapat akses.
Struktur project
Pisahkan source code, konfigurasi deployment, dan data runtime:
my-app/
├── Dockerfile
├── compose.yml
├── .dockerignore
├── .env.example
├── package.json
├── package-lock.json
├── src/
└── infra/
└── Caddyfile
.env.example boleh masuk Git karena hanya berisi nama variable. .env production harus berada di server atau secret manager dan masuk .gitignore.
.env
.env.*
!.env.example
node_modules
.next
Membuat Dockerfile yang layak production
Gunakan multi-stage build agar dependency development dan source yang tidak diperlukan tidak ikut masuk runtime image:
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
USER node
EXPOSE 3000
CMD ["npm", "start"]
Detail pentingnya bukan hanya ukuran image. Build harus reproducible melalui lockfile, proses runtime tidak berjalan sebagai root, dan runtime image tidak membawa tool build yang tidak diperlukan. Jika framework memakai standalone output, salin hanya bundle standalone agar ukuran lebih kecil.
.dockerignore mencegah credential, cache, dan file lokal masuk build context:
.git
.env*
node_modules
.next
coverage
*.log
Dockerfile*
compose*.yml
Docker Compose untuk aplikasi
Contoh berikut memakai app, PostgreSQL, dan Redis. Database dan Redis tidak memiliki ports, sehingga hanya dapat diakses melalui network internal Compose.
services:
app:
build:
context: .
target: runtime
restart: unless-stopped
env_file: .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks: [internal]
expose:
- "3000"
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks: [internal]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks: [internal]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
networks:
internal:
volumes:
postgres_data:
redis_data:
depends_on membantu urutan start, tetapi bukan readiness aplikasi. Aplikasi tetap harus melakukan retry koneksi database dengan batas waktu dan backoff. Healthcheck juga tidak menggantikan migration dan smoke test.
Environment dan secret
Buat .env.example seperti ini:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://app:change-me@postgres:5432/app
REDIS_URL=redis://redis:6379
APP_URL=https://app.example.com
Di server, buat .env dengan permission terbatas:
sudo install -m 600 /dev/null /opt/my-app/.env
sudoedit /opt/my-app/.env
Jangan memasukkan .env ke image dengan COPY . . tanpa .dockerignore. Untuk cloud credential, OIDC atau secret manager lebih baik daripada static key panjang umur. Jika secret pernah masuk Git, rotate secret tersebut; menghapus commit tidak menghapus salinan dari clone atau log.
Menjalankan lokal
Sebelum menyentuh server, uji jalur utama di lokal:
docker compose config
docker compose build --no-cache
docker compose up -d
docker compose ps
curl -f http://localhost:3000/health
docker compose logs --tail=100 app
docker compose config mendeteksi kesalahan interpolasi dan struktur YAML. Endpoint /health harus ringan, tidak bergantung pada proses mahal, dan membedakan proses hidup dari dependency yang siap dipakai. Tambahkan endpoint readiness terpisah jika diperlukan.
Untuk menghentikan stack tanpa menghapus volume:
docker compose down
Jangan memakai down -v pada production kecuali memang berniat menghapus volume dan sudah memiliki restore yang teruji.
Reverse proxy dengan Caddy
Caddy menangani TLS dan meneruskan request ke app melalui network Docker atau port lokal. Contoh saat Caddy juga dijalankan dalam Compose:
app.example.com {
encode gzip zstd
reverse_proxy app:3000
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
}
}
Jika Caddy berjalan di host, publish app hanya ke loopback:
ports:
- "127.0.0.1:3000:3000"
Jangan memakai 0.0.0.0:3000 jika app hanya perlu diakses reverse proxy. DNS A atau AAAA domain harus menunjuk ke VPS, dan port 80/443 harus dapat diakses agar sertifikat TLS diterbitkan.
Domain dan HTTPS
Urutannya:
- Buat DNS record ke IP VPS.
- Pastikan firewall mengizinkan TCP 80 dan 443.
- Pastikan Caddy dapat reach app.
- Jalankan Caddy dan lihat log certificate.
- Tes
curl -I https://app.example.com.
dig +short app.example.com
curl -I https://app.example.com
docker compose logs --tail=100 caddy
Jangan membuat redirect HTTPS di banyak layer tanpa memahami proxy chain. Aplikasi harus mempercayai header forwarded hanya dari proxy yang kamu kontrol, dan URL canonical harus memakai domain production yang eksplisit.
Migration database
Migration harus menjadi langkah deployment yang dapat diulang dan aman. Pola umum:
docker compose run --rm app npm run db:migrate
Gunakan perubahan backward-compatible: tambahkan kolom nullable dulu, deploy kode yang memahami format lama dan baru, backfill, lalu hapus format lama pada release berikutnya. Migration yang langsung rename atau drop kolom dapat mematahkan container lama saat rolling deployment.
Jangan menjalankan migration otomatis dari setiap replica yang start tanpa lock atau mekanisme concurrency. Satu job release lebih mudah diaudit daripada beberapa container berlomba melakukan perubahan schema.
Deployment dari GitHub Actions
Pipeline sebaiknya membangun image dengan tag commit SHA, push ke registry, lalu server menarik image yang sama. Build ulang di server membuat hasil deploy bergantung pada source tree dan environment server.
name: deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy over SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /opt/my-app
git fetch origin main
git reset --hard origin/main
docker compose build app
docker compose run --rm app npm run db:migrate
docker compose up -d app
curl --fail --retry 10 http://127.0.0.1:3000/health
Untuk sistem yang lebih matang, push image ke GHCR atau registry private, lalu deployment hanya melakukan pull berdasarkan digest. Pin action ke commit SHA untuk pipeline sensitif dan batasi permission token.
Go live checklist
Sebelum membuka domain ke user:
- [ ] SSH key tested dan password auth sesuai kebijakan.
- [ ] UFW hanya membuka 22, 80, dan 443 sesuai kebutuhan.
- [ ] Docker Engine dan Compose plugin terpasang dari repository resmi.
- [ ]
.envtidak masuk Git atau image. - [ ] Container runtime tidak berjalan sebagai root.
- [ ] Database dan Redis tidak dipublish ke internet.
- [ ] Volume database memiliki backup dan restore test.
- [ ] Healthcheck dan smoke test tersedia.
- [ ] Caddy menerbitkan sertifikat valid.
- [ ] Log dan restart policy sudah diverifikasi.
- [ ] Migration backward-compatible.
- [ ] Rollback image dan konfigurasi sudah diketahui.
- [ ] Resource limit dan disk alert sudah ada.
Operasi setelah go live
Lihat status dan log tanpa mengikuti semua output secara buta:
docker compose ps
docker compose logs --since=15m app
docker stats --no-stream
curl -fsS https://app.example.com/health
Tetapkan log retention. Container log yang tidak dibatasi dapat memenuhi disk dan mematikan Docker, database, serta aplikasi sekaligus. Pantau disk Docker dengan docker system df, tetapi jangan menjalankan docker system prune -a secara otomatis tanpa memahami image yang masih dibutuhkan untuk rollback.
Backup database harus diuji restore. Backup yang hanya pernah dibuat tetapi belum pernah dipulihkan belum membuktikan recovery. Simpan backup di lokasi berbeda dan gunakan encryption sesuai kebutuhan data.
Rollback
Rollback harus mengembalikan artifact, konfigurasi, dan bila perlu schema ke kombinasi yang kompatibel. Menjalankan git checkout lalu docker compose up tidak selalu cukup jika image sudah berubah atau migration tidak reversible.
Simpan release identifier:
docker image inspect my-app:release-abc123 --format '{{.Id}}'
docker compose images
Pada deployment berbasis registry, rollback berarti mengubah image tag/digest ke release sebelumnya, menjalankan smoke test, lalu mengamati error rate. Rollback database adalah masalah terpisah; hindari migration destruktif supaya aplikasi lama masih dapat hidup selama rollback.
Kesalahan yang sering terjadi
- Membuka port 5432, 6379, atau 3000 ke publik tanpa alasan.
- Menyimpan secret di
docker-compose.ymlatau repository. - Menggunakan
latestsehingga rollback tidak deterministik. - Menghapus volume saat membersihkan container.
- Menjalankan semua proses sebagai root.
- Tidak punya healthcheck, smoke test, atau endpoint readiness.
- Menganggap
depends_onberarti database sudah siap. - Build di server production tanpa artifact yang bisa dilacak.
- Menjalankan migration dari semua replica.
- Tidak membatasi log dan penggunaan disk.
Rekomendasi desain
Untuk satu aplikasi dan satu VPS, Docker Compose plus Caddy sudah cukup jika backup, monitoring, firewall, dan prosedur rollback dikerjakan serius. Untuk beberapa server, registry dan deployment controller mulai memberi manfaat. Untuk banyak service yang membutuhkan scheduling dan autoscaling, evaluasi Kubernetes setelah batas Compose benar-benar terasa.
Docker bukan tujuan akhir. Ia adalah boundary packaging dan process isolation. Go live yang sehat memerlukan boundary network, credential, data persistence, observability, dan recovery yang sama jelasnya.



