Deployment Asisten
Panduan ini dipakai oleh asisten trainer setelah koordinator memberikan akses IAM ke project kelompok GCP. Satu project dipakai oleh lima asisten. Karena itu, setiap asisten harus memakai slug unik pada Service Account, VM, GitHub connection, linked repository, trigger, image, alamat IP, dan DNS zone.
Panduan ini disusun berurutan dalam tiga tahap:
- Bagian 1 sampai 2, dari laptop. Fork repository, lengkapi berkas konfigurasi, commit, lalu push.
- Bagian 3 sampai 12, dari Cloud Shell dan VM. Buka project, buat Service Account, dan buat VM.
- Bagian 13 sampai 31, dari VM dan Cloud Shell. Clone fork, isi
.env, jalankan Cloud Build, uji GeoServer, cadangkan data, pantau akses portal, daftarkan ke mesin pencari, lalu bersihkan resource dan hentikan VM.
Panduan ini sudah melewati satu pengujian penuh pada 11 September 2026 dengan satu email.
Alur fork, VM, GitHub, Cloud Build, tiga container, halaman /portal, GeoServer, unggah
shapefile, dan pembacaan layer lewat WFS semuanya berjalan. Bagian 24 tentang domain, DNS, dan
HTTPS juga sudah lulus uji pada 12 September 2026, dengan sertifikat Let’s Encrypt untuk satu
subdomain sungguhan dan halaman portal yang membalas 200 lewat HTTPS.
Yang belum diuji adalah lima VM yang menyala bersamaan di dalam satu project, undangan lima email beserta sepuluh role IAM, dan lima GitHub connection dalam satu project. Bagian 26 tentang pemantauan akses portal sudah diuji pada 13 September 2026: Nginx dimatikan pukul 11:48:02 WIB, incident dan email firing muncul sekitar 11:50 WIB, lalu email recovered untuk incident yang sama diterima setelah pemulihan. Durasi incident 8 menit 42 detik. Yang belum diuji itulah yang dicatat lewat workbook asisten trainer.
VM memakai spesifikasi default dari modul, yaitu e2-medium dengan heap GeoServer
-Xmx2048m. Pengukuran 11 September 2026 menunjukkan spesifikasi ini lega, bukan mepet:
GeoServer memakai 882 MiB dari batas 2048 MiB dan 2,3 GiB dari 3,8 GiB masih tersedia. Karena
itu spesifikasi default dipertahankan tanpa dinaikkan.
Hasil akhir yang diharapkan
Section titled “Hasil akhir yang diharapkan”Setelah panduan ini selesai:
- Fork berisi enam berkas konfigurasi:
docker-compose.yml,nginx.conf,.env.example,.gitignore,scripts/check-config.mjs, danscripts/periksa-nginx.mjs. - Push ke branch
mainmemicu Cloud Build yang membangun image dan menjalankannya di VM. - API bersama dan Artifact Registry sudah tersedia di project kelompok.
- Service Account deployment dan VM dengan nama unik asisten sudah tersedia.
- Tiga container berjalan:
nextjs_portal,geoserver_app, dannginx_proxy. - Geoportal dapat dibuka melalui path
/portal. - GeoServer dapat dibuka melalui path
/geoserver/webtanpa slash di akhir. - Data spasial dapat diunggah dan dibaca kembali melalui endpoint WFS.
- Jika diminta, domain asisten dapat diarahkan ke alamat IP VM.
- Folder
geoserver-datadan berkas.envtersalin ke luar VM sebagai cadangan. - Kesehatan tiga container, memori, dan ruang disk sudah diperiksa dan tercatat.
- Jika diminta, domain sudah terverifikasi di Google Search Console dan sitemapnya terkirim.
1. Fork repository dan siapkan berkas konfigurasi
Section titled “1. Fork repository dan siapkan berkas konfigurasi”Bagian ini dikerjakan dari laptop sebelum menyentuh Google Cloud. Alasannya sederhana:
repository instruktur belum memuat docker-compose.yml, nginx.conf, maupun
.env.example, sedangkan Cloud Build membaca berkas-berkas itu dari fork. Kalau fork belum
lengkap, build pertama akan gagal walaupun project GCP sudah benar.
Urutan yang benar adalah fork, sunting berkas di laptop, verifikasi, commit, push, baru lanjut ke bagian 4. Jangan membuat VM lebih dahulu, karena VM hanya menyalin hasil fork.
Enam berkas berikut disiapkan pada bagian ini, lalu diverifikasi pada bagian 2 dan di-commit pada bagian 3:
| Berkas | Fungsi |
|---|---|
docker-compose.yml |
Tiga service: nextjs, geoserver, dan nginx |
nginx.conf |
Rute /portal dan /geoserver/web |
.env.example |
Contoh isi .env tanpa nilai rahasia |
.gitignore |
Menambah /geoserver-data/ supaya data GeoServer tidak masuk Git |
scripts/check-config.mjs |
Pemeriksa dua berkas YAML, jalan dengan Node.js saja |
scripts/periksa-nginx.mjs |
Pemeriksa struktur nginx.conf, juga tanpa container |
Buat fork
Section titled “Buat fork”- Buka
https://github.com/matiurari/personal-geoportalpada browser. - Pilih Fork, lalu pilih akun GitHub asisten sebagai tujuan.
- Beri nama fork
personal-geoportal-pesertasupaya seluruh contoh perintah pada panduan ini cocok. Jika nama itu sudah dipakai repository lain di akun GitHub asisten, biarkan nama bawaanpersonal-geoportaldan sesuaikan URL clone pada langkah berikutnya. - Pastikan branch default fork adalah
main.
Repository public dapat difork tanpa izin khusus. Pastikan fork benar-benar berada di akun asisten, bukan di organisasi yang tidak dipakai untuk pelatihan.
Siapkan file konfigurasi tiga container
Section titled “Siapkan file konfigurasi tiga container”Repository instruktur belum memuat docker-compose.yml, nginx.conf, maupun .env.example.
Ketiga berkas itu hanya muncul sebagai teks pada modul, jadi asisten perlu membuatnya sendiri
di fork. Enam berkas berikut wajib disiapkan sebelum Cloud Build dijalankan. Tanpa
perubahan ini container geoserver_app tidak pernah dibuat dan GeoServer tidak bisa dibuka.
docker-compose.yml di root repository fork:
services: nextjs: image: ${NEXTJS_IMAGE:?Set NEXTJS_IMAGE in .env} container_name: nextjs_portal env_file: - .env depends_on: - geoserver networks: - app-network restart: unless-stopped
geoserver: image: kartoza/geoserver:2.24.1 container_name: geoserver_app environment: - INITIAL_MEMORY=512m - MAXIMUM_MEMORY=2048m - GEOSERVER_ADMIN_USER=admin # Diisi dari .env supaya kata sandi tidak tertulis di repository public. - GEOSERVER_ADMIN_PASSWORD=${GEOSERVER_ADMIN_PASSWORD} - GEOSERVER_CORS_ENABLED=true - GEOSERVER_CORS_ALLOWED_ORIGINS=* volumes: - ./geoserver-data:/opt/geoserver/data_dir networks: - app-network restart: unless-stopped
nginx: image: nginx:1.27-alpine container_name: nginx_proxy depends_on: - nextjs - geoserver ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro networks: - app-network restart: unless-stopped
networks: app-network: driver: bridgeCatatan dua variabel memori. Image kartoza/geoserver membaca INITIAL_MEMORY dan
MAXIMUM_MEMORY, bukan JAVA_OPTS seperti yang tertulis di modul. MAXIMUM_MEMORY=2048m
adalah heap default yang sengaja dipertahankan pada e2-medium 4 GB.
Catatan Nginx. Dua blok location memindahkan alamat tujuan ke dalam variabel sebelum
proxy_pass, dan menambahkan resolver 127.0.0.11. Pola ini bukan pilihan gaya, melainkan
syarat supaya Nginx mau start.
Nginx menyelesaikan nama service saat start, dan berhenti dengan host not found in upstream bila nama itu belum ada. Padahal urutan deployment ini sengaja menyalakan
geoserver dan nginx lebih dahulu, sebelum image nextjs dibangun. Tanpa pola variabel,
container nginx_proxy langsung keluar dengan kode 1, dan gejalanya di browser hanya
Connection refused pada port 80, tanpa petunjuk apa pun tentang penyebabnya.
Dengan variabel, penyelesaian nama ditunda sampai ada permintaan. Jadi Nginx start dengan tenang, lalu membalas 502 hanya untuk permintaan yang tujuannya memang belum siap.
Alamat 127.0.0.11 adalah DNS internal Docker pada jaringan bridge buatan Compose. Jangan
menggantinya dengan alamat lain.
Catatan kata sandi. Nilai ${GEOSERVER_ADMIN_PASSWORD} dibaca dari .env, bukan ditulis di
berkas compose, supaya kata sandi tidak ikut terbit di repository public. Variabel ini membuat
kata sandi admin ditentukan saat container pertama kali dijalankan, sehingga kata sandi
bertahan setelah container di-restart. Jika variabelnya dibiarkan kosong, image kartoza
membuat kata sandi acak baru pada setiap start dan menuliskannya ke log container.
nginx.conf di root repository fork:
server { listen 80; server_name _;
location = / { return 302 /portal; }
# Diarahkan langsung ke bentuk kanonik tanpa slash, karena GeoServer # mengalihkan /geoserver/web/ ke /geoserver/web. location = /geoserver { return 302 /geoserver/web; }
# Nama service di-resolve saat ada permintaan, bukan saat Nginx start. # Tanpa pola ini, Nginx menolak start dengan "host not found in upstream" # selama container nextjs belum ada. Padahal geoserver dan nginx sengaja # dinyalakan lebih dahulu, sebelum image nextjs dibangun. location /geoserver/ { resolver 127.0.0.11 valid=10s ipv6=off;
set $geoserver_upstream http://geoserver:8080; rewrite ^/geoserver/(.*)$ /geoserver/$1 break; proxy_pass $geoserver_upstream;
proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host;
proxy_buffers 16 16k; proxy_buffer_size 32k; }
location / { resolver 127.0.0.11 valid=10s ipv6=off;
set $nextjs_upstream http://nextjs:3000; proxy_pass $nextjs_upstream;
proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}Perhatikan pola set lalu proxy_pass pada variabel. Itu bukan gaya penulisan biasa,
melainkan syarat supaya Nginx mau start sebelum container tujuannya ada. Penjelasannya ada
pada catatan di bawah tabel berkas.
.env.example di root repository fork. Berkas ini menjadi contoh isi .env di VM,
sehingga nilai asli tidak perlu ikut masuk Git. NEXTAUTH_URL dan BASE_URL memakai alamat
yang sama, yaitu alamat IP eksternal VM atau domainnya, keduanya tanpa slash di akhir.
# Salin berkas ini menjadi .env di VM, lalu isi nilai aslinya di sana.# Jangan pernah commit berkas .env yang sudah terisi.
# Diisi otomatis oleh Cloud Build pada deploy pertama.## Sebelum build pertama, pakai image yang benar-benar ada sebagai nilai sementara,# misalnya nginx:1.27-alpine. Jangan memakai nama karangan seperti belum-dibangun,# karena compose akan mencoba menariknya dan gagal dengan pull access denied.## Nilai kosong juga bermasalah: interpolasi berjalan sebelum service dipilih, jadi# seluruh perintah docker compose gagal, termasuk yang hanya menyalakan geoserver.NEXTJS_IMAGE=nginx:1.27-alpine
# Boleh dikosongkan untuk menguji build, Nginx, dan halaman publik.# Login, Prisma, dan fitur katalog memerlukan database yang valid.DATABASE_URL=
# Buat dua nilai acak yang berbeda dengan: openssl rand -hex 32JWT_SECRET=JWT_EXPIRES_IN=1hNEXTAUTH_SECRET=
# Wajib memakai alamat IP eksternal VM atau domainnya, tanpa slash di akhir,# contoh: NEXTAUTH_URL=http://IP_EKSTERNAL_VM/portalNEXTAUTH_URL=
# Kata sandi admin GeoServer. Pakai openssl rand -hex 16 supaya hanya berisi huruf# dan angka. Jangan memakai tanda dolar, karena compose membacanya sebagai variabel# dan karakter setelahnya bisa hilang tanpa peringatan yang jelas.GEOSERVER_ADMIN_PASSWORD=
AUTH_API_URL=http://localhost:3000/portal/api
# Origin lengkap aplikasi, tanpa slash di akhir.# Sejak sinkronisasi 12 September 2026, kode repository instruktur tidak lagi# membaca BASE_URL. Variabel ini disimpan supaya versi lama tetap jalan.BASE_URL=http://IP_EKSTERNAL_VM/portal
# Pengganti BASE_URL untuk kode versi baru. Dipakai satu kali pada rute# api/katalog-data-3d/create untuk menyusun URL berkas model. Nilainya sama dengan# BASE_URL. Kalau kosong, URL modelnya menjadi "undefined/api/..." dan model 3D# gagal dibuka. Dibaca saat runtime, jadi cukup diisi di berkas .env ini.NEXT_PUBLIC_URL_BASE_PATH=http://IP_EKSTERNAL_VM/portal
# Sebelas variabel berikut hanya diperlukan fitur katalog 2D dan 3D, yaitu fitur# yang membuat tabel spasial di PostGIS lalu menerbitkannya sebagai layer di# GeoServer. Halaman publik, GeoServer, dan WFS berjalan tanpa variabel ini.## POSTGIS_* dipakai pustaka pg secara langsung, bukan Prisma. Pada Supabase pakai# koneksi langsung port 5432, bukan pooler 6543.POSTGIS_HOST=POSTGIS_PORT=5432POSTGIS_DB=postgresPOSTGIS_USER=POSTGIS_PASSWORD=# Schema tempat tabel spasial dibuat. Modul 4 memakai schema gis.POSTGIS_SCHEMA=gis
# GEOSERVER_* dipakai untuk memanggil REST API GeoServer dari dalam container.GEOSERVER_URL=http://geoserver:8080/geoserverGEOSERVER_USERNAME=admin# Harus sama persis dengan GEOSERVER_ADMIN_PASSWORD di atas.GEOSERVER_PASSWORD=GEOSERVER_WORKSPACE=geoportal# Nama datastore PostGIS yang sudah dibuat di GeoServer.GEOSERVER_POSTGIS_DATASTORE=.gitignore di root repository fork. Tambahkan satu baris supaya data GeoServer tidak
ikut masuk Git:
/geoserver-data/Tambahkan skrip pemeriksa
Section titled “Tambahkan skrip pemeriksa”Dua skrip ini dipakai pada bagian 2 untuk memeriksa berkas konfigurasi sebelum push, dan berguna juga setiap kali berkas itu diubah lagi. Keduanya berjalan dengan Node.js saja.
mkdir -p scriptsscripts/check-config.mjs memeriksa docker-compose.yml dan cloudbuild.yaml, dan
gagal bila service di berkas compose bukan tiga.
import { readFileSync } from 'node:fs';import { createRequire } from 'node:module';
// YAML dimuat lewat createRequire karena paket ini berbentuk CommonJS,// sehingga import bernama tidak tersedia di berkas .mjs.const YAML = createRequire(import.meta.url)('yaml');
const names = ['docker-compose.yml', 'cloudbuild.yaml'];let failed = false;
for (const file of names) { try { const data = YAML.parse(readFileSync(file, 'utf8')); console.log('OK ' + file + ' -> ' + Object.keys(data).join(', ')); if (names[0] === file) { const services = Object.keys(data.services ?? {}); console.log(' service: ' + services.join(', ')); if (services.length !== 3) { console.error(' HARUS 3 service, ditemukan ' + services.length); failed = true; } } } catch (error) { console.error('GAGAL ' + file + ': ' + error.message); failed = true; }}
process.exit(failed ? 1 : 0);scripts/periksa-nginx.mjs memeriksa struktur nginx.conf. Skrip ini sengaja tidak
memakai Docker maupun Podman, supaya bisa dijalankan di sistem operasi apa pun.
Isi scripts/periksa-nginx.mjs
// Pemeriksa berkas konfigurasi Nginx yang berjalan dengan Node.js saja.// Jalankan: node scripts/periksa-nginx.mjs nginx.confimport { readFileSync } from 'node:fs';
const berkas = process.argv[2] ?? 'nginx.conf';const isi = readFileSync(berkas, 'utf8');
// Buang komentar, tetapi hormati tanda kutip supaya tanda # di dalam nilai aman.function tanpaKomentar(teks) { let hasil = ''; let kutip = null; for (let i = 0; i < teks.length; i++) { const c = teks[i]; if (kutip) { hasil += c; if (c === kutip && teks[i - 1] !== '\\') kutip = null; continue; } if (c === '"' || c === "'") { kutip = c; hasil += c; continue; } if (c === '#') { while (i < teks.length && teks[i] !== '\n') i++; hasil += '\n'; continue; } hasil += c; } return hasil;}
const bersih = tanpaKomentar(isi);const masalah = [];
// Menghilangkan titik koma membuat dua baris menyatu, sehingga jumlah titik koma// pada baris itu menjadi kurang dari jumlah pernyataannya.bersih.split('\n').forEach((baris, nomor) => { const t = baris.trim(); if (!t) return;
const kurungBuka = (t.match(/\{/g) ?? []).length; const kurungTutup = (t.match(/\}/g) ?? []).length; const titikKoma = (t.match(/;/g) ?? []).length;
if (kurungBuka > 0 && kurungTutup === 0) { if (!t.replace(/[{};]/g, '').trim()) { masalah.push(`baris ${nomor + 1}: blok dibuka tanpa nama directive di depannya`); } return; } if (kurungBuka === 0 && kurungTutup > 0 && titikKoma === 0) return;
if (titikKoma === 0) { masalah.push(`baris ${nomor + 1}: "${t.slice(0, 50)}" tidak diakhiri titik koma`); return; } if (titikKoma > 1 && kurungBuka === 0) { masalah.push(`baris ${nomor + 1}: ada ${titikKoma} pernyataan tanpa blok`); }});
const totalBuka = (bersih.match(/\{/g) ?? []).length;const totalTutup = (bersih.match(/\}/g) ?? []).length;if (totalBuka !== totalTutup) { masalah.push(`kurung kurawal tidak seimbang, ${totalBuka} buka dan ${totalTutup} tutup`);}
const proxy = [...bersih.matchAll(/proxy_pass\s+([^;]+);/g)].map((m) => m[1].trim());const adaResolver = /resolver\s+[^;]+;/.test(bersih);
if (proxy.some((p) => p.startsWith('$')) && !adaResolver) { masalah.push('proxy_pass memakai variabel tanpa directive resolver, nama upstream tidak akan terselesaikan');}if (!/server\s*\{/.test(bersih)) masalah.push('tidak ada blok server');if (!/listen\s+\d+/.test(bersih)) masalah.push('tidak ada directive listen');if (proxy.length === 0) masalah.push('tidak ada proxy_pass sama sekali');
console.log(`Berkas : ${berkas}`);console.log(`proxy_pass : ${proxy.length > 0 ? proxy.join(', ') : '(tidak ada)'}`);console.log(`resolver : ${adaResolver ? 'ada' : 'tidak ada'}`);
if (masalah.length === 0) { console.log('HASIL: struktur konfigurasi valid'); process.exit(0);}console.log(`HASIL: ${masalah.length} masalah`);for (const m of masalah) console.log(` - ${m}`);process.exit(1);Simpan berkasnya, lalu lanjut ke bagian 2 untuk menjalankan keduanya.
2. Verifikasi berkas sebelum push
Section titled “2. Verifikasi berkas sebelum push”Kesalahan pada berkas konfigurasi baru ketahuan setelah Cloud Build berjalan, dan satu siklus build memakan beberapa menit. Periksa dulu dari laptop supaya kesalahan tertangkap dalam hitungan detik.
Bagian ini memakai Node.js saja, tanpa Docker maupun Podman. Alasannya dua. Asisten memakai sistem operasi dan perkakas yang berbeda-beda, dan memasang container runtime hanya untuk memeriksa berkas terlalu berat. Yang diperiksa di sini juga bukan perilaku container, melainkan kesalahan penulisan, dan itu bisa dikerjakan Node.js.
Pembuktian yang sebenarnya tetap terjadi di VM pada bagian 14 dan seterusnya.
Langkah 1: periksa dua berkas YAML
Section titled “Langkah 1: periksa dua berkas YAML”node scripts/check-config.mjsHasil yang diharapkan:
OK docker-compose.yml -> services, networks service: nextjs, geoserver, nginxOK cloudbuild.yaml -> substitutions, steps, images, optionsKalau service yang muncul bukan tiga, berarti ada bagian services yang hilang atau salah
indentasi.
Langkah 2: periksa struktur nginx.conf
Section titled “Langkah 2: periksa struktur nginx.conf”node scripts/periksa-nginx.mjs nginx.confHasil yang diharapkan:
Berkas : nginx.confproxy_pass : $geoserver_upstream, $nextjs_upstreamresolver : adaHASIL: struktur konfigurasi validSkrip ini memeriksa empat hal yang paling sering salah: directive tanpa titik koma, blok yang
dibuka tanpa nama directive, kurung kurawal yang tidak seimbang, serta proxy_pass yang
memakai variabel tetapi tidak disertai resolver. Yang terakhir itu penting, karena tanpa
resolver container nginx_proxy akan keluar dengan kode 1 dan gejalanya hanya
Connection refused di browser.
Kalau ada yang tidak lolos
Section titled “Kalau ada yang tidak lolos”Skrip akan menyebut nomor baris dan masalahnya, misalnya:
HASIL: 1 masalah - baris 46: "proxy_pass $nextjs_upstream" tidak diakhiri titik komaPerbaiki berkasnya lalu jalankan ulang sampai keduanya lolos sebelum lanjut ke bagian 3.
Daftar periksa manual
Section titled “Daftar periksa manual”Kalau skrip tidak bisa dijalankan, periksa lima hal berikut dengan mata:
docker-compose.ymlmemuat tiga service:nextjs,geoserver, dannginx.nginx.confmemuat bloklocation /geoserver/danlocation = /geoserver.- Setiap
proxy_passmemakai nama service, bukanlocalhost. Nilai yang benar adalahhttp://nextjs:3000danhttp://geoserver:8080. GEOSERVER_ADMIN_PASSWORDsudah diisi di.envdenganopenssl rand -hex 16, dan tidak memuat tanda dolar. Nilai ini tidak ditulis didocker-compose.yml..gitignorememuat/geoserver-data/.
3. Commit dan push fork
Section titled “3. Commit dan push fork”Setelah keenam berkas siap, commit dan push dari laptop. Push inilah yang nanti memicu Cloud Build pada bagian 17.
cd personal-geoportal-pesertagit status --shortgit add docker-compose.yml nginx.conf .env.example .gitignore \ scripts/check-config.mjs scripts/periksa-nginx.mjsgit commit -m "feat: tambah GeoServer dan rute nginx"git push origin mainJangan pernah memakai git add . tanpa memeriksa git status --short lebih dahulu, supaya
berkas .env yang sudah terisi tidak ikut ter-commit.
4. Buka project sebagai asisten
Section titled “4. Buka project sebagai asisten”Login ke Google Cloud Console menggunakan email yang didaftarkan oleh koordinator. Pada kunjungan pertama, Google mungkin meminta persetujuan terhadap persyaratan layanan.

Gambar 1. Persetujuan persyaratan layanan muncul sebelum project dapat dibuka.
Pilih project yang diberikan oleh akun master. Pastikan nama project terlihat pada bagian atas Google Cloud Console.

Gambar 2. Pengujian awal membuktikan bahwa email peserta dapat membuka project melalui IAM. Pada skema kelompok, pilih Project ID yang dikirim akun master.
5. Tetapkan variabel Cloud Shell
Section titled “5. Tetapkan variabel Cloud Shell”Buka Cloud Shell menggunakan akun asisten. Ganti PARTICIPANT_ID dengan slug yang sudah
disepakati. Gunakan huruf kecil, angka, atau tanda hubung tanpa spasi. Contoh berikut
memakai slug nama01.
PROJECT_ID="geoportal-kelompok-a-xxxxx"REGION="asia-southeast2"ZONE="asia-southeast2-b"PARTICIPANT_ID="nama01"
REPOSITORY="katalog-images"APP_DIR="/opt/webgis/app"VM_NAME="webgis-${PARTICIPANT_ID}"BUILD_SA_NAME="cb-deployer-${PARTICIPANT_ID}"CONNECTION_NAME="github-${PARTICIPANT_ID}"LINKED_REPO_NAME="repo-${PARTICIPANT_ID}"TRIGGER_NAME="deploy-${PARTICIPANT_ID}"IMAGE_NAME="nextjs-${PARTICIPANT_ID}"STATIC_IP_NAME="webgis-ip-${PARTICIPANT_ID}"DNS_ZONE_NAME="geoportal-${PARTICIPANT_ID}"
BUILD_SA="${BUILD_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"VM_REGION="${ZONE%-*}"
gcloud config set project "$PROJECT_ID"gcloud auth listgcloud config get-value projectDua variabel di atas punya tugas yang berbeda, dan bedanya penting begitu kamu pindah region:
| Variabel | Artinya | Boleh diubah? |
|---|---|---|
REGION |
Lokasi Artifact Registry bersama katalog-images |
Tidak. Semua asisten memakai asia-southeast2 |
ZONE |
Tempat VM sendiri dibuat | Boleh. Isi asia-southeast1-b bila jatah 4 alamat IP publik di asia-southeast2 sudah habis |
VM_REGION diturunkan otomatis dari ZONE supaya keduanya tidak pernah bertentangan. Nilai
itu dipakai saat mengunci alamat IP statis pada bagian 24, karena alamat IP statis harus
berada di region yang sama dengan VM-nya. Kalau keduanya berbeda, perintahnya gagal dengan
pesan yang tidak menyebut region sebagai penyebabnya.
Pastikan akun aktif adalah email asisten dan hasil perintah terakhir sama dengan
PROJECT_ID. Jangan memakai slug asisten lain. Variabel Cloud Shell tidak tersimpan
secara permanen. Jalankan kembali blok ini jika sesi Cloud Shell terputus atau terminal
baru dibuka.
6. Verifikasi API bersama
Section titled “6. Verifikasi API bersama”API sudah diaktifkan oleh akun master. Asisten cukup memeriksa layanan utama:
gcloud services list --enabled \ --filter="config.name:(compute.googleapis.com OR cloudbuild.googleapis.com OR artifactregistry.googleapis.com OR iap.googleapis.com OR dns.googleapis.com OR domains.googleapis.com OR secretmanager.googleapis.com)" \ --format="value(config.name)"Hubungi akun master jika satu layanan belum terlihat. Asisten memang memiliki izin untuk mengaktifkan layanan yang terlewat, tetapi perubahan API berlaku untuk seluruh project kelompok.
7. Buat Service Account deployment
Section titled “7. Buat Service Account deployment”Asisten dapat membuat Service Account sendiri karena akun master sudah memberikan role IAM yang diperlukan.
gcloud iam service-accounts create "$BUILD_SA_NAME" \ --display-name="Cloud Build Deployer"Jika perintah menampilkan bahwa Service Account sudah ada, lanjutkan ke pemberian role.
for ROLE in \ roles/cloudbuild.builds.builder \ roles/artifactregistry.writer \ roles/compute.instanceAdmin.v1 \ roles/compute.osAdminLogin \ roles/iap.tunnelResourceAccessor \ roles/logging.logWriterdo gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:$BUILD_SA" \ --role="$ROLE" \ --condition=NonedoneJika gcloud meminta kondisi karena policy telah memiliki binding bersyarat, gunakan
--condition=None seperti pada contoh. Jangan memilih kondisi sementara milik koneksi
Cloud Build.
8. Periksa Artifact Registry bersama
Section titled “8. Periksa Artifact Registry bersama”gcloud artifacts repositories describe "$REPOSITORY" \ --location="$REGION"Jangan membuat repository katalog-images kembali. Semua asisten memakai repository
yang sama, tetapi nama image wajib berbeda melalui variabel IMAGE_NAME.
9. Siapkan identitas VM
Section titled “9. Siapkan identitas VM”VM memakai Compute Engine default Service Account. Service Account ini memerlukan akses untuk menarik image dari Artifact Registry. Service Account Cloud Build juga harus boleh bertindak pada identitas VM saat membuka SSH berbasis OS Login.
PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" \ --format='value(projectNumber)')"VM_SA="${PROJECT_NUMBER}-asisten01@example.com"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:$VM_SA" \ --role="roles/artifactregistry.reader" \ --condition=None
gcloud iam service-accounts add-iam-policy-binding "$VM_SA" \ --member="serviceAccount:$BUILD_SA" \ --role="roles/iam.serviceAccountUser" \ --condition=NoneBinding kedua sengaja diberikan pada VM_SA, bukan hanya pada project. Google
merekomendasikan pemberian roles/iam.serviceAccountUser pada Service Account tertentu
saat identitas tersebut dipakai oleh Compute Engine. Lihat
dokumentasi IAM Compute Engine.
10. Buat VM
Section titled “10. Buat VM”Konfigurasi yang telah diuji memakai e2-medium, Ubuntu 22.04 LTS, disk balanced 30 GB,
dan OS Login.
gcloud compute instances create "$VM_NAME" \ --zone="$ZONE" \ --machine-type="e2-medium" \ --image-family="ubuntu-2204-lts" \ --image-project="ubuntu-os-cloud" \ --boot-disk-size="30GB" \ --boot-disk-type="pd-balanced" \ --service-account="$VM_SA" \ --scopes="https://www.googleapis.com/auth/cloud-platform" \ --tags="webgis-http,webgis-iap-ssh" \ --metadata="enable-oslogin=TRUE"Jika VM sudah ada, jangan menjalankan perintah pembuatan kembali. Periksa statusnya:
gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format="table(name,status,machineType.basename(),networkInterfaces[0].accessConfigs[0].natIP)"Kunci alamat IP publik
Section titled “Kunci alamat IP publik”Kerjakan langkah ini segera setelah VM dibuat, sebelum melangkah lebih jauh. Alamat IP
sementara yang diberikan Compute Engine berubah setiap kali VM dimatikan dan dinyalakan
lagi, dan perubahan itu mematahkan tiga hal sekaligus: penanda halaman di browser, record DNS
bila kamu memakai domain, dan NEXTAUTH_URL pada .env.
Promosikan alamat yang sedang dipakai menjadi alamat statis:
EXTERNAL_IP="$(gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format='get(networkInterfaces[0].accessConfigs[0].natIP)')"
gcloud compute addresses create "$STATIC_IP_NAME" \ --region="$VM_REGION" \ --addresses="$EXTERNAL_IP"
gcloud compute addresses describe "$STATIC_IP_NAME" \ --region="$VM_REGION" \ --format="table(name,address,status)"Perhatikan $VM_REGION, bukan $REGION. Alamat IP statis harus berada di region yang sama
dengan VM-nya, sedangkan REGION menunjuk lokasi Artifact Registry bersama.
Nama alamat statis memakai slug asisten. Jangan memakai nama milik asisten lain.
Kalau alamat dengan nama itu sudah pernah dibuat, perintah create akan gagal dengan pesan
already exists. Itu bukan masalah, karena tujuannya sudah tercapai. Periksa hasilnya dengan
gcloud compute addresses describe, lalu pastikan statusnya IN_USE dan alamatnya sama
dengan alamat VM sendiri. Kalau berbeda, berarti alamat itu milik resource lain dan namanya
perlu diganti.
Batas kuota yang sudah diukur
Section titled “Batas kuota yang sudah diukur”Angka berikut diukur pada 12 dan 13 September 2026 di project kelompok, dengan empat VM berjalan. Sebagian kuota berlaku per project per region, sebagian lagi per project tanpa terikat region, dan perbedaannya penting: kuota yang per project tidak dapat ditambah dengan membuat project baru di region lain.
Nilainya bisa kembali ke bawaan bila billing project terganggu atau billing account diganti.
| Kuota | Cakupan | Batas | Artinya untuk pelatihan |
|---|---|---|---|
| In-use regional external IPv4 addresses | project per region | 4 | 4 VM ber-IP publik dalam satu region |
| VM instances | project per region | 8 | 8 VM dalam satu region |
| Static regional external IPv4 addresses | project per region | 8 | 8 IP statis dalam satu region |
| SSD_TOTAL_GB untuk disk balanced | project per region | 250 GB | 8 VM berdisk 30 GB |
| DISKS_TOTAL_GB untuk disk standard | project per region | 2048 GB | 68 VM berdisk 30 GB |
| CPUs | project per region | 32 | jauh di atas kebutuhan, bukan penghalang |
| CPUs (all regions) | project | 12 | batas seluruh VM dalam satu project, lintas region |
| E2_CPUS | project per region | 8 | batas khusus keluarga mesin E2 |
Empat hal yang perlu dipahami dari tabel itu:
-
Yang paling dulu mengikat adalah alamat IP publik. Batasnya 4 per project per region, dan itu terpakai penuh oleh empat VM. Menambah VM kelima di region yang sama ditolak dengan pesan berikut, dan penolakan ini sudah dibuktikan pada 13 September 2026:
Quota 'IN_USE_ADDRESSES' exceeded. Limit: 4.0 in region asia-southeast2.metric name = compute.googleapis.com/regional_in_use_addresseslimit name = IN-USE-ADDRESSES-per-project-regionlimit = 4.0dimensions = region: asia-southeast2Perhatikan baris
limit nameitu, karena namanya sendiri menyatakan cakupan kuotanya, yaitu per project per region. Pesan yang sama juga menyarankan memindahkan permintaan ke zone lain. Saran itu tidak berlaku di sini: kuota ini dihitung per region, jadi berpindah zone di region yang sama tidak menambah jatah satu alamat pun. -
Kuota dihitung per project, bukan per akun. Tiga project berbeda di region yang sama masing-masing mendapat jatah penuh, jadi tiga project memberi 12 alamat IP publik. Inilah sebabnya struktur pelatihan memakai satu project untuk empat peserta, bukan satu project untuk dua belas peserta.
-
Kuota dihitung per region, bukan per zone. Membagi VM ke
asia-southeast2-a,-b, dan-ctidak menambah jatah satu pun. Yang menambah jatah hanya pindah region. -
Jenis disk menentukan kuota mana yang terpakai.
pd-balancedmasuk keSSD_TOTAL_GByang hanya 250 GB, sedangkanpd-standardmasuk keDISKS_TOTAL_GByang 2048 GB. Panduan ini memakaipd-balancedkarena lebih cepat, dan pada pengukuran 13 September 2026 empat VM berdisk 30 GB memakai 120 GB dari 250 GB yang tersedia.
Bukti 12 VM di tiga project
Section titled “Bukti 12 VM di tiga project”Diuji pada 13 September 2026 memakai satu akun master dan tiga project di region yang sama,
asia-southeast2. Sebuah VM direkam menjadi machine image, lalu delapan VM dibuat dari rekaman
itu, empat di project B dan empat di project C, sementara project A tetap memakai empat VM yang
sudah ada. Keduabelas VM berstatus RUNNING pada saat bersamaan, masing-masing dengan alamat IP
publiknya sendiri.
Yang dibuktikan uji ini ada tiga. Pertama, kuota regional memang dihitung per project, bukan per akun, sehingga tiga project di region yang sama tidak saling mengurangi. Kedua, batas praktisnya adalah empat VM ber-IP publik per project per region, dan itu ditegaskan oleh pesan penolakan di atas. Ketiga, batas itu tidak dapat dilewati dengan berpindah zone.
Beban CPU tiga belas VM berjalan bersamaan
Section titled “Beban CPU tiga belas VM berjalan bersamaan”Diukur dari Cloud Shell memakai metrik compute.googleapis.com/instance/cpu/utilization, jendela
satu jam dengan rata-rata per lima menit. Angka di bawah adalah pemakaian terhadap satu vCPU,
sehingga nilai di atas 100 persen berarti VM sesaat memakai lebih dari satu vCPU.
| Kelompok | Jumlah VM | Umur saat diukur | CPU rata-rata per VM |
|---|---|---|---|
| VM tenang | 4 | 14 sampai 46 jam | 2,74 persen |
| VM baru menyala | 8 | sekitar 35 menit | 14,00 persen |
VM yang baru menyala memakai CPU sekitar lima kali lebih tinggi daripada VM yang sudah tenang. Penyebabnya fase menyalakan container, bukan isi datanya. Yang membuktikan itu: VM di project A justru yang berisi shapefile terunggul, tetapi pemakaian CPU-nya paling rendah.
Beban agregat seluruh VM pada saat pengukuran:
| Project | Jumlah VM | Rata-rata | Puncak |
|---|---|---|---|
| A | 5 | 0,12 vCPU | 0,74 vCPU |
| B | 4 | 0,42 vCPU | 2,51 vCPU |
| C | 4 | 0,71 vCPU | 3,80 vCPU |
| Total | 13 | 1,25 vCPU | 7,05 vCPU |
Bandingkan dengan kapasitas 32 vCPU per region dan 12 vCPU per project. Empat VM yang menyala bersamaan meminta sekitar 3,8 vCPU, sehingga satu project masih sanggup menyalakan keempat VM pesertanya secara serentak tanpa menyentuh batas CPU.
Mengukur pemakaian CPU seluruh VM dari Cloud Shell
Section titled “Mengukur pemakaian CPU seluruh VM dari Cloud Shell”Perintah gcloud tidak punya cara membaca deret waktu metrik, jadi pengukurannya lewat REST API
Cloud Monitoring dengan token dari gcloud. Jalankan dari Cloud Shell, bukan dari dalam VM.
PROJECTS="geoportal-kelompok-a-xxxxx geoportal-kelompok-b-xxxxx geoportal-kelompok-c-xxxxx"TOKEN="$(gcloud auth print-access-token)"END="$(date -u +%Y-%m-%dT%H:%M:%SZ)"START="$(date -u -v-60M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '60 minutes ago' +%Y-%m-%dT%H:%M:%SZ)"
for P in $PROJECTS; do curl -sS -H "Authorization: Bearer $TOKEN" \ "https://monitoring.googleapis.com/v3/projects/$P/timeSeries?filter=metric.type%3D%22compute.googleapis.com%2Finstance%2Fcpu%2Futilization%22&interval.startTime=$START&interval.endTime=$END&aggregation.alignmentPeriod=300s&aggregation.perSeriesAligner=ALIGN_MEAN" \ -o "/tmp/cpu-$P.json" echo "$P: $(wc -c < /tmp/cpu-$P.json) byte"doneLalu olah berkasnya. Nama VM ada di metric.labels.instance_name, bukan di
resource.labels, dan nilainya berupa pecahan nol sampai satu sehingga perlu dikali 100 untuk
menjadi persen.
python3 - <<'PY'import json, os
PROJECTS = ['geoportal-kelompok-a-xxxxx', 'geoportal-kelompok-b-xxxxx', 'geoportal-kelompok-c-xxxxx']SHORT = {p: p.split('-')[-2].upper() for p in PROJECTS}VCPU = 2 # e2-medium
semua = []for p in PROJECTS: path = f'/tmp/cpu-{p}.json' if not os.path.exists(path): print(f" {p}: berkas tidak ada"); continue d = json.load(open(path)) if 'error' in d: print(f" {p}: {d['error'].get('message','?')[:80]}"); continue for ts in d.get('timeSeries', []): nama = ts.get('metric', {}).get('labels', {}).get('instance_name', '?') vals = [float(pt['value']['doubleValue']) for pt in ts.get('points', []) if 'doubleValue' in pt['value']] if vals: semua.append({'project': SHORT[p], 'vm': nama, 'rata': sum(vals)/len(vals)*100, 'puncak': max(vals)*100, 'titik': len(vals)})
semua.sort(key=lambda r: -r['rata'])print(f"\n{'PROJ':<4} {'VM':<20} {'RATA2':>8} {'PUNCAK':>8} {'SMP':>5}")print("-" * 52)for r in semua: print(f"{r['project']:<4} {r['vm']:<20} {r['rata']:>7.2f}% {r['puncak']:>7.2f}% {r['titik']:>5}")print("-" * 52)tot = sum(r['rata'] for r in semua)print(f" {len(semua)} VM{'':<15} {tot:>7.2f}% setara {tot/100:.2f} vCPU")PYUmur VM diambil dari metadata instance, bukan dari metrik uptime. Metrik itu bersifat
kumulatif, sehingga jika dirata-ratakan hasilnya menyesatkan:
for P in $PROJECTS; do gcloud compute instances list --project="$P" \ --format="csv[no-heading](name,creationTimestamp)" | sed "s|^|$P,|"doneKenaikan kuota belum bisa diajukan sekarang. Formulir permohonannya menolak nilai di atas batas yang berlaku dengan alasan riwayat pemakaian belum mencukupi. Billing account ini sudah diaktifkan sebagai akun berbayar, jadi penolakan itu bukan karena jenis akun melainkan karena projectnya masih baru. Coba ajukan lagi setelah project punya riwayat pemakaian beberapa hari. Sementara itu, rancang di dalam angka tabel di atas.
asia-southeast1 sudah diperiksa dan batasnya identik dengan asia-southeast2: 4 alamat
IP publik, 8 VM, 8 IP statis, dan 250 GB disk balanced. Jadi memindahkan sebagian VM ke sana
benar-benar menambah jatah, sedangkan memindahkan VM ke zone lain di region yang sama tidak
menambah apa pun.
Mematikan VM melepas alamat IP-nya
Section titled “Mematikan VM melepas alamat IP-nya”VM yang dimatikan tetap memakai slot INSTANCES dan tetap memakai disknya, tetapi melepas
alamat IP publik dan berhenti memakai kuota CPU. Ini terlihat pada bacaan kuota 12 September
2026: tiga VM terdaftar, tetapi hanya dua alamat IP yang terpakai dan hanya dua unit CPU yang
tercatat.
Akibat praktisnya, alamat IP publik bisa dipakai bergiliran selama asisten tidak menguji bersamaan. Ini tidak berlaku untuk hari pelatihan, karena saat itu semua VM harus hidup pada waktu yang sama.
Alamat IP yang dilepas saat VM dimatikan adalah alamat sementara. Untuk VM yang dihubungkan ke domain, pakai alamat IP statis supaya record A pada DNS tidak perlu diperbarui setiap kali VM dinyalakan.
Kalau VM harus dihubungkan ke domain
Section titled “Kalau VM harus dihubungkan ke domain”Domain menuntut alamat IP publik, karena record A pada DNS hanya bisa menunjuk ke alamat IP. Terowongan IAP tidak bisa menggantikannya untuk keperluan ini: terowongan hanya hidup selama perintahnya berjalan di komputer seseorang, sedangkan domain harus tetap terbuka kapan saja bagi siapa saja.
Karena itu urutan pilihannya begini:
- Pindah region. Jatah 4 alamat IP publik berlaku per region, jadi VM kelima dan
seterusnya ditaruh di region lain, misalnya
asia-southeast1. Ini satu-satunya cara menambah VM berdomain tanpa mengubah arsitektur. - Pakai terowongan IAP hanya untuk VM yang memang tidak perlu domain, misalnya VM tambahan untuk mengukur beban lima VM serentak.
Pindah region tidak menuntut penyesuaian lain. Variabel ZONE sudah per asisten, firewall
allow-webgis-http berlaku di seluruh region karena memakai network tag, dan Cloud Domains
maupun Cloud DNS tidak terikat region.
VM tanpa alamat IP publik
Section titled “VM tanpa alamat IP publik”Kalau VM itu tidak perlu domain dan keempat slot alamat IPv4 eksternal sudah terpakai, VM berikutnya tetap bisa dibuat tanpa alamat IP publik dan diakses lewat terowongan IAP. Perbedaannya hanya satu tanda:
gcloud compute instances create "$VM_NAME" \ --zone="$ZONE" \ --machine-type="e2-medium" \ --image-family="ubuntu-2204-lts" \ --image-project="ubuntu-os-cloud" \ --boot-disk-size="30GB" \ --boot-disk-type="pd-balanced" \ --no-address \ --service-account="$VM_SA" \ --scopes="https://www.googleapis.com/auth/cloud-platform" \ --tags="webgis-http,webgis-iap-ssh" \ --metadata="enable-oslogin=TRUE"Terowongan IAP sudah terbukti bekerja pada 12 September 2026: /portal membalas 200 dan
/geoserver/web membalas 302 lewat terowongan, sama seperti lewat alamat IP publik.
Untuk memeriksa tanpa browser, jalankan terowongan di satu tab Cloud Shell lalu periksa dari tab kedua:
curl -sS -o /dev/null -w "portal %{http_code}\n" http://localhost:8080/portalcurl -sS -o /dev/null -w "geoserver %{http_code}\n" http://localhost:8080/geoserver/web11. Periksa firewall bersama
Section titled “11. Periksa firewall bersama”Firewall dibuat satu kali oleh akun master. Asisten hanya perlu memeriksa dua rule:
gcloud compute firewall-rules list \ --filter="name:(allow-webgis-http OR allow-webgis-iap-ssh)"
Gambar 3. VM berjalan dan dua rule firewall tersedia untuk dipakai bersama.
12. Masuk ke VM dan instal Docker
Section titled “12. Masuk ke VM dan instal Docker”Masuk ke VM melalui IAP:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iapJalankan perintah berikut di dalam VM untuk memasang Docker Engine, Docker Compose, Git, dan Google Cloud CLI.
sudo apt-get updatesudo apt-get install -y ca-certificates curl git gnupg
sudo install -m 0755 -d /etc/apt/keyringssudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.ascsudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOFTypes: debURIs: https://download.docker.com/linux/ubuntuSuites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")Components: stableArchitectures: $(dpkg --print-architecture)Signed-By: /etc/apt/keyrings/docker.ascEOF
sudo apt-get updatesudo apt-get install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | \ sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list > /dev/null
sudo apt-get updatesudo apt-get install -y google-cloud-cliPanduan instalasi Docker mengikuti dokumentasi resmi Docker untuk Ubuntu.
13. Periksa Docker dan gcloud CLI
Section titled “13. Periksa Docker dan gcloud CLI”Pastikan dua perangkat utama sudah terpasang sebelum menarik repository. Perintah
docker compose version harus menampilkan versi plugin Compose, bukan pesan bahwa
subperintah tidak dikenal.
sudo docker compose versionsudo docker run --rm hello-worldgcloud --versionJika docker run menolak dengan pesan izin, kelompok docker belum aktif untuk pengguna
saat ini. Tambahkan pengguna ke kelompok tersebut, keluar dari sesi SSH, lalu masuk kembali.
sudo usermod -aG docker "$USER"exitCloud Build menjalankan Docker sebagai root, jadi izin pengguna biasa tidak menghambat
deployment, tetapi perintah manual pada VM akan gagal tanpa perbaikan ini.
Siapkan juga folder aplikasi sebelum repository di-clone pada bagian berikutnya.
sudo mkdir -p /opt/webgissudo chown "$USER:$USER" /opt/webgisls -ld /opt/webgis14. Clone fork ke VM dan isi .env
Section titled “14. Clone fork ke VM dan isi .env”Container nextjs belum bisa dijalankan sebelum Cloud Build mengisi NEXTJS_IMAGE. Karena
itu urutan yang benar adalah clone, isi .env, hubungkan repository, buat trigger, baru push.
Setelah push, Cloud Build menarik image dan menyalakan container nextjs.
Clone fork
Section titled “Clone fork”Repository instruktur bersifat public, jadi clone cukup memakai HTTPS tanpa token. Ganti
GITHUB_USERNAME dengan username GitHub asisten.
GITHUB_USERNAME="ISI_USERNAME_GITHUB"GITHUB_REPOSITORY="https://github.com/${GITHUB_USERNAME}/personal-geoportal-peserta.git"git clone "$GITHUB_REPOSITORY" /opt/webgis/appcd /opt/webgis/appcp .env.example .envPeriksa pemilik direktori aplikasi sebelum melanjutkan:
ls -ld /opt/webgis/appHasilnya harus menampilkan nama pengguna VM, bukan root. Kalau dimiliki root, semua
perintah berikutnya yang menulis ke direktori itu akan gagal dengan Permission denied.
Gejala pertamanya biasanya muncul sebagai fatal: detected dubious ownership in repository,
karena git menolak bekerja di repository yang pemiliknya berbeda dari pengguna yang
menjalankannya. Penyebabnya adalah langkah sudo chown "$USER:$USER" /opt/webgis pada bagian
12 terlewat, atau clone dijalankan memakai sudo. Perbaiki dengan:
sudo chown -R "$USER:$USER" /opt/webgis/appls -ld /opt/webgis/appMenambahkan safe.directory lewat git config tidak menyelesaikan masalah ini. Perintah itu
hanya membungkam peringatannya, sedangkan izin tulisnya tetap tidak ada sehingga git pull
berikutnya gagal lagi.
Jika nama fork mengikuti nama bawaan personal-geoportal, sesuaikan URL-nya:
GITHUB_REPOSITORY="https://github.com/${GITHUB_USERNAME}/personal-geoportal.git"Periksa remote dan commit yang dipakai:
git remote -vgit log --oneline -1Commit dan push dikerjakan dari laptop yang sudah login GitHub, karena git push selalu
butuh kredensial walaupun repository public. VM hanya perlu membaca repository, jadi tidak
ada token yang perlu disimpan di VM.
Sinkronkan fork bila ada perubahan
Section titled “Sinkronkan fork bila ada perubahan”Kalau fork sudah pernah di-clone ke VM ini lalu berkas konfigurasinya berubah, tarik perubahannya sebelum menyalakan container. Tanpa langkah ini, VM masih memakai berkas lama dan gejalanya membingungkan karena berkas di GitHub sudah benar.
cd /opt/webgis/appgit status --shortgit pull --ff-onlygit log --oneline -1git status --short harus kosong sebelum git pull. Kalau ada perubahan lokal yang belum
disimpan, simpan atau batalkan dulu, karena --ff-only akan menolak menarik perubahan di
atas berkas yang sudah diubah.
Isi .env
Section titled “Isi .env”Ambil alamat IP eksternal VM dari Cloud Shell lain, atau catat nilai EXTERNAL_IP yang
muncul saat VM dibuat.
gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format='get(networkInterfaces[0].accessConfigs[0].natIP)'Buka .env pada VM:
nano /opt/webgis/app/.envBuat dulu tiga nilai acak. Masing-masing harus berbeda:
openssl rand -hex 32 # untuk JWT_SECRETopenssl rand -hex 32 # untuk NEXTAUTH_SECRETopenssl rand -hex 16 # untuk GEOSERVER_ADMIN_PASSWORDBuka .env pada VM, lalu isi dengan pola berikut. Ganti IP_EKSTERNAL_VM dengan alamat IP
yang muncul di atas, dan ganti ketiga nilai acak dengan hasil perintah sebelumnya. Jangan
memasukkan berkas .env yang sudah terisi ke Git.
# Nilai sementara. Cloud Build menimpanya dengan tag image setelah build pertama.NEXTJS_IMAGE=nginx:1.27-alpineDATABASE_URL=JWT_SECRET=ISI_SECRET_ACAKJWT_EXPIRES_IN=1hNEXTAUTH_SECRET=ISI_SECRET_ACAK_BERBEDANEXTAUTH_URL=http://IP_EKSTERNAL_VM/portalAUTH_API_URL=http://localhost:3000/portal/apiGEOSERVER_ADMIN_PASSWORD=ISI_KATA_SANDI_GEOSERVERBASE_URL=http://IP_EKSTERNAL_VM/portalNEXT_PUBLIC_URL_BASE_PATH=http://IP_EKSTERNAL_VM/portalPOSTGIS_HOST=ISI_HOST_POSTGISPOSTGIS_PORT=5432POSTGIS_DB=postgresPOSTGIS_USER=ISI_USER_POSTGISPOSTGIS_PASSWORD=ISI_KATA_SANDI_POSTGISPOSTGIS_SCHEMA=gisGEOSERVER_URL=http://geoserver:8080/geoserverGEOSERVER_USERNAME=adminGEOSERVER_PASSWORD=ISI_KATA_SANDI_GEOSERVERGEOSERVER_WORKSPACE=geoportalGEOSERVER_POSTGIS_DATASTORE=ISI_NAMA_DATASTOREBASE_URL dan NEXTAUTH_URL memakai nilai yang sama, keduanya tanpa slash di akhir. Ini
berbeda dari versi panduan sebelumnya yang mewajibkan slash pada NEXTAUTH_URL, karena
aplikasi tidak lagi memakai trailingSlash. Keduanya tanpa slash
karena kode menambahkan sendiri path API setelahnya.
BASE_URL tidak lagi dibaca kode setelah sinkronisasi 12 September 2026, karena pemanggilan
API dari komponen klien kini memakai alamat relatif. Penggantinya adalah
NEXT_PUBLIC_URL_BASE_PATH, yang dipakai satu kali untuk menyusun URL berkas model pada rute
api/katalog-data-3d/create. Kalau nilainya kosong, ketiga panggilan itu menjadi
relatif dan gagal dengan 404. Fitur ini juga memerlukan DATABASE_URL yang valid, jadi
pengujiannya baru bisa dilakukan setelah database diisi.
Untuk menguji build, Nginx, dan halaman publik, DATABASE_URL boleh kosong. Login, Prisma,
dan fitur katalog memerlukan database yang valid.
Nyalakan GeoServer dan Nginx lebih dahulu
Section titled “Nyalakan GeoServer dan Nginx lebih dahulu”Dua container ini tidak menunggu Cloud Build, jadi bisa dinyalakan sekarang untuk memastikan
berkas konfigurasi sudah benar. Langkah ini juga memicu pembuatan data directory GeoServer
pertama kali, jadi container geoserver_app terasa lambat pada percobaan pertama.
cd /opt/webgis/appsudo docker compose up -d --no-deps geoserver nginxsudo docker compose psTanda --no-deps penting. Tanpa itu, compose ikut menyalakan service yang menjadi dependensi
nginx, termasuk nextjs yang image-nya belum dibangun. Dengan tanda itu, hanya dua service
yang disebut yang dijalankan.
Hasil yang benar pada tahap ini hanya menampilkan dua container: geoserver_app dan
nginx_proxy. Container nextjs_portal memang belum ada, karena baru dibuat oleh Cloud Build
pada bagian 17.
Kolom PORTS pada geoserver_app menampilkan 8080/tcp tanpa 0.0.0.0 di depannya. Itu
artinya port tersebut hanya terbuka di dalam jaringan container, dan memang disengaja. Akses
dari luar melewati Nginx pada port 80, dengan alamat /geoserver/web. Jadi jangan menguji
GeoServer lewat http://IP_VM:8080, karena koneksinya memang akan ditolak.
docker compose up untuk nextjs belum bisa dijalankan karena NEXTJS_IMAGE masih kosong.
Biarkan container nextjs dibuat oleh Cloud Build.
Periksa instalasi, lalu keluar dari VM:
sudo docker compose versiongcloud --versionexit15. Hubungkan repository GitHub
Section titled “15. Hubungkan repository GitHub”Lakukan langkah berikut pada Google Cloud Console:
- Buka Cloud Build, lalu pilih Repositories.
- Pilih Connect repository.
- Isi nama connection sesuai
CONNECTION_NAME, misalnyagithub-nama01. - Pilih GitHub dan login memakai akun pemilik fork.
- Izinkan Cloud Build GitHub App hanya untuk repository pelatihan jika memungkinkan.
- Pilih repository fork asisten.
- Isi nama linked repository sesuai
LINKED_REPO_NAME, misalnyarepo-nama01. - Selesaikan koneksi dan pastikan statusnya
COMPLETE.
Jika halaman meminta Secret Manager API, kembali ke bagian 6 dan pastikan
secretmanager.googleapis.com sudah aktif.
Satu project dapat memiliki beberapa GitHub connection. Jangan menghapus atau mengubah connection asisten lain.
16. Buat trigger Cloud Build
Section titled “16. Buat trigger Cloud Build”Buat trigger dengan konfigurasi berikut:
| Pengaturan | Nilai |
|---|---|
| Event | Push to a branch |
| Repository | Fork milik asisten |
| Branch | ^main$ |
| Configuration | Cloud Build configuration file |
| File | cloudbuild.yaml |
| Trigger name | Nilai TRIGGER_NAME, misalnya deploy-nama01 |
| Service Account | Nilai BUILD_SA, misalnya cb-deployer-nama01@PROJECT_ID.iam.gserviceaccount.com |
Tambahkan empat substitution variable berikut. Keempatnya wajib diisi. Berkas
cloudbuild.yaml pada fork tidak lagi menyediakan nilai bawaan untuk variabel ini, jadi
build akan berhenti dengan pesan yang menyebut nama variabel yang kosong. Kegagalan itu
disengaja: tanpa nilai unik, image akan memakai satu nama bersama dan menimpa image milik
asisten lain di Artifact Registry.
_VM_NAME = webgis-nama01_VM_ZONE = asia-southeast2-b_VM_APP_DIR = /opt/webgis/app_IMAGE_NAME = nextjs-nama01Ganti suffix nama01 sesuai PARTICIPANT_ID. Nilai _VM_NAME, _IMAGE_NAME, dan Service
Account harus menunjuk ke resource milik asisten yang sama. Simpan trigger, lalu pilih
Run. Push berikutnya ke branch main akan menjalankan build, push image unik, login ke
VM asisten melalui IAP, mengisi NEXTJS_IMAGE, menarik image, dan menjalankan Docker
Compose.
17. Push dari laptop dan jalankan build
Section titled “17. Push dari laptop dan jalankan build”Commit dilakukan dari laptop, bukan dari VM. Push inilah yang memicu trigger dan menjalankan seluruh step Cloud Build.
cd personal-geoportal-pesertagit status --shortgit add docker-compose.yml nginx.conf cloudbuild.yaml .gitignoregit commit -m "feat: tambah GeoServer dan rute nginx"git push origin mainPastikan berkas .env yang sudah terisi tidak ikut ter-commit. Periksa dengan
git status --short sebelum git add, dan jangan pernah memakai git add . tanpa
memeriksa daftar berkas terlebih dahulu.
18. Verifikasi Cloud Build dan container
Section titled “18. Verifikasi Cloud Build dan container”Build yang berhasil menampilkan seluruh step dengan status hijau.

Gambar 4. Build, push, dan deployment SSH selesai tanpa error.
Periksa container dari Cloud Shell. Ada tiga container yang harus terlihat: nextjs_portal,
geoserver_app, dan nginx_proxy.
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && sudo docker compose ps'
Gambar 5. Tangkapan dari pengujian 10 September 2026 ini baru menampilkan nextjs_portal dan
nginx_proxy. Setelah GeoServer berjalan pada 11 September 2026, daftar container bertambah
satu sehingga menjadi tiga.
Cloud Build hanya menarik image nextjs. Container geoserver_app dan nginx_proxy sudah
berjalan sejak bagian 14 dan tidak ikut dibangun ulang. Jika Cloud Build hijau tetapi
geoserver_app hilang, berarti docker-compose.yml di VM masih versi dua container dan
perlu disinkronkan seperti pada bagian 28.
19. Buka GeoServer
Section titled “19. Buka GeoServer”GeoServer berjalan di belakang Nginx pada port 8080 di dalam jaringan container, jadi port tersebut tidak dibuka ke internet. Akses masuk melalui Nginx pada alamat berikut:
EXTERNAL_IP="$(gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format='get(networkInterfaces[0].accessConfigs[0].natIP)')"
echo "http://${EXTERNAL_IP}/geoserver/web"curl -sSIL --max-redirs 3 "http://${EXTERNAL_IP}/geoserver/web"Perhatikan alamat yang dipakai adalah /geoserver/web tanpa slash di akhir. GeoServer
mengalihkan /geoserver/web/ ke bentuk tanpa slash, jadi kalau dibalik, kamu hanya melihat
satu lompatan tambahan yang tidak perlu.
Login memakai nilai GEOSERVER_ADMIN_USER dan GEOSERVER_ADMIN_PASSWORD pada
docker-compose.yml, bukan admin dan geoserver bawaan image.
Container GeoServer memerlukan waktu boot paling lama di antara tiga container. Periksa log sampai muncul penanda selesai:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='sudo docker compose -f /opt/webgis/app/docker-compose.yml logs --tail=40 geoserver'Jika halaman tidak terbuka, periksa tiga hal berurutan:
docker compose psmenampilkangeoserver_appdengan statusrunning.sudo docker compose exec geoserver curl -sS http://localhost:8080/geoserver/webmenghasilkan HTML. Port 8080 sengaja tidak dipublikasikan ke VM, jadi perintah ini harus dijalankan di dalam container, bukan langsung dari shell VM.- Blok
location /geoserver/padanginx.confsudah ikut ter-push ke VM. Berkas Nginx berasal dari clone repository, bukan dari image, jadi perubahannginx.confperlu sinkronisasi seperti pada bagian 28.
20. Unggah data spasial ke GeoServer
Section titled “20. Unggah data spasial ke GeoServer”Bagian ini adalah inti pengujian. Tujuannya membuktikan bahwa GeoServer di VM benar-benar
menerima data spasial baru, bukan hanya bisa dibuka. Gunakan berkas contoh
batas_admin.geojson atau jalan.geojson dari folder public/data/ pada repository fork.
Buat workspace
Section titled “Buat workspace”- Buka Data, lalu Workspaces.
- Pilih Add new workspace.
- Isi Name dengan
geoportaldan Namespace URI denganhttp://IP_EKSTERNAL_VM/geoportal. - Pilih Save.
Siapkan data di VM
Section titled “Siapkan data di VM”GeoServer tidak menyediakan store GeoJSON pada instalasi bawaannya. Format itu adalah
ekstensi terpisah, dan image kartoza/geoserver tidak memasangnya. Jadi berkas GeoJSON tidak
bisa diunggah langsung.
Data diubah lebih dahulu menjadi shapefile, karena store Shapefile tersedia tanpa konfigurasi tambahan dan geometrinya dikenali langsung dari header berkas. Repository fork sudah memuat hasil konversinya, jadi langkah ini tidak perlu diulang.
cd /opt/webgis/appgit pull
sudo docker compose cp public/data/batas_admin.zip geoserver:/opt/geoserver/data_dir/sudo docker compose exec -T geoserver sh -c \ "cd /opt/geoserver/data_dir && unzip -o batas_admin.zip -d batas_admin"sudo docker compose exec -T geoserver ls -la /opt/geoserver/data_dir/batas_admin/Perintah terakhir harus menampilkan empat berkas: batas_admin.shp, .shx, .dbf, dan
.prj. GeoServer tidak dapat membaca shapefile dari dalam zip, jadi ekstraksi ini wajib.
Buat store shapefile
Section titled “Buat store shapefile”- Buka Data, lalu Stores.
- Pilih Add new store.
- Pilih Shapefile pada daftar Vector Data Sources.
- Pilih workspace
geoportal. - Isi Data Source Name dengan
batas_admin. - Pada Connection Parameters, tekan Browse, buka folder
batas_admin, lalu pilihbatas_admin.shp. - Isi Charset dengan
UTF-8. - Pilih Save.
Nama Data Source yang dipakai adalah batas_admin, sama dengan nama berkasnya. GeoServer
memakai nama itu juga sebagai nama layer, sehingga layer akhirnya menjadi
geoportal:batas_admin. Memakai nama berbeda untuk store dan layer hanya menambah
kebingungan.
Publish layer
Section titled “Publish layer”Setelah Save, GeoServer menampilkan halaman New Layer dengan batas_admin terdeteksi.
- Pilih Publish.
- Pada tab Data, periksa bagian Feature Type Details. Salah satu kolom harus bertipe Geometry. Kalau semua kolom bertipe String, berarti store tidak menemukan geometrinya.
- Tekan Compute from data, lalu Compute from native bounds.
- Periksa bahwa Native Bounding Box sudah terisi angka, bukan
0dan-1. - Pilih Save.
Atur izin baca publik
Section titled “Atur izin baca publik”Layer baru hanya bisa dilihat pengguna yang login. Sebelum izin diberikan, layer bahkan tidak
muncul pada GetCapabilities, sehingga permintaan WFS membalas InvalidParameterValue dengan
locator="typeName" seolah-olah layernya tidak ada. Pesan itu menyesatkan, karena layernya
sudah ada tetapi belum diumumkan ke publik.
- Buka Security, lalu Layers.
- Cari
geoportal:batas_admin. - Pilih ROLE_ANONYMOUS, lalu aktifkan Read.
- Pilih Save.
Jika langkah ini terlewat, permintaan WMS akan menampilkan halaman login, bukan gambar peta, dan permintaan WFS akan gagal dengan pesan layer tidak ditemukan.
21. Verifikasi data hasil unggah
Section titled “21. Verifikasi data hasil unggah”Periksa kapabilitas dan geometri melalui endpoint WFS. Nama layer mengikuti nama Data Source,
jadi pada contoh ini geoportal:batas_admin.
EXTERNAL_IP="$(gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format='get(networkInterfaces[0].accessConfigs[0].natIP)')"NAMA_LAYER="geoportal:batas_admin"
curl -sS "http://${EXTERNAL_IP}/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typeNames=${NAMA_LAYER}&outputFormat=application/json&count=1"Data dinyatakan berhasil diunggah jika perintah tersebut mengembalikan JSON dengan isi
features, seperti ini:
{"type":"FeatureCollection","features":[{"type":"Feature","id":"batas_admin.1","geometry":{"type":"MultiPolygon","coordinates":...Jika jawabannya ServiceException dengan locator="typeName", periksa dua hal berurutan.
Pertama, pastikan nama workspace dan nama layer sudah benar pada Data → Layers. Kedua,
pastikan izin Read untuk ROLE_ANONYMOUS sudah diberikan, karena tanpa izin itu layer tidak
muncul pada GetCapabilities dan seolah-olah tidak ada.
Data yang sudah diunggah tersimpan pada volume ./geoserver-data di VM, sehingga bertahan
setelah container di-restart. Ini pengujian terpenting pada bagian ini, karena tanpa volume
kerja, seluruh layer hasil unggahan akan hilang setiap kali container dibuat ulang.
Restart container satu kali untuk membuktikan hal ini:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && sudo docker compose restart geoserver'Ulangi perintah WFS setelah GeoServer selesai boot. Jika layer hilang setelah restart, catat sebagai temuan karena volume tidak terpasang dengan benar.
22. Buka Geoportal
Section titled “22. Buka Geoportal”Ambil alamat IP eksternal:
EXTERNAL_IP="$(gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format='get(networkInterfaces[0].accessConfigs[0].natIP)')"
echo "http://${EXTERNAL_IP}/portal"curl -sSIL --max-redirs 3 "http://${EXTERNAL_IP}/portal"Buka URL yang dicetak oleh perintah tersebut. Aplikasi tidak memakai trailingSlash, jadi
/portal adalah bentuk kanoniknya. Bentuk berslash tetap bekerja karena dialihkan, tetapi
menambah satu lompatan.

Gambar 6. Geoportal hasil deployment dapat diakses melalui browser.
23. Catat temuan spesifikasi default
Section titled “23. Catat temuan spesifikasi default”Catat keadaan VM sebelum ada perubahan konfigurasi apa pun. Data inilah yang menentukan
apakah heap -Xmx2048m cukup untuk e2-medium. Pengukuran pertama pada 11 September 2026
sudah menunjukkan hasil yang lega, dan angkanya tersedia sebagai kolom acuan pada
workbook asisten trainer. Tugasmu adalah memastikan VM sendiri
sejauh itu juga.
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='free -h && sudo docker stats --no-stream && sudo docker inspect --format "{{.Name}} OOMKilled={{.State.OOMKilled}}" geoserver_app'Isi hasilnya ke tabel bukti pada workbook asisten trainer. Jika
OOMKilled=true atau memori tersisa di bawah 300 MB, turunkan heap GeoServer dengan
mengubah MAXIMUM_MEMORY menjadi 1024m, lalu jalankan sudo docker compose up -d geoserver.
Jangan mengubah tipe mesin sebelum temuan ini dicatat lebih dahulu. Pengukuran lima VM
serentak ada pada bagian 12 di workbook asisten trainer.
24. Arahkan domain ke VM
Section titled “24. Arahkan domain ke VM”Bagian ini dijalankan kalau VM perlu dibuka lewat nama domain. Ada dua jalan, dan yang pertama jauh lebih murah.
Alamat IP statis sudah dikunci pada bagian 10, jadi record DNS di bawah ini tidak akan patah saat VM dimatikan dan dinyalakan kembali.
Pakai domain atau subdomain yang sudah ada
Section titled “Pakai domain atau subdomain yang sudah ada”Ini jalur yang paling murah, dan sudah dibuktikan pada 12 September 2026. Tidak ada registrasi baru, tidak ada biaya tahunan, dan Cloud DNS tidak diperlukan. Cukup domain yang sudah dimiliki, bisa dari registrar mana pun.
-
Pastikan alamat IP statis sudah terkunci seperti pada bagian 10.
-
Buka pengelola DNS di tempat domain itu didaftarkan.
-
Tambahkan record berikut. Ganti
webgisdengan nama subdomain pilihanmu.Nama Jenis TTL Isi webgisA300alamat IP statis VM www.webgisbila perluCNAME300webgis.contoh.com.dengan titik di akhir -
Tunggu propagasi, lalu periksa memakai perintah pada bagian di bawah.
Subdomain lebih disarankan daripada domain utama, karena satu domain bisa dipakai beberapa asisten dengan nama berbeda tanpa saling menimpa.
Pakai subdomain dari satu domain lembaga
Section titled “Pakai subdomain dari satu domain lembaga”Ini jalur termurah untuk pelatihan berjamaah. Lembaga penyelenggara membeli satu domain
untuk semua orang, misalnya gisbigtrainer.com, lalu setiap asisten memakai satu subdomain
unik seperti nama01.gisbigtrainer.com. Biaya tahunannya satu domain saja, bukan satu domain
per peserta.
Yang perlu disiapkan pemilik domain:
- Satu domain yang DNS-nya bisa diatur. Arahkan nameserver-nya ke Cloudflare kalau ingin HTTPS gratis tanpa sertifikat di VM.
- Satu record
Aper asisten, dengan nama subdomain sesuai slug dan isi alamat IP statis asisten itu. - TTL 300 detik supaya perubahan cepat tersebar.
Alurnya berjalan dua arah, jadi tetapkan tenggat sebelum hari pelatihan. Asisten tidak bisa menyelesaikan bagian ini sendirian.
| Langkah | Siapa | Yang dilakukan |
|---|---|---|
| 1 | Asisten | Buat VM, kunci alamat IP statisnya, lalu catat alamatnya |
| 2 | Asisten | Laporkan slug dan alamat IP statis ke koordinator |
| 3 | Pemilik domain | Tambahkan record A bernama slug menuju alamat IP itu |
| 4 | Asisten | Periksa hasilnya dengan dig, lalu lanjut ke HTTPS |
SUBDOMAIN="nama01.gisbigtrainer.com"
dig +short "$SUBDOMAIN"curl -sSIL --max-redirs 3 "http://${SUBDOMAIN}/portal"Hasil dig harus sama persis dengan alamat IP statis VM sendiri. Kalau berbeda, bandingkan
dengan resolver publik sebelum menyalahkan recordnya:
dig +short "$SUBDOMAIN" @1.1.1.1Kalau resolver publik mengembalikan alamat yang benar sedangkan resolver di perangkatmu tidak, berarti masalahnya di jaringan lokal, bukan di recordnya. Jaringan rumah dengan AdGuard, Pi-hole, atau router yang mengarahkan seluruh subdomain ke satu server akan menang atas record publik. Untuk memastikan VM-nya sendiri benar, paksa alamatnya:
curl -sS -o /dev/null -w "ip=%{remote_ip} kode=%{http_code}\n" \ --resolve "$SUBDOMAIN:443:ALAMAT_IP_STATIS" \ "https://${SUBDOMAIN}/portal"Balasan 200 dengan ip sama dengan alamat statis berarti VM sehat. Uji juga dari ponsel
memakai data seluler, karena di sana tidak ada resolver lokal yang mengganggu.
Daftarkan domain baru
Section titled “Daftarkan domain baru”Jalur ini hanya perlu diambil kalau belum ada domain sama sekali. Harga bergantung pada akhiran domain yang dipilih, registrasi memiliki komitmen satu tahun, dan perpanjangan otomatis aktif secara default. Cloud DNS ditagih terpisah.
Cloud Domains tidak mengirim hak akses ke Billing Account kepada peserta pelatihan. Biaya domain masuk ke Billing Account yang sudah terhubung dengan project. Registrasi domain tidak dapat dipindahkan ke project GCP lain setelah dibuat.
- Buka Cloud Domains pada Google Cloud Console.
- Pilih Register domain.
- Terima persyaratan Squarespace jika diminta.
- Cari nama domain dan periksa harga tahunannya.
- Pilih Use Cloud DNS dan buat zone baru dengan nama sesuai
DNS_ZONE_NAME. - Isi data kontak registrant dengan data yang benar.
- Periksa kembali biaya, lalu selesaikan registrasi.
- Buka email registrant dan selesaikan verifikasi maksimal 15 hari setelah registrasi.
Dokumentasi resmi tersedia pada panduan registrasi Cloud Domains dan harga Cloud Domains.
Buat record DNS
Section titled “Buat record DNS”Ambil alamat statis yang sudah dikunci pada bagian 10:
gcloud compute addresses describe "$STATIC_IP_NAME" \ --region="$VM_REGION" \ --format="value(address)"Tambahkan record pada pengelola DNS yang kamu pakai. Kalau domainnya didaftarkan lewat Cloud Domains, pengelolanya ada pada Network services, lalu Cloud DNS. Kalau kamu memakai domain atau subdomain yang sudah ada, pengelolanya adalah situs registrar domain itu.
| DNS name | Jenis | TTL | Data |
|---|---|---|---|
| Nama domain atau subdomain | A |
300 |
alamat statis VM |
www |
CNAME |
300 |
nama domain utama dengan titik di akhir |
Contoh nilai CNAME untuk domain contoh.com adalah contoh.com.. Tunggu propagasi,
lalu periksa dari Cloud Shell:
DOMAIN_NAME="contoh.com"
dig +short "$DOMAIN_NAME"curl -sSIL --max-redirs 3 "http://${DOMAIN_NAME}/portal"Jika record sudah resolve, buka http://DOMAIN_ANDA/portal. Alamat ini tidak akan berubah
saat VM dimatikan dan dinyalakan kembali, karena alamat IP-nya sudah statis.
Aktifkan HTTPS dengan Certbot
Section titled “Aktifkan HTTPS dengan Certbot”Bagian ini menerbitkan sertifikat Let’s Encrypt langsung ke VM masing-masing. Setiap asisten memegang sertifikatnya sendiri, dan tidak ada lapisan perantara yang menyembunyikan cara kerjanya.
Repository fork sudah menyiapkan tiga hal yang diperlukan, yaitu direktori tantangan ACME,
direktori untuk arahan TLS, dan include pada nginx.conf yang membaca direktori itu.
Direktori TLS sengaja dibiarkan kosong, sehingga deployment pertama tetap berjalan sebagai
HTTP tanpa perubahan apa pun.
Langkah 1, pasang Certbot dan siapkan dua direktori.
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='sudo apt-get update && sudo apt-get install -y certbot && cd /opt/webgis/app && mkdir -p certbot-webroot tls'Langkah 2, sinkronkan konfigurasi dan nyalakan ulang.
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && git pull --ff-only && sudo docker compose up -d && sudo docker compose ps'Pastikan nginx_proxy berstatus running. Pada tahap ini situs masih dilayani lewat HTTP,
dan itu memang yang diinginkan.
Langkah 3, terbitkan sertifikat.
SUBDOMAIN="nama01.gisbigtrainer.com"EMAIL="nama01@example.com"
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command="cd /opt/webgis/app && sudo certbot certonly --webroot -w /opt/webgis/app/certbot-webroot -d $SUBDOMAIN --non-interactive --agree-tos -m $EMAIL"Domain harus sudah mengarah ke alamat IP statis VM sebelum langkah ini. Kalau dig +short
belum mengembalikan alamat yang benar, tunggu propagasi lebih dahulu.
Sebelum menerbitkan, periksa bahwa jalur tantangan ACME sudah dilayani Nginx dan bukan diteruskan ke aplikasi:
curl -sS -o /dev/null -w "acme %{http_code}\n" "http://${SUBDOMAIN}/.well-known/acme-challenge/uji"Balasan 404 adalah hasil yang benar, artinya Nginx mencari berkasnya di direktori tantangan
dan tidak menemukannya, persis seperti yang seharusnya. Kalau yang muncul 502 atau 200,
berarti permintaan itu jatuh ke aplikasi Next.js dan blok location untuk ACME belum ikut
terpasang. Perbaiki dulu sebelum menjalankan Certbot, karena kegagalan di sini akan muncul
sebagai pesan Timeout during connect yang menyesatkan.
Langkah 4, nyalakan HTTPS.
Satu berkas baru sudah cukup, dan berkas itu tidak ikut Git sehingga git pull berikutnya
tetap berjalan tanpa konflik:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command="printf '%s\n' 'listen 443 ssl;' 'ssl_certificate /etc/letsencrypt/live/$SUBDOMAIN/fullchain.pem;' 'ssl_certificate_key /etc/letsencrypt/live/$SUBDOMAIN/privkey.pem;' | sudo tee /opt/webgis/app/tls/aktifkan.conf >/dev/null && cd /opt/webgis/app && sudo docker compose exec -T nginx nginx -t && sudo docker compose exec -T nginx nginx -s reload"Perintah itu memeriksa sintaksnya lebih dahulu, lalu memuat ulang Nginx. Kalau nginx -t
gagal, Nginx tidak jadi dimuat ulang dan situs HTTP tetap hidup, sehingga tidak ada yang mati
akibat salah ketik.
Langkah ini cukup memakai nginx -s reload, tanpa membuat ulang container, karena
tls/aktifkan.conf berada di dalam direktori yang dipasang. Berkas baru di dalam direktori
yang dipasang langsung terlihat. Ini berbeda dari nginx.conf yang dipasang sebagai berkas
tunggal, dan perbedaannya dibahas pada bagian 28.
Periksa dari luar:
curl -sSIL --max-redirs 3 "https://${SUBDOMAIN}/portal"Yang benar adalah balasan 200 pada alamat berawalan https://.
Kalau permintaan HTTPS gagal, periksa dua hal ini lebih dahulu:
- Firewall
allow-webgis-httpmembuka port 80 dan 443. Kalau hanya 80, Let’s Encrypt tetap bisa memverifikasi, tetapi HTTPS tidak akan bisa dibuka. - Berkas
/opt/webgis/app/tls/aktifkan.confbenar-benar ada dan nama direktori sertifikatnya sama dengan subdomain sendiri.
Langkah 5, perpanjangan otomatis.
Sertifikat Let’s Encrypt berlaku 90 hari. Certbot memasang timer perpanjangan sendiri, tetapi container Nginx perlu dimuat ulang setiap sertifikat diperbarui. Buat skrip hook berikut:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy && printf "%s\n" "#!/bin/sh" "cd /opt/webgis/app && docker compose exec -T nginx nginx -s reload" | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null && sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh && sudo certbot renew --dry-run'--dry-run memakai server percobaan Let’s Encrypt, jadi aman dijalankan berulang kali dan
tidak memakai jatah penerbitan sebenarnya.
Catatan HTTPS
Section titled “Catatan HTTPS”Tidak ada role GCP tambahan untuk alur Let’s Encrypt atau Certbot pada modul. Role
roles/compute.osAdminLogin memberi peserta pelatihan akses administratif pada VM. Firewall
bersama allow-webgis-http sudah membuka port 80 dan 443 untuk VM dengan network tag
webgis-http. Jangan membuat firewall HTTPS baru untuk setiap asisten.
Deployment yang telah diuji menjalankan Nginx di dalam container. Perintah
certbot --nginx pada host dari modul tidak dapat langsung dipakai karena plugin tersebut
mencari Nginx host, sedangkan port 80 dipakai container.
Lihat juga catatan Modul 5.
Google Certificate Manager tidak digunakan pada alur modul ini. Karena itu,
roles/certificatemanager.editor dan Certificate Manager API tidak perlu diberikan.
Jika domain hanya dipakai untuk pengujian dan tidak ingin diperpanjang tahun berikutnya, matikan perpanjangan otomatis pada pengaturan registrasi Cloud Domains setelah pengujian. Biaya periode satu tahun yang sudah dibeli tetap berlaku.
25. Cadangkan data dan konfigurasi
Section titled “25. Cadangkan data dan konfigurasi”Tiga hal perlu dicadangkan, dan hanya dua di antaranya berada di VM.
| Yang dicadangkan | Letaknya | Keadaan sekarang |
|---|---|---|
docker-compose.yml, nginx.conf, cloudbuild.yaml, robots.js, sitemap.js |
fork di GitHub | tercadang otomatis setiap push |
Berkas .env |
/opt/webgis/app/.env di VM |
disalin manual ke pengelola kata sandi |
Folder geoserver-data |
/opt/webgis/app/geoserver-data di VM |
dicadangkan harian ke Cloud Storage |
Siapkan bucket, sekali saja
Section titled “Siapkan bucket, sekali saja”BUCKET="cadangan-webgis-${PROJECT_ID}"
gcloud storage buckets create "gs://${BUCKET}" \ --location=asia-southeast2 \ --uniform-bucket-level-accessNama bucket harus unik di seluruh dunia. Kalau nama itu sudah dipakai orang lain, tambahkan slug asisten di belakangnya.
Jadwalkan pencadangan harian
Section titled “Jadwalkan pencadangan harian”Ganti PROJECT_ID dan SLUG_ASISTEN dengan milik sendiri:
gcloud compute ssh "$VM_NAME" --zone="$ZONE" --tunnel-through-iap --command='sudo tee /etc/cron.daily/cadangkan-webgis >/dev/null <<"EOF"#!/bin/shset -eBUCKET="cadangan-webgis-PROJECT_ID"SLUG="SLUG_ASISTEN"STAMP=$(date +%F)cd /opt/webgis/apptar -czf /tmp/geoserver-data-$STAMP.tar.gz geoserver-datagcloud storage cp /tmp/geoserver-data-$STAMP.tar.gz gs://$BUCKET/$SLUG/rm -f /tmp/geoserver-data-$STAMP.tar.gzEOFsudo chmod +x /etc/cron.daily/cadangkan-webgis'Jalankan sekali untuk membuktikan skripnya bekerja, lalu periksa isi bucket:
gcloud compute ssh "$VM_NAME" --zone="$ZONE" --tunnel-through-iap \ --command='sudo /etc/cron.daily/cadangkan-webgis && echo "skrip selesai tanpa error"'
gcloud storage ls -l "gs://${BUCKET}/${SLUG_ASISTEN}/"Nama berkasnya memakai tanggal, jadi pencadangan di hari yang sama menimpa arsip sebelumnya. Itu memang yang diinginkan: satu arsip per hari, bukan menumpuk belasan.
Pulihkan dari arsip
Section titled “Pulihkan dari arsip”Jalankan seluruh blok berikut. Sesuaikan nama arsipnya dengan yang ada di bucket.
gcloud compute ssh "$VM_NAME" --zone="$ZONE" --tunnel-through-iap --command='cd /opt/webgis/appBUCKET="cadangan-webgis-PROJECT_ID"SLUG="SLUG_ASISTEN"ARCHIVE="geoserver-data-2026-09-12.tar.gz"
gcloud storage cp "gs://$BUCKET/$SLUG/$ARCHIVE" /tmp/
sudo docker compose stop geoserversudo tar -xzf /tmp/$ARCHIVEsudo docker compose up -d --force-recreate geoserver'Tiga hal yang perlu diperhatikan pada blok itu.
Ekstrak selalu dijalankan dari /opt/webgis/app, supaya isi arsip mendarat di tempat yang
benar. Arsip itu memuat jalur geoserver-data/... di dalamnya.
--force-recreate diperlukan. Tanpa itu Compose hanya menyalakan kembali container lama
dengan mount yang sudah ada.
Kepemilikan berkas biasanya sudah benar tanpa langkah tambahan. tar yang dijalankan
sebagai root mempertahankan kepemilikan yang tersimpan di dalam arsip, dan arsipnya dibuat
dari folder yang sama. Kalau GeoServer menolak menulis ke data directory-nya setelah
pemulihan, samakan kepemilikannya dengan folder yang sedang dipakai container:
sudo chown -R --reference=/opt/webgis/app/geoserver-data /opt/webgis/app/geoserver-dataTunggu sekitar satu menit supaya GeoServer selesai boot, lalu buktikan layernya terbaca
kembali lewat WFS memakai perintah pada bagian 21. Kalau jawabannya memuat FeatureCollection,
pemulihannya berhasil.
Cadangkan berkas .env
Section titled “Cadangkan berkas .env”Berkas ini tidak ikut ke bucket karena memuat kata sandi dan kunci rahasia. Salin manual:
gcloud compute scp \ --zone="$ZONE" \ --tunnel-through-iap \ "$VM_NAME:/opt/webgis/app/.env" "env-$PARTICIPANT_ID.txt"Simpan berkas itu di tempat yang aman, misalnya pengelola kata sandi. Jangan memasukkannya ke Git, dan jangan menempelkan isinya ke laporan atau tangkapan layar.
26. Pantau sistem dan container
Section titled “26. Pantau sistem dan container”Bagian ini memeriksa apakah deployment masih sehat setelah dipakai beberapa waktu.
Periksa dari dalam VM
Section titled “Periksa dari dalam VM”gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && sudo docker compose ps && sudo docker stats --no-stream && df -h / && free -h'| Yang dibaca | Nilai sehat | Tindakan bila menyimpang |
|---|---|---|
docker compose ps |
tiga container berstatus running |
lihat bagian 29 |
| Memori GeoServer | jauh di bawah batas 2048 MiB, acuan terukur 882 MiB | turunkan MAXIMUM_MEMORY seperti pada bagian 29 |
| Memori tersedia | di atas 300 MB | turunkan heap, atau hentikan container yang tidak dipakai |
Pemakaian disk / |
di bawah 80% | hapus arsip lama, lalu periksa ukuran geoserver-data |
OOMKilled ketiga container |
false |
cari pemakai memori yang sebenarnya sebelum mengubah konfigurasi |
Periksa dari luar VM
Section titled “Periksa dari luar VM”EXTERNAL_IP="$(gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format='get(networkInterfaces[0].accessConfigs[0].natIP)')"
curl -sS -o /dev/null -w "portal %{http_code}\n" "http://${EXTERNAL_IP}/portal"curl -sS -o /dev/null -w "geoserver %{http_code}\n" "http://${EXTERNAL_IP}/geoserver/web"Hasil yang benar adalah 200 untuk portal dan 302 untuk GeoServer.
Membaca log
Section titled “Membaca log”gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && sudo docker compose logs --tail=50 geoserver'Setelah VM dinyalakan kembali
Section titled “Setelah VM dinyalakan kembali”Ketiga service memakai restart: unless-stopped, jadi container menyala sendiri saat VM
boot. GeoServer tetap butuh waktu sekitar 47 detik sebelum siap melayani. Jangan menyimpulkan
ada kerusakan sebelum Server startup in [...] milliseconds muncul di log.
Pantau otomatis dengan uptime check
Section titled “Pantau otomatis dengan uptime check”Jalankan perintah di Cloud Shell pada akun dan project tujuan, bukan terminal lokal atau shell di dalam VM. Untuk project bersama, pilih hanya VM, check, dan policy milik sendiri. Jangan menghapus seluruh resource monitoring project untuk mengulang pengujian.
Isi variabel berikut sesuai deployment dan email yang benar-benar dibuka:
export PROJECT_ID="geoportal-kelompok-a-xxxxx"export VM_NAME="webgis-nama01"export ZONE="asia-southeast2-b"export DOMAIN="ujissl.nama01.it.com"export ALERT_EMAIL="ALAMAT_EMAIL_PENERIMA"
gcloud monitoring uptime list-configs --project="$PROJECT_ID" \ --format='table(displayName,name,period)'gcloud alpha monitoring channels list --project="$PROJECT_ID" \ --format='json(name,displayName,type,labels.email_address,enabled)'Jika check untuk domain tersebut sudah ada, gunakan ID-nya dan ubah interval menjadi satu menit:
export CHECK_ID="uji-portal-sTmhxgyyrQk"gcloud monitoring uptime update "$CHECK_ID" --project="$PROJECT_ID" --period=1Jika belum ada, buat sekali, lalu salin segmen terakhir name hasilnya ke CHECK_ID:
gcloud monitoring uptime create "portal-${VM_NAME}" \ --project="$PROJECT_ID" \ --resource-type=uptime-url \ --resource-labels="host=${DOMAIN},project_id=${PROJECT_ID}" \ --protocol=https --path=/portal --port=443 \ --period=1 --timeout=10 --validate-ssl=true \ --regions=asia-pacific,europe,usa-iowa--period=1 berarti satu menit, bukan satu detik. HTTPS memakai hostname agar TLS/SNI
sesuai sertifikat. Pemeriksaan HTTP sukses tidak membuktikan isi halaman atau seluruh
backend sehat. Next.js yang mati bisa menghasilkan 502 melalui Nginx dan terdeteksi;
GeoServer atau database yang mati belum tentu mengubah respons /portal.
Buat atau koreksi alert policy
Section titled “Buat atau koreksi alert policy”Gunakan channel email yang sudah ada dengan mengisi CHANNEL memakai nama resource lengkap.
Jika belum ada, buat sekali:
CHANNEL="$(gcloud alpha monitoring channels create \ --project="$PROJECT_ID" --display-name="Email asisten" --type=email \ --channel-labels="email_address=${ALERT_EMAIL}" --format='value(name)')"Pastikan channel aktif, alamat email benar, dan selesaikan verifikasi jika diminta Console.
Gunakan konfigurasi berikut untuk alert akses portal:
| Pengaturan | Nilai |
|---|---|
| Metric | monitoring.googleapis.com/uptime_check/check_passed |
| Resource dan filter | uptime_url, check ID milik portal tujuan |
| Alignment | 60s, ALIGN_NEXT_OLDER |
| Reducer | REDUCE_COUNT_FALSE |
| Kondisi | Above threshold, COMPARISON_GT, threshold 1 |
| Trigger | Any time series, count: 1 |
| Retest | No retest, duration: 0s untuk pengujian |
| Status | Enabled |
| Notification | Email saat open dan closure |
REDUCE_COUNT_FALSE menghitung time series lokasi yang gagal setelah alignment. Pada tiga
lokasi, sehat semua = 0, gagal semua = 3. > 1 berarti minimal dua lokasi gagal.
Di Monitoring → Alerting, edit policy milik sendiri atau buat melalui uptime check. Isi tabel di atas, pilih channel email, aktifkan notifikasi pembukaan dan penutupan, lalu simpan dan Enable. Tetapkan alignment 60s dan notifikasi open dan closure secara eksplisit sebelum menyimpan.
Alternatif untuk membuat policy baru melalui Cloud Shell (jangan jalankan jika sedang memperbaiki policy yang sudah ada):
POLICY_NAME="$(gcloud alpha monitoring policies create \ --project="$PROJECT_ID" --display-name="Portal ${VM_NAME} tidak tersedia" \ --notification-channels="$CHANNEL" \ --condition-display-name="Minimal dua lokasi gagal" \ --condition-filter="metric.type=\"monitoring.googleapis.com/uptime_check/check_passed\" AND resource.type=\"uptime_url\" AND metric.labels.check_id=\"${CHECK_ID}\"" \ --aggregation='{"alignmentPeriod":"60s","perSeriesAligner":"ALIGN_NEXT_OLDER","crossSeriesReducer":"REDUCE_COUNT_FALSE"}' \ --if='> 1' --trigger-count=1 --duration=0s --combiner=OR \ --format='value(name)')"Sesudah membuat lewat CLI, buka policy di Console dan pastikan Notify on alert open and closure. Setelah konfigurasi diperiksa, lakukan pengujian di bawah.
Verifikasi konfigurasi yang tersimpan; salin POLICY_NAME dari daftar jika memakai Console:
gcloud alpha monitoring policies list --project="$PROJECT_ID" \ --format='table(name,displayName,enabled)'gcloud alpha monitoring policies describe "$POLICY_NAME" --project="$PROJECT_ID" \ --format='json(name,enabled,conditions,notificationChannels,alertStrategy)'Pastikan COMPARISON_GT, threshold 1, trigger count 1, duration 0s, dan enabled true.
Nonaktifkan hanya policy percobaan milik sendiri yang tidak dipakai. Beri jeda setidaknya
lima menit setelah perubahan, lalu pastikan ketiga lokasi hijau dan URL membalas 200.
Jeda ini adalah langkah operasional, bukan jaminan waktu propagasi Google.
Uji layanan mati sampai email pemulihan
Section titled “Uji layanan mati sampai email pemulihan”Siapkan perintah pemulihan sebelum mulai. Stop Nginx saja, bukan VM, agar SSH tetap tersedia. Bedanya dijelaskan pada kotak di bagian 30. Catat jam stop dan pastikan URL yang sama dengan uptime check benar-benar gagal:
gcloud compute ssh "$VM_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ --tunnel-through-iap \ --command='sudo docker stop nginx_proxy && date -u "+NGINX STOP: %Y-%m-%d %H:%M:%S UTC"'curl --silent --show-error --output /dev/null --write-out 'HTTP %{http_code}\n' \ --max-time 15 "https://${DOMAIN}/portal"Exited dan gagal koneksi dengan HTTP 000 adalah hasil yang diharapkan. Jika masih 200,
periksa target URL sebelum menyalahkan policy. Pantau Last 1 hour, lalu periksa lokasi
uptime check, incident baru setelah waktu stop, dan email Alert firing. Cocokkan
nama policy dan waktu incident pada email dengan pengujian ini.
Setelah email masuk, atau maksimal sepuluh menit jika belum masuk, pulihkan:
gcloud compute ssh "$VM_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ --tunnel-through-iap \ --command='sudo docker start nginx_proxy && date -u "+NGINX START: %Y-%m-%d %H:%M:%S UTC"'curl --silent --show-error --output /dev/null --write-out 'HTTP %{http_code}\n' \ --max-time 20 "https://${DOMAIN}/portal"docker start menjalankan kembali container beserta prosesnya; tidak perlu membuat ulang
container untuk pemulihan biasa. Tunggu HTTP 200, lokasi hijau, incident yang sama otomatis
Closed, dan email Alert recovered. Jangan tutup manual incident yang sedang diuji.
Durasi di email adalah durasi incident, bukan latensi email atau durasi downtime persis.
Catat waktu stop, hasil HTTP, waktu incident terbuka, waktu start, dan email pemulihan pada workbook, lalu simpan hasil di salinan per asisten.
Untuk mengurangi alarm akibat gangguan sesaat, retest dapat diubah menjadi dua menit setelah pengujian awal selesai. Ulangi uji stop dan pemulihan setelah mengubah konfigurasi.
Bila hasil tidak sesuai
Section titled “Bila hasil tidak sesuai”- Pastikan project, check ID, policy ID, dan channel cocok; nama saja tidak cukup.
- Tampilkan Show closed alerts dan pilih rentang waktu pengujian.
- Acknowledged tidak sama dengan Closed. Jika penutupan ditolak karena active conditions, simpan bukti dan jangan membuat gangguan hanya untuk memaksa penutupan.
- Tutup dulu incident yang sedang terbuka sebelum mengubah condition policy. Ini bukan
saran gaya kerja, melainkan menghindari bug Cloud Monitoring yang diakui Google pada
issue tracker
183505672: incident yang terbuka saat condition diubah akan terus berbunyi memakai konfigurasi lama, sehingga muncul sebagai false positive dan tidak bisa ditutup manual. Kalau itu terjadi, incident akan menutup sendiri setelah 7 hari. Akibat praktisnya, incident lama yang menggantung jangan dipakai sebagai bukti apa pun tentang konfigurasi baru. Buat kondisi bersih lebih dahulu, baru nilai hasilnya. - Temuan dan koreksi terpusat di troubleshooting.
Uptime check dibuat per endpoint peserta; satu project kelompok bisa memiliki beberapa check dan policy. Channel email boleh dipakai bersama sesuai penerima yang dikehendaki. Untuk interval satu menit dan tiga lokasi, perkiraan jadwal pemeriksaan adalah 129.600 per 30 hari per endpoint. Periksa kuota dan harga Monitoring sebelum memperbanyak check; angka ini bukan bukti jumlah eksekusi yang ditagihkan.
Rujukan: policy uptime, contoh JSON, dan troubleshooting Google.
27. Daftarkan ke mesin pencari
Section titled “27. Daftarkan ke mesin pencari”Bagian ini membuat geoportal dapat ditemukan mesin pencari. Ada tiga hal yang dikerjakan, dan jenisnya berbeda-beda. Memisahkannya penting supaya tidak ada yang tertukar.
| Jenis | Yang dikerjakan | Di mana | Kapan |
|---|---|---|---|
| Berkas baru | src/app/robots.js dan src/app/sitemap.js |
Fork, ikut terbawa ke dalam image | Sekali, lalu build ulang |
| Konfigurasi | Dua blok location pada nginx.conf |
Fork, lalu disalin ke VM | Sekali, sebelum verifikasi |
| Pendaftaran | Properti Domain di Google Search Console | Peramban, di luar repo | Terakhir, setelah keduanya jalan |
Bagian ini dikerjakan paling akhir, setelah HTTPS pada bagian 24 selesai. Alasannya: mesin pencari menuntut alamat yang tetap, dan alamat itu baru ada setelah domain mengarah ke VM. Pendaftaran ke mesin pencari hanya masuk akal bila alamatnya sudah berupa domain, bukan alamat IP.
Pastikan akses publik lebih dahulu
Section titled “Pastikan akses publik lebih dahulu”Periksa dari luar VM bahwa halaman portal terbuka tanpa login:
curl -sS -o /dev/null -w "portal %{http_code}\n" "http://DOMAIN/portal"curl -sS -o /dev/null -w "internal %{http_code}\n" "http://DOMAIN/portal/internal"Halaman portal harus membalas 200, sedangkan /portal/internal memang meminta login.
Ganti DOMAIN dengan domain yang didaftarkan pada bagian 24.
Tambahkan robots.txt dan sitemap.xml
Section titled “Tambahkan robots.txt dan sitemap.xml”Next.js menyediakan kedua berkas itu lewat konvensi App Router. Buat dua berkas berikut di fork:
export default function robots() { return { rules: { userAgent: '*', allow: '/', disallow: ['/portal/internal/', '/portal/api/'] }, sitemap: 'https://DOMAIN/portal/sitemap.xml' };}export default function sitemap() { const base = 'https://DOMAIN/portal';
return [ { url: base + '/', priority: 1 }, { url: base + '/peta-latihan-1', priority: 0.8 } ];}Ganti DOMAIN dengan domain sendiri, lalu commit dan push supaya Cloud Build membangun ulang.
Kedua berkas ini ikut masuk ke dalam image, jadi tidak perlu disalin manual ke VM.
Verifikasi sebelum mendaftar
Section titled “Verifikasi sebelum mendaftar”Jangan mendaftar ke Search Console sebelum tiga pemeriksaan ini benar. Kalau salah satunya gagal, pendaftarannya ditolak dengan pesan yang tidak menyebut penyebabnya.
curl -sS "https://DOMAIN/robots.txt"curl -sS "https://DOMAIN/sitemap.xml"curl -sS -o /dev/null -w "internal %{http_code}\n" "https://DOMAIN/portal/internal"| Perintah | Hasil yang benar |
|---|---|
robots.txt |
Memuat baris Disallow: /portal/internal/ dan baris Sitemap: |
sitemap.xml |
Berisi XML dengan minimal satu alamat halaman publik |
/portal/internal |
Bukan 200, karena halaman admin memang meminta login |
Dua perintah pertama adalah yang paling sering gagal, dan penyebabnya selalu sama: blok
location pada langkah sebelumnya belum ikut tersalin ke VM.
Daftarkan ke Google Search Console
Section titled “Daftarkan ke Google Search Console”-
Buka https://search.google.com/search-console dan masuk memakai akun pemilik domain.
-
Pilih Add property, lalu pilih jenis Domain dan isi domain tanpa
http://. -
Verifikasi kepemilikan lewat record TXT pada Cloud DNS. Salin nilai TXT yang diberikan Google, lalu tambahkan pada zone yang dibuat di bagian 24:
Cloud Shell asisten gcloud dns record-sets create "DOMAIN." \--zone="$DNS_ZONE_NAME" \--type="TXT" \--ttl="300" \--rrdatas='"nilai-verifikasi-dari-google"' -
Tunggu propagasi record, lalu tekan Verify.
-
Setelah terverifikasi, kirim sitemap lewat menu Sitemaps dengan isi
sitemap.xml.
Verifikasi juga bisa memakai berkas HTML, tetapi cara itu menuntut berkas diletakkan di akar domain, sedangkan akar domain pada deployment ini dipakai Nginx. Record TXT lebih sederhana.
28. Sinkronkan file konfigurasi VM bila berubah
Section titled “28. Sinkronkan file konfigurasi VM bila berubah”Cloud Build membangun image aplikasi, tetapi docker-compose.yml, .env, dan
nginx.conf berada pada clone repository di VM. Perubahan file tersebut tidak otomatis
masuk ke VM hanya karena image baru berhasil dibangun.
Jika file konfigurasi repository berubah setelah clone awal, sinkronkan dan buat ulang container Nginx:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && git pull --ff-only && sudo docker compose up -d --force-recreate nginx && sudo docker compose exec -T nginx nginx -t'Direktori yang dipasang, seperti tls/ dan certbot-webroot/, tidak mengalami masalah ini
karena berkas baru di dalam direktori langsung terlihat tanpa membuat ulang container.
Jangan menjalankan git pull jika clone di VM memiliki perubahan lokal yang belum
disimpan. Periksa dahulu dengan git status --short.
29. Troubleshooting berdasarkan hasil pengujian
Section titled “29. Troubleshooting berdasarkan hasil pengujian”IAM meminta kondisi
Section titled “IAM meminta kondisi”Gejala:
The policy contains bindings with conditions, so specifying a condition is requiredPenyebabnya adalah project telah memiliki binding bersyarat, biasanya dari koneksi Cloud
Build. Tambahkan --condition=None pada binding baru. Ini menyatakan bahwa role baru tidak
memakai kondisi.
Trigger menampilkan could not parse resource []
Section titled “Trigger menampilkan could not parse resource []”Penyebabnya adalah _VM_NAME atau _VM_ZONE kosong. Buka konfigurasi trigger dan isi
empat substitution variable pada bagian 16. Error ini bukan masalah firewall atau SSH.
SSH menampilkan Permission denied (publickey)
Section titled “SSH menampilkan Permission denied (publickey)”Cloud Build sudah mencapai IAP, tetapi Service Account deployment belum boleh bertindak pada Service Account VM. Jalankan kembali binding berikut:
PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" \ --format='value(projectNumber)')"VM_SA="${PROJECT_NUMBER}-asisten01@example.com"
gcloud iam service-accounts add-iam-policy-binding "$VM_SA" \ --member="serviceAccount:$BUILD_SA" \ --role="roles/iam.serviceAccountUser" \ --condition=NoneArtifact Registry menampilkan Unauthenticated request
Section titled “Artifact Registry menampilkan Unauthenticated request”Pastikan Service Account VM memiliki roles/artifactregistry.reader:
gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:$VM_SA" \ --role="roles/artifactregistry.reader" \ --condition=NoneDeployment yang diuji memakai access token singkat dari identitas VM. Google merekomendasikan access token atau credential helper dibanding Service Account key JSON. Lihat autentikasi Docker untuk Artifact Registry.
Jika login berhasil tetapi pull masih unauthenticated, periksa pengguna yang menjalankan
Docker. Perintah sudo docker membaca /root/.docker/config.json, bukan konfigurasi
Docker milik user biasa. cloudbuild.yaml final memakai sudo gcloud auth print-access-token, sudo docker login, dan sudo -H docker compose agar kredensial
dibaca oleh user yang sama.
NEXTJS_IMAGE belum memiliki nilai
Section titled “NEXTJS_IMAGE belum memiliki nilai”Gejala:
required variable NEXTJS_IMAGE is missing a valueTrigger belum berhasil menyimpan tag image ke /opt/webgis/app/.env. Jalankan trigger
sampai seluruh step hijau. Setelah itu, periksa nilai tanpa menampilkan isi secret lain:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command="sudo grep '^NEXTJS_IMAGE=' /opt/webgis/app/.env"Browser menampilkan ERR_TOO_MANY_REDIRECTS
Section titled “Browser menampilkan ERR_TOO_MANY_REDIRECTS”Masalah ini berasal dari perbedaan URL kanonik antara Nginx dan Next.js, bukan dari
browser. Konfigurasi final repository memakai basePath: "/portal" dan
tanpa trailingSlash, sedangkan Nginx memproksi seluruh path melalui location /.
Pastikan VM memakai commit terbaru dan muat ulang Nginx dengan perintah pada bagian 28.
Kemudian akses http://IP_EKSTERNAL_VM/portal. Menghapus cookie hanya diperlukan jika
browser masih menyimpan redirect lama setelah konfigurasi server dipastikan benar.
Container GeoServer mati sendiri atau keluar dengan kode 137
Section titled “Container GeoServer mati sendiri atau keluar dengan kode 137”Gejala:
geoserver_app Exited (137)Periksa apakah container dihentikan oleh kernel karena kehabisan memori:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='sudo docker inspect --format "{{.State.OOMKilled}}" geoserver_app && free -h'Nilai true berarti kernel menghentikan container karena memori habis. Pada pengukuran
11 September 2026 hal itu tidak terjadi: dengan e2-medium dan MAXIMUM_MEMORY=2048m,
GeoServer memakai 882 MiB, OOMKilled bernilai false, dan 2,3 GiB memori masih tersedia.
Kalau nilai true justru muncul di VM-mu, periksa free -h dan docker stats --no-stream
lebih dahulu untuk mencari pemakai memori yang sebenarnya, lalu turunkan heap kalau GeoServer
memang penyebabnya:
environment: - INITIAL_MEMORY=256m - MAXIMUM_MEMORY=1024mMuat ulang container setelah berkas diubah:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && sudo docker compose up -d geoserver && sudo docker compose ps'Catat nilai free -h sebelum dan sesudah perubahan pada
workbook asisten trainer. Data ini yang menentukan apakah
e2-medium tetap layak dipakai untuk pelatihan atau perlu dinaikkan ke e2-standard-2.
Pengukuran pertama menjawabnya sementara ini dengan e2-medium yang masih lega.
Halaman /geoserver/web menampilkan 502 atau 404
Section titled “Halaman /geoserver/web menampilkan 502 atau 404”Nginx container belum memuat rute GeoServer. Rute tersebut berasal dari nginx.conf pada
repository fork. Pastikan commit berisi blok location /geoserver/, lalu sinkronkan berkas
konfigurasi ke VM dan muat ulang Nginx seperti pada bagian 28.
Jika Nginx sudah benar tetapi tetap 502, periksa container tujuan:
gcloud compute ssh "$VM_NAME" \ --zone="$ZONE" \ --tunnel-through-iap \ --command='cd /opt/webgis/app && sudo docker compose ps && sudo docker compose logs --tail=30 geoserver'Kata sandi admin GeoServer kembali ke nilai bawaan setelah restart
Section titled “Kata sandi admin GeoServer kembali ke nilai bawaan setelah restart”Kata sandi yang diubah lewat antarmuka web tidak bertahan karena image membaca
GEOSERVER_ADMIN_PASSWORD setiap kali container dijalankan. Isi variabel tersebut pada
docker-compose.yml, lalu jalankan sudo docker compose up -d geoserver.
Alert policy ditolak dengan groupByFields[0] had an invalid value
Section titled “Alert policy ditolak dengan groupByFields[0] had an invalid value”Gejalanya:
INVALID_ARGUMENT: Field aggregation.groupByFields[0] had an invalid value of"project_id": The group-by field is improperly formatted.Antarmuka Cloud Monitoring menampilkan nama field dengan gaya project_id dan
resource.label.host, sedangkan API menuntut project dan resource.labels.host. Bila kamu
memang perlu mengelompokkan hasil, pakai bentuk yang kedua. Untuk satu host, cara paling
sederhana adalah membuang groupByFields sama sekali seperti pada bagian 26.
30. Hentikan VM setelah selesai
Section titled “30. Hentikan VM setelah selesai”Jalankan dari Cloud Shell agar biaya compute berhenti saat VM tidak digunakan:
gcloud compute instances stop "$VM_NAME" \ --zone="$ZONE"Nyalakan kembali saat melanjutkan latihan:
gcloud compute instances start "$VM_NAME" \ --zone="$ZONE"Setelah VM menyala kembali, alamat IP eksternal dapat berubah jika alamat tersebut belum
direservasi. Ambil IP terbaru dan sesuaikan NEXTAUTH_URL bila diperlukan.
Bila VM akan dimatikan lama, bukan sekadar diistirahatkan semalam, hentikan juga pemantauan otomatisnya lebih dahulu. Alasannya, cara melakukannya, dan urutan pembersihan yang lengkap ada pada bagian 31.
31. Bersihkan semua resource setelah selesai
Section titled “31. Bersihkan semua resource setelah selesai”Bagian ini dipakai pada akhir pelatihan, atau kapan pun kamu ingin berhenti memakai project tanpa meninggalkan tagihan yang berjalan diam-diam.
Yang tetap menagih walaupun VM dimatikan
Section titled “Yang tetap menagih walaupun VM dimatikan”Ini bagian yang paling sering membuat kaget, karena VM yang berhenti bukan berarti tagihannya berhenti.
| Resource | Saat VM berjalan | Saat VM berhenti |
|---|---|---|
Compute e2-medium |
ditagih per detik | tidak ditagih |
Boot disk 30 GB pd-balanced |
sekitar $0,04 per GB per bulan | tetap ditagih |
| Static IP yang menempel | $0,005 per jam | berubah menjadi $0,01 per jam |
| IP statis yang sudah dicopot | tidak ditagih | tidak ditagih |
| Bucket Cloud Storage | ditagih per GB | tetap ditagih, tetapi arsip 60 KB praktis nol |
| Artifact Registry | sekitar $0,10 per GB per bulan | tetap ditagih |
Baris ketiga yang paling penting. Alamat IP statis yang menempel pada VM berstatus IN_USE
dengan tarif $0,005 per jam. Begitu VM dihentikan, statusnya berubah menjadi menganggur dan
tarifnya naik dua kali menjadi $0,01 per jam. Jadi menghentikan VM tanpa mencopot IP statisnya
menghemat compute tetapi menambah tagihan IP.
Hitungan nyata pada 13 September 2026 untuk empat VM e2-medium dan tiga IP statis:
| Pos | Semua VM berjalan | Semua VM berhenti, IP dicopot |
|---|---|---|
| Compute | sekitar $98 per bulan | $0 |
| Disk 4 x 30 GB | sekitar $5 per bulan | sekitar $5 per bulan |
| IP statis 3 buah | sekitar $11 per bulan | $0 |
| Total | sekitar $114 per bulan | sekitar $5 per bulan |
Kalau hanya ingin berhenti sementara
Section titled “Kalau hanya ingin berhenti sementara”Copot IP statisnya supaya tidak menagih selama menganggur:
gcloud compute instances stop "$VM_NAME" --zone="$ZONE"
gcloud compute addresses delete "NAMA_IP_STATIS" \ --region="$REGION" \ --quietKonsekuensinya harus diterima: alamat IP-nya hilang dan tidak bisa dipesan kembali. Saat VM
dinyalakan lagi, alamatnya akan berbeda, sehingga tiga hal ini harus disesuaikan, yaitu
NEXTAUTH_URL pada .env, record DNS di Cloudflare, dan penerbitan ulang sertifikat Let’s
Encrypt pada bagian 24. Ketiganya sudah pernah dijalankan dan tidak sulit, tetapi jangan
dilupakan.
Kalau alamat yang tetap lebih penting daripada penghematan, biarkan IP statisnya menempel dan terima tarif $0,005 per jam selama VM berjalan. Yang tidak boleh dilakukan adalah menghentikan VM sambil membiarkan IP statisnya menganggur, karena itu keadaan paling mahal dari ketiganya.
Kalau memang sudah selesai total
Section titled “Kalau memang sudah selesai total”Urutannya penting. Pemantauan lebih dahulu, lalu IP, terakhir baru VM dan disk.
# 1. Hentikan pemantauan supaya tidak ada email sampahgcloud alpha monitoring policies list --format="value(name)" | while read -r POLICY; do gcloud alpha monitoring policies delete "$POLICY" --quietdone
gcloud monitoring uptime list-configs --format="value(name)" | while read -r CHECK; do gcloud monitoring uptime delete "$CHECK" --quietdone
gcloud alpha monitoring channels list --format="value(name)" | while read -r CHANNEL; do gcloud alpha monitoring channels delete "$CHANNEL" --quietdone
# 2. Lepaskan IP statis sebelum VM dihapusgcloud compute addresses list --format="value(name)" | while read -r ADDRESS; do gcloud compute addresses delete "$ADDRESS" --region="$REGION" --quietdone
# 3. Hapus VM, lalu disk-nyafor VM in NAMA_VM_1 NAMA_VM_2 NAMA_VM_3 NAMA_VM_4; do gcloud compute instances delete "$VM" --zone="$ZONE" --quietdone
gcloud compute disks list --format="table(name,sizeGb,users)"Verifikasi bahwa pemantauan sudah bersih, dan pastikan ketiganya tidak menampilkan apa pun:
gcloud alpha monitoring policies list --format="value(displayName)"gcloud monitoring uptime list-configs --format="value(displayName)"gcloud alpha monitoring channels list --format="value(displayName)"Menghapus pemantauan tidak memengaruhi uptime check atau policy milik project lain, dan tidak menyentuh VM, disk, maupun bucket. Jadi langkah ini aman dijalankan lebih dahulu, dan justru dianjurkan sebelum VM dimatikan supaya tidak ada email peringatan yang datang selama layanannya sengaja berhenti.
Empat jebakan yang harus dihindari
Section titled “Empat jebakan yang harus dihindari”Nama VM tertukar. Di project ini pernah ada empat VM dengan nama yang mirip, dan menulis
satu nama yang salah berarti satu VM tertinggal dan tetap menagih. Jalankan
gcloud compute instances list lebih dahulu, salin namanya apa adanya, jangan mengandalkan
ingatan.
Menghapus VM tetapi membiarkan disk-nya. Kadang orang sengaja melakukannya untuk menyimpan
data. Kalau memang itu maksudnya, tidak apa-apa, tetapi ketahuilah bahwa disk 30 GB itu tetap
ditagih sekitar $1,2 per bulan per disk. Kalau tidak disengaja, periksa dengan
gcloud compute disks list dan pastikan kolom users kosong untuk disk yang sudah tidak
terpakai.
Menghapus VM lalu tidak memeriksa IP-nya. Setelah VM dihapus, IP statis yang tadinya
menempel bisa tetap berstatus RESERVED. Jalankan ini setelah penghapusan:
gcloud compute addresses list --format="table(name,status,users)"Kalau masih ada baris yang muncul, hapus manual. Kalau tidak diperiksa, alamat menganggur itu menagih $0,01 per jam tanpa kamu sadari, dan itu satu-satunya pos yang benar-benar bisa membocorkan kredit setelah semuanya berhenti.
Bucket terhapus padahal masih berguna. Bucket berisi arsip geoserver-data dan .env, jadi
pertahankan kalau kamu masih mungkin memakai project ini lagi. Ukurannya kecil dan biayanya
praktis nol. Hapus hanya kalau project-nya memang akan ditinggalkan sepenuhnya:
gcloud storage rm --recursive "gs://NAMA_BUCKET"Nyalakan kembali setelah pembersihan
Section titled “Nyalakan kembali setelah pembersihan”Alamat IP akan berbeda, jadi tiga hal ini perlu dikerjakan berurutan:
gcloud compute instances start "$VM_NAME" --zone="$ZONE"
gcloud compute instances describe "$VM_NAME" \ --zone="$ZONE" \ --format='get(networkInterfaces[0].accessConfigs[0].natIP)'Alamat yang muncul dipakai untuk memperbarui NEXTAUTH_URL pada .env, lalu perbarui record A
di Cloudflare, lalu terbitkan ulang sertifikat dengan prosedur pada bagian 24. Ketiga container
menyala sendiri karena memakai restart: unless-stopped, dan GeoServer butuh sekitar 47 detik
sebelum siap melayani.
Verifikasi terakhir
Section titled “Verifikasi terakhir”Jalankan ketiga perintah ini setelah pembersihan. Yang benar adalah ketiganya tidak menampilkan resource apa pun:
gcloud compute instances list --format="table(name,status)"
gcloud compute addresses list --format="table(name,status,users)"
gcloud compute disks list --format="table(name,sizeGb,users)"Terakhir, buka Billing Reports beberapa hari kemudian. Laporan itu tertinggal satu sampai dua hari, jadi tagihan dari resource yang sudah dihapus masih bisa muncul sesaat setelah pembersihan.