<?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: Marvin Purnama</title>
    <description>The latest articles on DEV Community by Marvin Purnama (@marvino).</description>
    <link>https://dev.to/marvino</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%2F4236107%2Ff95ae9f8-5455-4eb7-930f-1699c26a0b38.png</url>
      <title>DEV Community: Marvin Purnama</title>
      <link>https://dev.to/marvino</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marvino"/>
    <language>en</language>
    <item>
      <title>OrigamiPedia: Building a Multi-Tier App with Docker Compose</title>
      <dc:creator>Marvin Purnama</dc:creator>
      <pubDate>Tue, 10 Nov 2026 01:37:26 +0000</pubDate>
      <link>https://dev.to/marvino/origamipedia-building-a-multi-tier-app-with-docker-compose-2kej</link>
      <guid>https://dev.to/marvino/origamipedia-building-a-multi-tier-app-with-docker-compose-2kej</guid>
      <description>&lt;h2&gt;
  
  
  Masalah yang ingin diselesaikan
&lt;/h2&gt;

&lt;p&gt;Tutorial origami biasanya tersebar di video dan blog. Urutannya tidak jelas dan tidak dikelompokkan berdasarkan level, sehingga pemula sering bingung harus mulai dari model yang mana. OrigamiPedia mengumpulkan tutorial dalam satu katalog dengan filter tingkat kesulitan (mudah, sedang, sulit) dan langkah-langkah yang berurutan.&lt;/p&gt;

&lt;p&gt;Selain memecahkan masalah pengorganisasian konten, proyek statis ini sengaja dipilih sebagai sarana ideal untuk melatih perancangan arsitektur aplikasi 3-tier yang diorkestrasi menggunakan Docker Compose. Penguasaan infrastruktur backend yang stabil ini menjadi pondasi krusial yang selaras dengan tujuan karier saya di irisan Cloud Engineering dan Artificial Intelligence, khususnya untuk kebutuhan deployment model ke tahap produksi.&lt;/p&gt;

&lt;p&gt;Proyek ini sengaja dibuat sederhana supaya fokusnya ada pada containerization dan arsitektur tiga tier, bukan pada banyaknya fitur.&lt;/p&gt;

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

&lt;p&gt;Aplikasi berjalan di tiga container dalam satu network buatan bernama &lt;code&gt;origami-net&lt;/code&gt;. Container &lt;code&gt;app&lt;/code&gt; menjalankan Flask dengan Gunicorn dan melayani halaman web sekaligus API di port 8000. Container &lt;code&gt;db&lt;/code&gt; menjalankan PostgreSQL 16. Datanya disimpan di named volume &lt;code&gt;pgdata&lt;/code&gt;, dan port 5432 sengaja tidak dibuka ke localhost sehingga hanya container lain di network yang sama yang bisa mengaksesnya. Container ketiga, Adminer, hanya menyala bila dipanggil dengan profile &lt;code&gt;tools&lt;/code&gt; dan dipakai untuk melihat isi database.&lt;/p&gt;

&lt;p&gt;Alur request-nya: browser mengakses &lt;code&gt;localhost:8000&lt;/code&gt;, diteruskan ke &lt;code&gt;app&lt;/code&gt;, lalu &lt;code&gt;app&lt;/code&gt; memanggil database lewat nama service &lt;code&gt;db&lt;/code&gt;, dan data tersimpan di 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%2F7n019iilckeo6635yoae.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%2F7n019iilckeo6635yoae.png" alt=" " width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile
&lt;/h2&gt;

&lt;p&gt;Dockerfile memakai multi-stage build. Stage pertama (&lt;code&gt;builder&lt;/code&gt;) hanya menginstal dependency, sedangkan stage kedua mengambil hasil instalasi itu sehingga alat build tidak ikut ke image akhir.&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; python:3.12.7-slim AS builder
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /build
&lt;span class="k"&gt;COPY&lt;/span&gt; requirements.txt .
&lt;span class="k"&gt;RUN&lt;/span&gt; pip install --no-cache-dir --prefix=/install -r requirements.txt

&lt;span class="k"&gt;FROM&lt;/span&gt; python:3.12.7-slim
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /app
&lt;span class="k"&gt;COPY&lt;/span&gt; --from=builder /install /usr/local
&lt;span class="k"&gt;COPY&lt;/span&gt; app.py .
&lt;span class="k"&gt;COPY&lt;/span&gt; templates ./templates
&lt;span class="k"&gt;COPY&lt;/span&gt; static ./static
&lt;span class="k"&gt;EXPOSE&lt;/span&gt; 8000
&lt;span class="k"&gt;CMD&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"gunicorn"&lt;/span&gt;, &lt;span class="s2"&gt;"--bind"&lt;/span&gt;, &lt;span class="s2"&gt;"0.0.0.0:8000"&lt;/span&gt;, &lt;span class="s2"&gt;"app:app"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Base image memakai tag versi spesifik, bukan &lt;code&gt;latest&lt;/code&gt;, supaya hasil build selalu sama. File &lt;code&gt;requirements.txt&lt;/code&gt; disalin sebelum source code agar layer instalasi bisa memakai cache; kalau hanya &lt;code&gt;app.py&lt;/code&gt; yang berubah, library tidak diinstal ulang. Saya memakai &lt;code&gt;COPY&lt;/code&gt;, bukan &lt;code&gt;ADD&lt;/code&gt;, karena &lt;code&gt;ADD&lt;/code&gt; punya perilaku tambahan seperti mengunduh dari URL dan mengekstrak file tar. &lt;code&gt;CMD&lt;/code&gt; memakai exec form supaya Gunicorn menjadi proses utama dan menerima sinyal stop dengan benar. Untuk aplikasi sekecil ini selisih ukuran image dari multi-stage tidak besar, tetapi polanya penting untuk aplikasi yang lebih besar.&lt;/p&gt;

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

&lt;p&gt;Berikut bagian pentingnya (disingkat):&lt;br&gt;
&lt;/p&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;app&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;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"${APP_PORT:-8000}:8000"&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;origami-net&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-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;pgdata:/var/lib/postgresql/data&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 ${DB_USER} -d ${DB_NAME}"&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;timeout&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;5&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;origami-net&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Service &lt;code&gt;db&lt;/code&gt; punya healthcheck memakai &lt;code&gt;pg_isready&lt;/code&gt;, dan &lt;code&gt;app&lt;/code&gt; memakai &lt;code&gt;depends_on&lt;/code&gt; dengan &lt;code&gt;condition: service_healthy&lt;/code&gt;, jadi aplikasi baru dijalankan setelah database benar-benar siap. Aplikasi terhubung ke database lewat nama service &lt;code&gt;db&lt;/code&gt;, bukan IP atau localhost. Data database disimpan di named volume &lt;code&gt;pgdata&lt;/code&gt;. Password dan konfigurasi dibaca dari file &lt;code&gt;.env&lt;/code&gt; yang tidak di-commit; yang ada di repository hanya &lt;code&gt;.env.example&lt;/code&gt; berisi data dummy.&lt;/p&gt;

&lt;h2&gt;
  
  
  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/marvin173-digital/Origami-Pedia.git
&lt;span class="nb"&gt;cd&lt;/span&gt; Origami-Pedia
cp .env.example .env
docker compose up -d --build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lalu buka &lt;code&gt;http://localhost:8000&lt;/code&gt;. Untuk melihat database, jalankan &lt;code&gt;docker compose --profile tools up -d&lt;/code&gt; dan buka &lt;code&gt;http://localhost:8081&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hasil
&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%2Fagjw7vhslsqb1xqinw0i.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%2Fagjw7vhslsqb1xqinw0i.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Saya menguji persistensi dengan menambah data, menjalankan &lt;code&gt;docker compose down&lt;/code&gt;, lalu &lt;code&gt;docker compose up -d&lt;/code&gt;. Datanya masih ada karena disimpan di volume &lt;code&gt;pgdata&lt;/code&gt;. Kalau memakai &lt;code&gt;docker compose down -v&lt;/code&gt;, volume ikut terhapus dan database kembali kosong.&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%2Fj8jnp2hh5n36q6vu75so.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%2Fj8jnp2hh5n36q6vu75so.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Port 8080 di laptop saya sudah terpakai, jadi port Adminer saya pindahkan ke 8081.&lt;/li&gt;
&lt;li&gt;Error YAML terjadi karena satu kunci &lt;code&gt;image&lt;/code&gt; tertulis di service yang salah; indentasi dan struktur YAML harus benar-benar teliti.&lt;/li&gt;
&lt;li&gt;Gambar tidak tampil sampai folder &lt;code&gt;static&lt;/code&gt; ikut di-&lt;code&gt;COPY&lt;/code&gt; dan image dibuild ulang. Perubahan file harus diikuti &lt;code&gt;docker compose up -d --build&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Service yang memakai profile tidak ikut dimatikan oleh &lt;code&gt;docker compose down&lt;/code&gt; biasa, jadi perlu &lt;code&gt;--profile tools&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Waktu login Adminer, pilihan System-nya masih MySQL dan kolom Database kosong, jadi muncul error “Connection refused” dan “relation does not exist”. Ternyata harus memilih PostgreSQL, server db, dan mengisi nama database.&lt;/li&gt;
&lt;li&gt;Di PowerShell, curl adalah alias perintah lain, jadi harus memakai curl.exe. Mengirim JSON juga lebih aman lewat file (data.json) daripada ditulis langsung karena masalah tanda kutip.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Repository GitHub: &lt;a href="https://github.com/marvin173-digital/Origami-Pedia" rel="noopener noreferrer"&gt;https://github.com/marvin173-digital/Origami-Pedia&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Video demo: &lt;a href="https://youtu.be/3tUQnIgfwqQ" rel="noopener noreferrer"&gt;https://youtu.be/3tUQnIgfwqQ&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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