//BAGIAN 7 DARI 10
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
- 01Bagian 1: Kenapa Saya Bikin Secure CI/CD Pipeline, dan Persiapan Tools-nya
- 02Bagian 2: Vault, Secret, Policy, dan AppRole per Environment
- 03Bagian 3: Aplikasi Go Kecil yang Membaca Secret dari File dan Menolak Jalan Kalau Config Salah
- 04Bagian 4: Vault Agent Sidecar dan Docker Compose, Secret Masuk Tanpa App Tahu Vault Ada
- 05Bagian 5: Repo GitHub dan Workflow PR sebagai Security Gate
- 06Bagian 6: Ruleset yang Memblokir Merge, dan Lima Demo PR yang Harus Gagal
- 07Bagian 7: Menyiapkan Environment Dev dan Production di Dokploy
- 08Bagian 8: Workflow Deploy, Build Sekali, Scan, SBOM, GHCR, lalu Promosi ke Production
- 09Bagian 9: Proses Exception untuk Temuan Security yang Belum Bisa Diperbaiki
- 10Bagian 10: CI Mengambil Token Dokploy dari Vault lewat GitHub OIDC, dan Penutup Seri
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
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:
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:8080Bayangkan 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:
# 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: trueHubungannya dengan langkah Dokploy di atas:
Baris di compose | Diisi oleh |
|---|---|
|
|
| Dipatok di file, bukan dari |
| Karena |
| Ditarik dari GHCR setiap deploy. Image ini harus sudah ada |
| 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 |
| Nama yang terlihat di UI |
App Name | biarkan yang dibuat otomatis | Jadi nama project compose ( |
Compose Type | Docker Compose | Bukan Stack (Swarm). Kita cuma satu server |
Buka service-nya, tab General, pilih Provider: Git:
Field | Nilai |
|---|---|
Repository URL |
|
Branch |
|
Compose Path |
|
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
ROLE=secure-cicd-go-developmentvault list auth/approle/role/$ROLE/secret-idYang 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:
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"doneLoop 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:
VAULT_ADDR=https://vault.example.comVAULT_ROLE_ID=VAULT_SECRET_ID=Ambil role_id langsung ke clipboard, lalu tempel di belakang VAULT_ROLE_ID=:
vault read -field=role_id auth/approle/role/$ROLE/role-id | pbcopyBuat secret_id baru, juga langsung ke clipboard, lalu tempel di belakang VAULT_SECRET_ID=:
vault write -f -field=secret_id auth/approle/role/$ROLE/secret-id | pbcopyTerakhir kosongkan clipboard:
pbcopy < /dev/nullPastikan 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.
vault list auth/approle/role/$ROLE/secret-idCatatan
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 |
| Hanya |
Host |
| A record dari Bagian 1 |
Path |
| Semua path |
Container Port |
| Port yang di- |
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
developmentyang bersih, yang sudah lolos semua gate PR.Wajib lolos Trivy
HIGH,CRITICALdengan exit code 1 sebelum push.Hanya tag
development. Nggak ada tagsha-...dan nggak adaproduction, karena Bagian 8 mempromosikansha-<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
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
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 |
|---|---|
| Wajib. Mac saya Apple Silicon ( |
| Masuk ke |
| Label OCI standar. GHCR memakainya untuk menautkan package ke repo, supaya Actions repo itu bisa diberi akses nanti |
| 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:
docker image inspect "$IMG:development" \ --format '{{.Architecture}} {{index .Config.Labels "org.opencontainers.image.source"}}'amd64 https://github.com/dimss113/secure-cicd-goScan sebelum push
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 "$IMG:development"echo "exit=$?"Hasil di saya:
┌────────────────────────────────────────────────────────────┬──────────┬─────────────────┐│ Target │ Type │ Vulnerabilities │├────────────────────────────────────────────────────────────┼──────────┼─────────────────┤│ ghcr.io/dimss113/secure-cicd-go:development (debian 12.15) │ debian │ 0 ││ app │ gobinary │ 0 │└────────────────────────────────────────────────────────────┴──────────┴─────────────────┘exit=0Ini 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:
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.
Buka halaman package
secure-cicd-godi profil GitHub kamu → Package settings.Danger Zone → Change visibility → Public, ketik nama package untuk konfirmasi.
Di Manage Actions access, pastikan repo
secure-cicd-goada dengan role Write. Kalau belum: Add Repository →secure-cicd-go→ Write. Ini supayaGITHUB_TOKENdi Bagian 8 boleh push ke package ini.
Cek hasil: tarik tanpa login, seperti yang dilakukan Dokploy.
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:
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 StartedUrutan 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 |
|---|---|---|
|
| Login AppRole ke Vault asli berhasil |
|
| Template ter-render ke tmpfs |
|
| 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
D=https://secure-cicd-dev.domainkamu.comcurl -s $D/healthcurl -s $D/versionecho "harus sama dengan: $COMMIT"{"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:
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:
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
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 |
|
Provider | Git, |
Branch |
|
Compose Path |
|
Autodeploy | OFF |
Environment |
|
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:
ROLE=secure-cicd-go-productionYang 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?
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 restartDan 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:
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:
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.
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"{"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 |
|---|---|---|
| Tab Environment belum di-Save, atau Create Env File OFF | Save, nyalakan Create Env File, Deploy lagi |
| Image | Ulangi bootstrap, cek |
| Package GHCR masih private | Change visibility → Public |
app restart terus, log | Image | Build ulang dengan platform yang benar, push |
vault-agent unhealthy, | role_id/secret_id salah tempel, atau sudah dicabut | Buat secret_id baru, Save, Stop lalu Start |
vault-agent | DNS di dalam container. Di VPS saya pernah resolve ke |
|
Browser: | Let's Encrypt gagal: DNS belum benar saat deploy, atau port 80 tertutup | Cek |
| Domain belum aktif atau Service Name salah | Cek tab Domains → |
| 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-godi 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
:developmentdari laptop,linux/amd64, lolos Trivy HIGH/CRITICAL, package GHCR publik dengan akses Write untuk Actions repo.Dev jalan:
/healthok,/versionsama 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.