<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Valent Steve Asman</title>
    <description>The latest articles on DEV Community by Valent Steve Asman (@valent_steveasman_0111b6).</description>
    <link>https://dev.to/valent_steveasman_0111b6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4236187%2Fe6b1300c-5f10-45bb-8beb-ff4b7c46e939.png</url>
      <title>DEV Community: Valent Steve Asman</title>
      <link>https://dev.to/valent_steveasman_0111b6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/valent_steveasman_0111b6"/>
    <language>en</language>
    <item>
      <title>KerjaLab: Simulasi Kerja Multi-Tier dengan Docker Compose di Localhost</title>
      <dc:creator>Valent Steve Asman</dc:creator>
      <pubDate>Tue, 10 Nov 2026 03:12:44 +0000</pubDate>
      <link>https://dev.to/valent_steveasman_0111b6/kerjalab-simulasi-kerja-multi-tier-dengan-docker-compose-di-localhost-1k2a</link>
      <guid>https://dev.to/valent_steveasman_0111b6/kerjalab-simulasi-kerja-multi-tier-dengan-docker-compose-di-localhost-1k2a</guid>
      <description>&lt;h2&gt;
  
  
  Masalah yang Ingin Diselesaikan
&lt;/h2&gt;

&lt;p&gt;Banyak mahasiswa dan fresh graduate bingung membayangkan seperti apa pekerjaan nyata di bidang yang mereka incar. Teori di kelas tidak selalu sama dengan praktik di dunia kerja. Di sisi lain, perusahaan sulit menilai kemampuan hard skill maupun soft skill kandidat hanya dari CV.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KerjaLab&lt;/strong&gt; adalah aplikasi web simulasi kerja yang menjawab masalah itu. Pengguna memilih bidang (Teknologi, Sains, Bisnis, atau Soft Skill), mengerjakan simulasi dengan batas waktu, lalu mendapat &lt;strong&gt;koin&lt;/strong&gt; untuk kustomisasi profil dan &lt;strong&gt;XP&lt;/strong&gt; untuk menaikkan level skill. Jika gagal, pengguna boleh mencoba lagi &lt;strong&gt;tanpa penalti&lt;/strong&gt;, sehingga KerjaLab menjadi ruang aman untuk mencoba pekerjaan impian sebelum berkomitmen.&lt;/p&gt;

&lt;p&gt;Fitur utamanya:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login Google dengan persetujuan (consent) eksplisit sesuai UU No. 27 Tahun 2022 tentang Pelindungan Data Pribadi.&lt;/li&gt;
&lt;li&gt;12 simulasi dalam 4 bidang dengan 3 tingkat kesulitan. Semakin sulit, semakin besar hadiahnya.&lt;/li&gt;
&lt;li&gt;Tantangan harian dengan streak sebagai bonus.&lt;/li&gt;
&lt;li&gt;Profil yang menampilkan level hard skill dan soft skill, jumlah dan total waktu simulasi, serta link LinkedIn dan GitHub.&lt;/li&gt;
&lt;li&gt;Hak penghapusan data: pengguna dapat menghapus akun dan seluruh datanya.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mengapa Proyek Ini
&lt;/h2&gt;

&lt;p&gt;Saya mengarah ke peran Cloud/DevOps Engineer. Aplikasi ini memberi alasan nyata untuk merancang arsitektur multi-tier: frontend, backend, dan database yang masing-masing berjalan di container, lalu dirakit dengan Docker Compose. Fokus penilaiannya ada pada &lt;em&gt;containerization&lt;/em&gt;, jadi fitur dibuat secukupnya.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arsitektur
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9k4yes97s5n0u9z2jwd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9k4yes97s5n0u9z2jwd.png" alt="Arsitektur Aplikasi" width="799" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tiga tier berjalan pada satu user-defined network bernama &lt;code&gt;kerjalab-net&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;th&gt;Teknologi&lt;/th&gt;
&lt;th&gt;Port ke localhost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;frontend&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backend&lt;/td&gt;
&lt;td&gt;Node.js (Express)&lt;/td&gt;
&lt;td&gt;4000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db&lt;/td&gt;
&lt;td&gt;PostgreSQL 16&lt;/td&gt;
&lt;td&gt;tidak dibuka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;adminer (profile &lt;code&gt;tools&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Adminer&lt;/td&gt;
&lt;td&gt;8080&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Alur request: browser mengakses &lt;code&gt;http://localhost:3000&lt;/code&gt;, lalu Next.js meneruskan panggilan &lt;code&gt;/api/*&lt;/code&gt; ke &lt;code&gt;http://backend:4000&lt;/code&gt;. Backend terhubung ke database lewat &lt;code&gt;db:5432&lt;/code&gt;. Data disimpan di named volume &lt;code&gt;kerjalab_pgdata&lt;/code&gt;. Database tidak dipublikasikan ke localhost, sehingga hanya container di dalam network yang bisa mengaksesnya.&lt;/p&gt;

&lt;p&gt;Container dipilih dibanding virtual machine karena berbagi kernel host. Isolasinya memakai &lt;em&gt;namespaces&lt;/em&gt; dan pembatasan sumber daya memakai &lt;em&gt;cgroups&lt;/em&gt;, sehingga image jauh lebih kecil dan startup hanya hitungan detik, bukan menit seperti VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile: Multi-Stage Build
&lt;/h2&gt;

&lt;p&gt;Backend memakai dua stage. Stage &lt;code&gt;builder&lt;/code&gt; memasang dependency, sedangkan stage &lt;code&gt;runtime&lt;/code&gt; hanya membawa hasil yang diperlukan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt; node:22.14.0-alpine3.21 AS builder
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /app
&lt;span class="k"&gt;COPY&lt;/span&gt; package.json package-lock.json ./
&lt;span class="k"&gt;RUN&lt;/span&gt; npm ci --omit=dev
&lt;span class="k"&gt;COPY&lt;/span&gt; src ./src

&lt;span class="k"&gt;FROM&lt;/span&gt; node:22.14.0-alpine3.21 AS runtime
&lt;span class="k"&gt;ENV&lt;/span&gt; &lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /app
&lt;span class="k"&gt;COPY&lt;/span&gt; --from=builder --chown=node:node /app ./
&lt;span class="k"&gt;USER&lt;/span&gt; node
&lt;span class="k"&gt;EXPOSE&lt;/span&gt; 4000
&lt;span class="k"&gt;CMD&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;, &lt;span class="s2"&gt;"src/index.js"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Penjelasan singkat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tag versi spesifik&lt;/strong&gt; (&lt;code&gt;22.14.0-alpine3.21&lt;/code&gt;), bukan &lt;code&gt;latest&lt;/code&gt;, agar build dapat diulang dengan hasil yang sama.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File dependency disalin lebih dulu&lt;/strong&gt; sebelum source code. Selama &lt;code&gt;package.json&lt;/code&gt; tidak berubah, layer &lt;code&gt;npm ci&lt;/code&gt; diambil dari cache sehingga build jauh lebih cepat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage&lt;/strong&gt; membuat image akhir tidak membawa cache npm dan tool build. Hanya &lt;code&gt;node_modules&lt;/code&gt; produksi dan &lt;code&gt;src&lt;/code&gt; yang ikut terbawa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;COPY&lt;/code&gt;, bukan &lt;code&gt;ADD&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;COPY&lt;/code&gt; hanya menyalin file. &lt;code&gt;ADD&lt;/code&gt; punya fungsi tambahan (ekstraksi tar dan unduh dari URL) yang membuat perilakunya kurang transparan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CMD&lt;/code&gt; dalam exec form&lt;/strong&gt; membuat proses &lt;code&gt;node&lt;/code&gt; menjadi PID 1 dan menerima sinyal penghentian langsung dari Docker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.dockerignore&lt;/code&gt;&lt;/strong&gt; mencegah &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, dan &lt;code&gt;.git&lt;/code&gt; ikut masuk ke konteks build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frontend memakai tiga stage (&lt;code&gt;deps&lt;/code&gt;, &lt;code&gt;builder&lt;/code&gt;, &lt;code&gt;runtime&lt;/code&gt;) dengan mode &lt;code&gt;standalone&lt;/code&gt; bawaan Next.js, sehingga image runtime hanya berisi server minimal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Compose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16.6-alpine&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;kerjalab_pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;kerjalab-net&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CMD-SHELL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;kerjalab-net&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;adminer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;adminer:4.8.1&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Poin pentingnya:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend memakai &lt;code&gt;DB_HOST: db&lt;/code&gt;, yaitu &lt;strong&gt;nama service&lt;/strong&gt;, bukan IP atau &lt;code&gt;localhost&lt;/code&gt;. DNS internal Docker yang menerjemahkannya.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;healthcheck&lt;/code&gt; pada database dan &lt;code&gt;depends_on&lt;/code&gt; dengan &lt;code&gt;condition: service_healthy&lt;/code&gt; memastikan backend baru berjalan setelah PostgreSQL benar-benar siap menerima koneksi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named volume&lt;/strong&gt; menjaga data tetap ada setelah &lt;code&gt;docker compose down&lt;/code&gt;. Hanya &lt;code&gt;docker compose down -v&lt;/code&gt; yang ikut menghapus volume.&lt;/li&gt;
&lt;li&gt;Konfigurasi dibaca dari file &lt;code&gt;.env&lt;/code&gt;. Yang masuk repository hanya &lt;code&gt;.env.example&lt;/code&gt; berisi nilai dummy, sehingga tidak ada secret yang ter-commit.&lt;/li&gt;
&lt;li&gt;Service &lt;code&gt;adminer&lt;/code&gt; berada di &lt;strong&gt;profile &lt;code&gt;tools&lt;/code&gt;&lt;/strong&gt;, jadi hanya aktif saat dibutuhkan dengan &lt;code&gt;docker compose --profile tools up -d&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cara Menjalankan di Localhost
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/valentntnt/KerjaLab.git
&lt;span class="nb"&gt;cd&lt;/span&gt; KerjaLab
cp .env.example .env
docker compose up -d --build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buka &lt;code&gt;http://localhost:3000&lt;/code&gt;. Untuk mengecek kesehatan service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
curl -i http://localhost:4000/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek6mj8oo678908sv3jbc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek6mj8oo678908sv3jbc.png" alt="Uji docker compose ps" width="797" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Untuk menguji persistensi, buat data lalu jalankan &lt;code&gt;docker compose down&lt;/code&gt; dan &lt;code&gt;docker compose up -d&lt;/code&gt;. Data masih ada karena tersimpan di named volume.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7qnyfwuxyod5oipr092.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7qnyfwuxyod5oipr092.png" alt="Tampilan Aplikasi" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pelajaran yang Didapat
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Urutan perintah Dockerfile memengaruhi kecepatan build.&lt;/strong&gt; Menaruh instruksi yang jarang berubah di atas membuat layer cache terpakai maksimal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage build adalah cara paling sederhana menjaga image tetap ramping&lt;/strong&gt; tanpa mengorbankan kemudahan proses build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;depends_on&lt;/code&gt; saja tidak cukup.&lt;/strong&gt; Container yang sudah start belum tentu siap. Healthcheck dengan &lt;code&gt;service_healthy&lt;/code&gt; menyelesaikan masalah itu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pemisahan konfigurasi dari kode&lt;/strong&gt; lewat &lt;code&gt;.env&lt;/code&gt; dan &lt;code&gt;.env.example&lt;/code&gt; menjaga secret tetap aman sekaligus memudahkan orang lain menjalankan proyek.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container memudahkan perpindahan ke cloud.&lt;/strong&gt; Karena aplikasi sudah dikemas bersama dependensinya, memindahkannya ke VM cloud kelak cukup dengan menginstal Docker lalu menjalankan &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tautan
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository GitHub: &lt;a href="https://github.com/valentntnt/KerjaLab" rel="noopener noreferrer"&gt;https://github.com/valentntnt/KerjaLab&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Video demo YouTube: GANTI_DENGAN_LINK_YOUTUBE&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>devops</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
