Bagian 7: Menyiapkan Environment Dev dan Production di Dokploy

· 19 menit baca

English
//TUTORIAL · 7 DARI 10 — Secure CI/CD Pipeline dengan Go, GitHub Actions, Vault, dan Dokploy

Sampai Bagian 6, semua yang kita bangun masih ada di GitHub: workflow PR yang memeriksa kode, dan ruleset yang menolak merge kalau ada check merah. Belum ada satu pun container yang jalan di server.

Di bagian ini kita menyiapkan tempat aplikasinya jalan. Saya pakai Dokploy di VPS, dengan dua service Compose: satu untuk dev, satu untuk production. Masing-masing punya kredensial Vault sendiri, domain sendiri dengan HTTPS, dan yang paling penting: auto-deploy-nya mati. Yang boleh memicu deploy hanya CI, dan workflow CI-nya baru kita tulis di Bagian 8.

Ada satu masalah ayam dan telur di sini. Compose dev menarik image ghcr.io/dimss113/secure-cicd-go:development, padahal image itu baru akan dibuat CI di Bagian 8. Jadi untuk dev saya buat satu image bootstrap dari laptop, dengan aturan ketat. Production sengaja belum di-deploy sama sekali sampai Bagian 8.

Bonusnya, deploy dev ini sekaligus jadi pembuktian pertama Bagian 4 dengan Vault asli, bukan Vault dev lokal.

Hasil akhirnya

text
Dokploy (VPS)└── Project secure-cicd-go    ├── secure-cicd-go-dev   (Compose, branch development, docker-compose.development.yml)    │     env   : VAULT_ADDR, VAULT_ROLE_ID, VAULT_SECRET_ID  ← AppRole DEV    │     domain: https://secure-cicd-dev.domainkamu.com → app:8080 (Let's Encrypt)    │     status: jalan (image bootstrap)    │    └── secure-cicd-go-prod  (Compose, branch main, docker-compose.production.yml)          env   : VAULT_ADDR, VAULT_ROLE_ID, VAULT_SECRET_ID  ← AppRole PRODUCTION          domain: https://secure-cicd.domainkamu.com → app:8080 (dipasang di Bagian 8)          status: belum di-deployAuto deploy: MATI di keduanya. Yang memicu deploy hanya CI (Bagian 8).

Sebelum mulai, pastikan dua hal. DNS dari Bagian 1 sudah benar (dig +short secure-cicd-dev.domainkamu.com harus mengembalikan IP VPS), dan Vault CLI di terminal kamu sudah login sebagai admin seperti di Bagian 2. Terminal itu dipakai terus di bagian ini.

Apa yang Dokploy lakukan saat tombol Deploy ditekan

Sebelum klik apa pun, saya mau paham dulu apa yang sebenarnya terjadi. Dokumentasinya nggak terlalu detail, jadi saya baca langsung source code Dokploy v0.30.10 (bagian builder compose). Urutannya kira-kira begini:

text
1. git clone repo (branch yang dipilih) → /etc/dokploy/compose/<appName>/code/2. Baca compose file, lalu SUNTIKKAN:     • label Traefik (router, domain, HTTPS, certresolver) ke service yang punya domain     • network dokploy-network ke service itu3. Tulis isi tab Environment → file .env di sebelah compose file4. Jalankan:     docker compose -p <appName> --env-file <dir>/.env -f <compose> up -d --build --remove-orphans5. Traefik melihat label baru → https://domain diteruskan ke container app:8080

Bayangkan Dokploy itu tukang yang datang ke server. Dia mengambil cetak biru dari Git, menempelkan papan nama (label domain), menaruh kunci di laci (.env), lalu menyalakan mesin.

Ini compose dev dari repo. File production-nya sama persis, bedanya cuma APP_ENV: production dan tag image :production:

docker-compose.development.yml yaml
# Development stack on Dokploy. Dokploy writes VAULT_ADDR, VAULT_ROLE_ID and VAULT_SECRET_ID to .env# next to this file. APP_ENV is pinned here, never read from .env, so a wrong value cannot pull# another environment's secrets.services:  vault-agent:    build: ./vault-agent    restart: unless-stopped    environment:      APP_ENV: development      VAULT_ADDR: ${VAULT_ADDR:?set VAULT_ADDR}      VAULT_ROLE_ID: ${VAULT_ROLE_ID:?set VAULT_ROLE_ID}      VAULT_SECRET_ID: ${VAULT_SECRET_ID:?set VAULT_SECRET_ID}    volumes:      - vault_auth:/vault/auth      - config_vol:/vault/out    read_only: true    cap_drop: [ALL]    security_opt: [no-new-privileges:true]   app:    image: ghcr.io/dimss113/secure-cicd-go:development    pull_policy: always    restart: unless-stopped    depends_on:      vault-agent:        condition: service_healthy    # Only the rendered config; the AppRole volume is never mounted here.    volumes:      - config_vol:/run/config:ro    expose:      - "8080"    read_only: true    cap_drop: [ALL]    security_opt: [no-new-privileges:true]    networks:      - dokploy-network # tmpfs: secrets live in RAM only and vanish when the stack stops.volumes:  vault_auth:    driver_opts: {type: tmpfs, device: tmpfs, o: "uid=65532,gid=65532,mode=0700"}  config_vol:    driver_opts: {type: tmpfs, device: tmpfs, o: "uid=65532,gid=65532,mode=0700"} networks:  # Created by Dokploy; Traefik routes the domain to the app over it.  dokploy-network:    external: true

Hubungannya dengan langkah Dokploy di atas:

Baris di compose

Diisi oleh

VAULT_ADDR: ${VAULT_ADDR:?set VAULT_ADDR}

.env dari tab Environment. Kalau kosong, compose berhenti dengan pesan set VAULT_ADDR

APP_ENV: development

Dipatok di file, bukan dari .env. Salah isi tab Environment nggak bisa membuat dev membaca path secret production

build: ./vault-agent

Karena --build, image vault-agent dibangun di server dari folder repo

image: ...:development + pull_policy: always

Ditarik dari GHCR setiap deploy. Image ini harus sudah ada

networks: dokploy-network

Network milik Traefik. Dokploy juga menambahkannya otomatis, jadi nggak bentrok

Perhatikan juga: hanya app yang ada di dokploy-network. Network itu jalur masuk dari internet lewat Traefik, dan yang perlu menerima request cuma app. vault-agent cukup di network default project, yang tetap bisa keluar ke Vault. Setiap container hanya terhubung ke yang ia butuhkan.

Dan app memakai expose, bukan ports. ports: "8080:8080" membuka port di IP publik VPS, jadi orang bisa akses http://<IP_VPS>:8080 tanpa HTTPS dan tanpa lewat Traefik. expose hanya membuka port di dalam network Docker, jadi satu-satunya pintu masuk adalah Traefik.

Perhatian

Deploy bukan berarti restart

Perintahnya docker compose up -d. Compose hanya membuat ulang container kalau ada yang berubah: image baru, env berubah, atau konfigurasi service berubah. Tekan Deploy tanpa perubahan apa pun, container nggak di-restart. Ini penting nanti waktu rotasi secret.

Buat project dan service Compose dev

Di panel Dokploy: Projects → Create Project, nama secure-cicd-go. Dokploy versi baru punya environment di dalam project (defaultnya production). Kalau ada, buat juga environment development, lalu taruh service dev di sana dan service prod di production.

Kenapa satu project dengan dua service, bukan satu service yang ganti-ganti branch? Karena setiap service Compose punya env sendiri (AppRole sendiri), domain sendiri, dan appName sendiri yang jadi prefix container dan volume di server. Dev dan production terpisah total: volume tmpfs beda, kredensial beda.

Sekarang di environment development: Create Service → Compose.

Field

Nilai

Kenapa

Name

secure-cicd-go-dev

Nama yang terlihat di UI

App Name

biarkan yang dibuat otomatis

Jadi nama project compose (-p), prefix container dan volume

Compose Type

Docker Compose

Bukan Stack (Swarm). Kita cuma satu server

Buka service-nya, tab General, pilih Provider: Git:

Field

Nilai

Repository URL

https://github.com/dimss113/secure-cicd-go.git

Branch

development

Compose Path

./docker-compose.development.yml

SSH Key

kosong (repo publik)

Save. Lalu pastikan Autodeploy OFF, dan jangan pasang webhook Dokploy di GitHub.

Kenapa provider "Git", bukan "GitHub"

Provider GitHub memasang GitHub App milik Dokploy ke akun kamu. App itu dapat akses ke repo dan bisa memicu deploy setiap ada push. Provider Git cuma melakukan git clone ke URL publik. Dokploy nggak punya akses apa pun ke akun GitHub kamu.

Ini least privilege: Dokploy cukup bisa membaca repo publik. Kalau server Dokploy dibobol, penyerang nggak sekalian dapat akses ke GitHub.

Kenapa auto-deploy harus mati

Kalau hidup, setiap push ke development langsung di-deploy oleh Dokploy, tanpa menunggu hasil Trivy di CI. Image yang belum di-scan bisa jalan di server.

Dengan auto-deploy mati, satu-satunya jalan deploy adalah CI, dan CI baru memanggil API Dokploy setelah semua gate lolos. Ini yang di Bagian 1 saya sebut anti-goal: pipeline nggak boleh bisa dilewati.

Jujur saja, ini nggak menutup semuanya. Admin Dokploy tetap bisa klik Deploy manual dari UI, karena Dokploy nggak punya opsi "deploy hanya dari CI". Mitigasinya: batasi siapa yang punya akun Dokploy, dan smoke test di Bagian 8 memeriksa /version sama dengan commit yang di-scan, jadi deploy yang menyimpang kelihatan.

Isi kredensial AppRole dev

Tujuannya: membuat satu secret_id baru khusus Dokploy dev, menempelkannya ke Dokploy tanpa pernah tampil di layar, dan mencabut semua secret_id lama sisa uji di Bagian 2.

Cabut secret_id lama

bash
ROLE=secure-cicd-go-developmentvault list auth/approle/role/$ROLE/secret-id

Yang keluar adalah daftar accessor, semacam nomor seri tiap secret_id. Accessor bukan rahasia dan nggak bisa dipakai login. Kalau keluar No value found, berarti nggak ada yang aktif dan langkah ini bisa dilewati. Kalau ada, cabut semuanya:

bash
for a in $(vault list -format=json auth/approle/role/$ROLE/secret-id | jq -r '.[]'); do  vault write auth/approle/role/$ROLE/secret-id-accessor/destroy secret_id_accessor="$a"done

Loop ini mengambil tiap accessor dari output JSON, lalu menghancurkan secret_id pemiliknya. Kenapa repot? Setiap secret_id yang masih aktif itu satu "password" yang masih berlaku. Yang nggak dipakai cuma menambah peluang bocor.

Perhatian

Stack lokal Bagian 4 ikut kena

Kalau stack lokal dengan Vault asli masih jalan, ia tetap hidup dengan token yang sudah didapat, tapi gagal login saat di-restart. Itu benar dan memang diharapkan. Buat secret_id baru kalau mau menguji lokal lagi.

Tempel ke Dokploy lewat clipboard

Di service secure-cicd-go-dev → tab Environment, ketik kerangkanya:

text
VAULT_ADDR=https://vault.example.comVAULT_ROLE_ID=VAULT_SECRET_ID=

Ambil role_id langsung ke clipboard, lalu tempel di belakang VAULT_ROLE_ID=:

bash
vault read -field=role_id auth/approle/role/$ROLE/role-id | pbcopy

Buat secret_id baru, juga langsung ke clipboard, lalu tempel di belakang VAULT_SECRET_ID=:

bash
vault write -f -field=secret_id auth/approle/role/$ROLE/secret-id | pbcopy

Terakhir kosongkan clipboard:

bash
pbcopy < /dev/null

Pastikan opsi Create Env File (kalau ada di versimu) ON, lalu Save.

Kenapa | pbcopy, bukan read -rs seperti di Bagian 2? Di sana nilainya perlu masuk variabel shell untuk perintah berikutnya. Di sini tujuannya form web, jadi jalur terpendeknya clipboard. Hasilnya sama-sama aman: nilainya nggak masuk history shell, nggak tampil di layar (aman dari screenshot dan screen sharing), dan nggak pernah ditulis ke file.

Perhatian

Clipboard manager

Kalau kamu pakai clipboard manager seperti Raycast, Paste, atau Maccy, aplikasi itu menyimpan riwayat clipboard. Hapus entrinya dari riwayat, atau matikan sementara.

Cek hasil: harus ada tepat satu accessor.

bash
vault list auth/approle/role/$ROLE/secret-id

Catatan

Di mana Dokploy menyimpan nilai ini

Dari source code-nya: di database Dokploy (tidak terenkripsi), di file .env di sebelah compose file di VPS, dan sebagai env container vault-agent yang terlihat lewat docker inspect. Jadi siapa pun yang punya admin Dokploy atau root VPS bisa membaca secret_id. Ini accepted risk dari secret_id statis. Mitigasinya: policy hanya bisa read path dev sendiri, token Vault TTL 1 jam, secret_id bisa dicabut kapan saja, dan akses admin dibatasi. Pola yang lebih kuat di dunia nyata adalah response wrapping atau platform auth (Kubernetes/AWS IAM), yaitu identitas mesin, bukan secret yang ditempel.

Pasang domain dev dengan HTTPS

Service secure-cicd-go-dev → tab Domains → Add Domain:

Field

Nilai

Kenapa

Service Name

app

Hanya app yang menerima request. Jangan vault-agent

Host

secure-cicd-dev.domainkamu.com

A record dari Bagian 1

Path

/

Semua path

Container Port

8080

Port yang di-expose app

HTTPS

ON

Certificate Provider

Let's Encrypt

Gratis, diperpanjang otomatis oleh Traefik

Kalau daftar Service Name kosong, klik tombol fetch/muat ulang supaya Dokploy membaca compose file dari branch-nya.

Domain baru aktif saat Deploy berikutnya, karena label Traefik ditulis ke compose waktu deploy. Dan Let's Encrypt hanya mau memberi sertifikat kalau domainnya sudah menunjuk ke VPS. Kalau DNS belum benar saat deploy, browser menampilkan sertifikat palsu TRAEFIK DEFAULT CERT.

Image bootstrap dev, satu kali dari laptop

Masalahnya tadi: compose dev menarik ghcr.io/dimss113/secure-cicd-go:development, tapi image itu belum ada sampai CI di Bagian 8. Tanpa image, deploy gagal dengan manifest unknown.

Solusinya: sekali ini saja, build dari laptop, scan dengan standar yang sama atau lebih ketat, lalu push.

Perhatian

Ini pengecualian yang dikontrol, bukan jalur normal

  • Build dari development yang bersih, yang sudah lolos semua gate PR.

  • Wajib lolos Trivy HIGH,CRITICAL dengan exit code 1 sebelum push.

  • Hanya tag development. Nggak ada tag sha-... dan nggak ada production, karena Bagian 8 mempromosikan sha-<commit> ke production. Image buatan tangan nggak boleh bisa terpilih.

  • Token dengan hak tulis package dicabut setelah push.

  • Image ini ditimpa oleh deploy CI pertama di Bagian 8.

Di perusahaan, langkah seperti ini disebut break-glass: dicatat dan alasannya ditulis.

Siapkan kode yang bersih

bash
git switch developmentgit pull --ff-onlygit status --short

--ff-only menolak pull kalau harus membuat merge commit lokal, jadi lokal persis sama dengan GitHub. git status --short harus kosong. Kalau ada perubahan, git stash dulu.

Build untuk linux/amd64

bash
COMMIT=$(git rev-parse HEAD)BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)IMG=ghcr.io/dimss113/secure-cicd-go docker buildx build --platform linux/amd64 \  --build-arg COMMIT="$COMMIT" \  --build-arg BUILD_TIME="$BUILD_TIME" \  --label org.opencontainers.image.source=https://github.com/dimss113/secure-cicd-go \  -t "$IMG:development" \  --load .

Beberapa bagian yang penting:

Bagian

Artinya

--platform linux/amd64

Wajib. Mac saya Apple Silicon (arm64), VPS-nya amd64. Tanpa ini, di VPS muncul exec format error

--build-arg COMMIT

Masuk ke ARG COMMIT di Dockerfile, lalu lewat -ldflags -X muncul di /version

--label org.opencontainers.image.source

Label OCI standar. GHCR memakainya untuk menautkan package ke repo, supaya Actions repo itu bisa diberi akses nanti

--load

Simpan hasil build ke Docker lokal, supaya bisa di-scan dulu sebelum push

Build-nya sekitar 1 sampai 2 menit, karena Go dikompilasi lewat emulasi amd64.

Cek hasil:

bash
docker image inspect "$IMG:development" \  --format '{{.Architecture}} {{index .Config.Labels "org.opencontainers.image.source"}}'
text
amd64 https://github.com/dimss113/secure-cicd-go

Scan sebelum push

bash
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 "$IMG:development"echo "exit=$?"

Hasil di saya:

text
┌────────────────────────────────────────────────────────────┬──────────┬─────────────────┐│                           Target                           │   Type   │ Vulnerabilities │├────────────────────────────────────────────────────────────┼──────────┼─────────────────┤│ ghcr.io/dimss113/secure-cicd-go:development (debian 12.15) │  debian  │        0        ││ app                                                        │ gobinary │        0        │└────────────────────────────────────────────────────────────┴──────────┴─────────────────┘exit=0

Ini lebih ketat dari gate trivy-image di pr-checks.yml, yang hanya memblokir CRITICAL (HIGH masuk laporan SARIF). Image yang dibuat di luar CI saya beri standar lebih tinggi. Kalau exit=1, berhenti. Jangan push.

Login GHCR sementara, push, cabut lagi

Token gh saya cuma punya scope gist, read:org, repo. Push ke GHCR butuh write:packages, jadi saya tambahkan sebentar:

bash
gh auth refresh -h github.com -s write:packagesgh auth token | docker login ghcr.io -u dimss113 --password-stdindocker push "$IMG:development"docker logout ghcr.iogh auth refresh -h github.com -r write:packages

-s menambah scope (browser terbuka untuk konfirmasi), gh auth token | docker login --password-stdin memasukkan token lewat stdin supaya nggak muncul di layar atau history, dan -r di baris terakhir mencabut scope itu lagi. Hak tulis hanya ada selama dibutuhkan.

Cek hasil: baris terakhir push berbunyi development: digest: sha256:... size: ....

Jadikan package publik dan beri akses Actions

Package GHCR baru selalu private. Dokploy di setup ini menarik image tanpa login, jadi package-nya harus publik.

  1. Buka halaman package secure-cicd-go di profil GitHub kamu → Package settings.

  2. Danger Zone → Change visibility → Public, ketik nama package untuk konfirmasi.

  3. Di Manage Actions access, pastikan repo secure-cicd-go ada dengan role Write. Kalau belum: Add Repository → secure-cicd-go → Write. Ini supaya GITHUB_TOKEN di Bagian 8 boleh push ke package ini.

Cek hasil: tarik tanpa login, seperti yang dilakukan Dokploy.

bash
docker pull --platform linux/amd64 "$IMG:development"

Harus berhasil walaupun kamu sudah docker logout.

Apakah image publik itu aman? Aman, karena image-nya nggak berisi secret. Secret baru datang saat container jalan, dari Vault lewat sidecar. Ini sekaligus menguji desainnya: kalau ada secret di dalam image, membuatnya publik sama dengan membocorkannya, dan itu yang dijaga Gitleaks dan Trivy. Di perusahaan, registry biasanya private dan server memakai pull credential khusus yang read-only. Di sini saya pilih publik karena gratis dan sederhana.

Deploy dev pertama dan uji

Dokploy → secure-cicd-go-dev → Deploy. Buka tab Deployments dan klik deployment teratas untuk melihat log-nya:

text
Command: docker compose -p secure-cicd-go-dev-xxxxxx --env-file ... -f ./docker-compose.development.yml up -d --build --remove-orphans... Container secure-cicd-go-dev-xxxxxx-vault-agent-1  Healthy Container secure-cicd-go-dev-xxxxxx-app-1  Started

Urutan Healthy lalu Started itu hasil depends_on: condition: service_healthy: app baru dinyalakan setelah config.json ter-render.

Di tab Logs, cek per container:

Container

Baris yang dicari

Artinya

vault-agent

authentication successful

Login AppRole ke Vault asli berhasil

vault-agent

rendered "/vault/config/config.json.ctmpl" => "/vault/out/config.json"

Template ter-render ke tmpfs

app

{"level":"INFO","msg":"listening","addr":":8080","env":"development"}

App membaca config dan jalan sebagai dev

Pastikan juga nggak ada nilai secret di log mana pun. Vault Agent nggak mencetak isi template, dan app hanya mencetak env.

Uji /health dan /version

bash
D=https://secure-cicd-dev.domainkamu.comcurl -s $D/healthcurl -s $D/versionecho "harus sama dengan: $COMMIT"
text
{"status":"ok"}{"buildTime":"2026-10-31T..Z","commit":"<SHA lengkap>"}harus sama dengan: <SHA lengkap yang sama>

/version inilah yang nanti jadi smoke test di Bagian 8: deploy baru dianggap berhasil kalau commit yang jalan sama dengan commit yang di-scan.

Lalu uji bahwa secret dari Vault benar-benar dipakai. JWT:

bash
TOKEN=$(curl -s -X POST $D/v1/tokens -d '{"subject":"demo-user"}' | jq -r .token)curl -s $D/v1/me -H "Authorization: Bearer $TOKEN"   # → {"subject":"demo-user"}curl -s $D/v1/me -H "Authorization: Bearer salah"    # → {"error":"invalid token"}

Dan AES:

bash
CT=$(curl -s -X POST $D/v1/encrypt -d '{"plaintext":"halo bank fiktif"}' | jq -r .ciphertext)curl -s -X POST $D/v1/decrypt -d "{\"ciphertext\":\"$CT\"}"   # → {"plaintext":"halo bank fiktif"}

Token demo ini palsu dan berumur pendek, jadi aman tampil di terminal.

Cek sertifikat HTTPS

bash
echo | openssl s_client -connect secure-cicd-dev.domainkamu.com:443 \  -servername secure-cicd-dev.domainkamu.com 2>/dev/null \  | openssl x509 -noout -issuer -subject -dates

-servername mengirim SNI, supaya Traefik tahu domain mana yang diminta (satu IP, banyak domain). Cek hasil: issuer berisi Let's Encrypt, subject berisi domain dev kamu, dan masa berlakunya sekitar 90 hari. Kalau issuer berisi TRAEFIK DEFAULT CERT, lihat tabel masalah di bawah.

Service production: konfigurasi saja, jangan Deploy

Ulangi langkah service, Environment, dan domain di environment production Dokploy, dengan nilai ini:

Field

Nilai

Name

secure-cicd-go-prod

Provider

Git, https://github.com/dimss113/secure-cicd-go.git

Branch

main

Compose Path

./docker-compose.production.yml

Autodeploy

OFF

Environment

VAULT_ADDR=https://vault.example.com, kredensial role production

Domain

Belum bisa sekarang, dipasang di Bagian 8

Untuk kredensialnya, ganti dulu variabel role, lalu ulangi cabut-lama, pbcopy role_id, pbcopy secret_id, kosongkan clipboard, dan cek tepat satu accessor:

bash
ROLE=secure-cicd-go-production

Yang sempat bikin saya bingung: tab Domains di service prod nggak bisa memilih app. Daftarnya kosong. Ternyata Dokploy mengambil daftar service dari compose file di branch yang dipilih, dan branch main saat itu cuma berisi commit inisialisasi, belum ada docker-compose.production.yml. Nggak masalah, karena domain baru dipakai saat deploy. Domain prod dipasang di Bagian 8, setelah PR development ke main di-merge dan sebelum approval deploy production.

Jangan tekan Deploy. Image :production belum ada, dan memang nggak boleh ada sebelum Bagian 8.

Kenapa production nggak ikut bootstrap seperti dev? Karena aturannya: production hanya menjalankan image yang dibuat CI, sudah jalan di dev, dan sudah di-approve. Image buatan tangan dari laptop nggak boleh pernah sampai ke production, walaupun sudah di-scan. Untuk dev, risiko bootstrap kecil dan umurnya pendek. Untuk production, prinsip itu lebih penting daripada cepat terlihat jalan. Sampai Bagian 8, domain production menjawab 404 page not found dari Traefik. Itu normal.

Satu hal lagi yang dicatat untuk Bagian 8: composeId tiap service. Ambil dari URL halaman service di Dokploy (segmen terakhir setelah /services/compose/). composeId bukan rahasia, cuma ID yang nggak bisa dipakai tanpa API key, jadi nanti masuk GitHub Environment variable, bukan secret.

Catatan

API key Dokploy belum dibuat

Rencana awal saya membuat API key Dokploy di tahap ini. Saya pindahkan ke Bagian 8, tepat sebelum ditempel ke tujuannya. Secret sebaiknya dibuat saat dipakai dan langsung ditempel, bukan dibuat lebih awal lalu "disimpan dulu". API key Dokploy berlaku untuk seluruh akun, jadi makin pendek hidupnya di luar Dokploy, makin baik.

Latihan runbook: rotasi secret

Ini bahan bagus untuk interview: bagaimana merotasi secret aplikasi tanpa menyentuh kode?

text
Vault (secret baru)   │  Vault Agent mengambil ulang secret KV v2 statis tiap ±5 menit (default)   ▼config.json di tmpfs (sudah baru)   │  app membaca config HANYA saat start   ▼app (masih pakai secret lama di memori)  ← butuh restart

Dan kita sudah tahu Deploy tanpa perubahan nggak me-restart container. Jadi caranya Stop lalu Start. Saat Stop, tmpfs ikut kosong (sudah saya uji dengan compose lokal), jadi saat Start vault-agent login ulang dan langsung me-render secret terbaru, tanpa menunggu 5 menit.

Coba di dev. Buat token dengan secret lama:

bash
OLD=$(curl -s -X POST $D/v1/tokens -d '{"subject":"sebelum-rotasi"}' | jq -r .token)curl -s $D/v1/me -H "Authorization: Bearer $OLD"   # → {"subject":"sebelum-rotasi"}

Rotasi secret JWT dev di Vault. $(openssl rand -hex 32) membuat nilai acak, dan history shell hanya menyimpan teks perintahnya, bukan nilainya:

bash
vault kv put -mount=static-secret secure-cicd-go/development/secrets/jwt secret_key="$(openssl rand -hex 32)"

Cek lagi: token lama masih diterima, karena app belum restart. Lalu Dokploy → secure-cicd-go-dev → Stop, tunggu, Start.

bash
curl -s $D/v1/me -H "Authorization: Bearer $OLD"NEW=$(curl -s -X POST $D/v1/tokens -d '{"subject":"sesudah-rotasi"}' | jq -r .token)curl -s $D/v1/me -H "Authorization: Bearer $NEW"
text
{"error":"invalid token"}{"subject":"sesudah-rotasi"}

Rotasi berhasil tanpa mengubah kode atau env Dokploy. Konsekuensinya juga perlu ditulis jujur: Stop-Start berarti downtime beberapa detik, rotasi JWT mengeluarkan semua user (di dunia nyata dipakai dua key saat transisi, misalnya JWKS dengan kid), dan rotasi kunci AES lebih berbahaya karena data lama nggak bisa didekripsi lagi tanpa re-encryption atau versi kunci seperti Vault Transit.

Kalau ada masalah

Gejala

Penyebab

Solusi

required variable VAULT_ADDR is missing a value: set VAULT_ADDR

Tab Environment belum di-Save, atau Create Env File OFF

Save, nyalakan Create Env File, Deploy lagi

manifest unknown saat pull app

Image :development belum di-push atau salah tag

Ulangi bootstrap, cek docker buildx imagetools inspect $IMG:development

denied / unauthorized saat pull

Package GHCR masih private

Change visibility → Public

app restart terus, log exec format error

Image arm64, lupa --platform linux/amd64

Build ulang dengan platform yang benar, push

vault-agent unhealthy, invalid role or secret ID

role_id/secret_id salah tempel, atau sudah dicabut

Buat secret_id baru, Save, Stop lalu Start

vault-agent no such host ke Vault

DNS di dalam container. Di VPS saya pernah resolve ke 127.0.x.x karena /etc/hosts

docker exec <vault-agent> getent hosts vault.example.com; workaround extra_hosts lewat PR

Browser: TRAEFIK DEFAULT CERT

Let's Encrypt gagal: DNS belum benar saat deploy, atau port 80 tertutup

Cek dig, tunggu, Deploy lagi. Lihat log container dokploy-traefik

404 page not found (teks polos)

Domain belum aktif atau Service Name salah

Cek tab Domains → app, lalu Deploy

502 Bad Gateway

Port bukan 8080, atau app sedang crash

Cek Container Port dan log app

Ganti env tapi nggak berefek

Deploy tanpa perubahan nggak restart container

Kalau ragu, Stop lalu Start

Rangkuman

  • Project secure-cicd-go di Dokploy dengan dua service Compose dari provider Git biasa, tanpa akses ke akun GitHub.

  • Auto-deploy mati di keduanya, jadi satu-satunya jalan deploy adalah CI.

  • Kredensial AppRole terpisah per environment, ditempel lewat clipboard tanpa tampil di layar, tepat satu secret_id aktif per role.

  • Domain dev dengan HTTPS Let's Encrypt lewat Traefik, hanya ke app:8080.

  • Image bootstrap :development dari laptop, linux/amd64, lolos Trivy HIGH/CRITICAL, package GHCR publik dengan akses Write untuk Actions repo.

  • Dev jalan: /health ok, /version sama dengan commit, JWT dan AES dari Vault asli terbukti dipakai.

  • Production sudah dikonfigurasi tapi belum di-deploy, plus runbook rotasi secret yang sudah dicoba.

Di Bagian 8, kita tulis deploy.yml: build image sekali di CI, scan, buat SBOM, push ke GHCR, lalu memanggil API Dokploy untuk deploy dev, dan mempromosikan image yang sama ke production setelah approval. Di situ juga API key Dokploy akhirnya dibuat, dan image bootstrap tadi pensiun.

Artikel terkait