<?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: reerii code</title>
    <description>The latest articles on DEV Community by reerii code (@reeriicode).</description>
    <link>https://dev.to/reeriicode</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%2F4147428%2F524b6ead-f058-4241-acbe-b1c27dc059f2.png</url>
      <title>DEV Community: reerii code</title>
      <link>https://dev.to/reeriicode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reeriicode"/>
    <language>en</language>
    <item>
      <title>Docker ฉบับเข้าใจง่าย: Image, Container, Volume, Network และ Docker Compose</title>
      <dc:creator>reerii code</dc:creator>
      <pubDate>Wed, 04 Nov 2026 10:33:50 +0000</pubDate>
      <link>https://dev.to/reeriicode/docker-chbabekhaaaicchngaay-image-container-volume-network-aela-docker-compose-37g2</link>
      <guid>https://dev.to/reeriicode/docker-chbabekhaaaicchngaay-image-container-volume-network-aela-docker-compose-37g2</guid>
      <description>&lt;p&gt;เคยเจอไหม เขียนโปรเจกต์บน Windows มาอย่างดี รันในเครื่องตัวเองผ่านหมด แต่พอเอาไป deploy บนเซิร์ฟเวอร์ Linux ดันรันไม่ขึ้น เพราะ library บางตัวทำงานต่างกันในคนละสภาพแวดล้อม&lt;/p&gt;

&lt;p&gt;หรือบางที ส่งโปรเจกต์ให้เพื่อนไปรัน เพื่อนก็รันไม่ได้อีก เพราะเวอร์ชันที่ติดตั้งไม่ตรงกัน หรือเวอร์ชันตรงกันแล้วแต่ดันอยู่คนละ OS&lt;/p&gt;

&lt;p&gt;นี่แหละคือปัญหาคลาสสิก &lt;strong&gt;"It works on my machine"&lt;/strong&gt; และเป็นเหตุผลที่ Docker เกิดขึ้นมา&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker ต่างจาก VM ยังไง
&lt;/h2&gt;

&lt;p&gt;ถ้ามองแบบหยาบ ๆ Docker ก็คล้าย VM นะ คือแยกแอปออกไปรันในพื้นที่ของตัวเอง แต่ต่างกันตรงนี้&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VM (Virtual Machine)&lt;/strong&gt; คือการจำลองคอมพิวเตอร์ขึ้นมาทั้งเครื่อง มี OS ของตัวเองครบชุด เลยกินทรัพยากรเยอะ และบูตนาน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; เอาแค่สิ่งที่แอปต้องใช้จริง ๆ เช่น โค้ด runtime library และ config มัดรวมเป็นก้อนเดียว แล้วใช้ kernel ร่วมกับเครื่องหลัก เลยเบาและเปิดขึ้นได้ในไม่กี่วินาที&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ก้อนที่มัดรวมทุกอย่างไว้นั่นแหละ เรียกว่า &lt;strong&gt;Image&lt;/strong&gt;&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%2Fq6ghwhv6v08hqfiv5bm8.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%2Fq6ghwhv6v08hqfiv5bm8.png" alt="เปรียบเทียบ VM กับ Docker" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Image: แม่แบบของแอป
&lt;/h2&gt;

&lt;p&gt;Image มีอยู่ 2 แบบ&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Image สำเร็จรูป&lt;/strong&gt; ที่มีคน build ไว้ให้แล้วบน &lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt; เช่น &lt;code&gt;mysql&lt;/code&gt;, &lt;code&gt;phpmyadmin&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;nginx&lt;/code&gt; ดึงมาใช้ได้เลย&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image ของแอปเราเอง&lt;/strong&gt; อันนี้ต้อง build เอง ถ้าโปรเจกต์มีหน้าเว็บ 1 ตัว กับ API 1 ตัว ก็ต้อง build ออกมาเป็นอย่างละ 1 image&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;จะ build image ได้ ต้องมีไฟล์ชื่อ &lt;strong&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/strong&gt; อยู่ในแต่ละโปรเจกต์ (web กับ api แยกกันคนละไฟล์) ข้างในจะบอกว่า&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ใช้ image ตั้งต้นตัวไหน เช่น &lt;code&gt;node:22-alpine&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ตอน build ต้องรันคำสั่งอะไร เช่น &lt;code&gt;npm install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ตอนเปิดแอปให้รันคำสั่งอะไร เช่น &lt;code&gt;npm start&lt;/code&gt; หรือ &lt;code&gt;node dist/index.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ตัวอย่าง &lt;code&gt;Dockerfile&lt;/code&gt; ของ API (Node.js) หน้าตาประมาณนี้&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="c"&gt;# 1) ใช้ image ตั้งต้นเป็น Node.js 22 แบบ alpine (ขนาดเล็ก)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:22-alpine&lt;/span&gt;

&lt;span class="c"&gt;# 2) กำหนดโฟลเดอร์ทำงานใน container&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# 3) copy ไฟล์ package ก่อน แล้วติดตั้ง dependency&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

&lt;span class="c"&gt;# 4) copy โค้ดที่เหลือเข้าไป&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# 5) บอกว่าแอปใช้ port 3000&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# 6) คำสั่งที่รันตอน container เริ่มทำงาน&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "src/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 ทำไม copy &lt;code&gt;package.json&lt;/code&gt; ก่อนโค้ด? เพราะ Docker build เป็นชั้น ๆ (layer) และจำ cache ไว้ ถ้าแก้แค่โค้ดแต่ไม่ได้แก้ dependency รอบหน้าจะไม่ต้อง &lt;code&gt;npm ci&lt;/code&gt; ใหม่ build เร็วขึ้นเยอะ&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Container: Image ที่ถูกรันแล้ว
&lt;/h2&gt;

&lt;p&gt;แต่ image เฉย ๆ ทำอะไรไม่ได้นะ มันเป็นแค่แม่แบบ จะใช้งานได้ต้องเอาไป &lt;strong&gt;run&lt;/strong&gt; ก่อน ถึงจะกลายเป็น &lt;strong&gt;Container&lt;/strong&gt; ซึ่งก็คือแอปที่กำลังทำงานอยู่จริง เปิดใช้งานได้ตามปกติ&lt;/p&gt;

&lt;p&gt;ถ้าเป็นสายเขียนโค้ด ให้นึกว่า &lt;strong&gt;image คือ class&lt;/strong&gt; ส่วน &lt;strong&gt;container คือ object&lt;/strong&gt; ที่ new ออกมา จาก image เดียวจะรันกี่ container ก็ได้&lt;/p&gt;

&lt;p&gt;ดังนั้นถ้าโปรเจกต์มี &lt;strong&gt;web + api + database&lt;/strong&gt; ก็จะมี &lt;strong&gt;3 image&lt;/strong&gt; และพอรันก็จะได้ &lt;strong&gt;3 container&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ไม่ยากใช่ไหมล่ะ แค่ build image แล้วรันเป็น container 😄&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%2Fw0080frvjgcz4u6bhgpo.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%2Fw0080frvjgcz4u6bhgpo.png" alt="จาก Dockerfile สู่ Image และ Container" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Volume: ที่เก็บข้อมูลที่ไม่หายไปกับ Container
&lt;/h2&gt;

&lt;p&gt;นอกจาก image กับ container แล้ว ยังมีอีก 2 ตัวที่ต้องรู้จัก ตัวแรกคือ &lt;strong&gt;Volume&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;container ถูกออกแบบมาให้ลบทิ้งแล้วสร้างใหม่ได้ตลอด ถ้าเก็บข้อมูลไว้ข้างใน container พอลบ ข้อมูลก็หายไปด้วย&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume&lt;/strong&gt; คือพื้นที่เก็บข้อมูลที่แยกออกมาไว้ข้างนอก container เช่น ไฟล์ฐานข้อมูลของ MySQL หรือรูปที่ผู้ใช้อัปโหลด สมมติวันหนึ่งเราลบ container หรือลบ image ไปเลย แต่ถ้าไม่ได้ลบ volume ข้อมูลที่ใช้งานมาก็ยังอยู่ครบ สร้าง container ใหม่แล้วผูกกับ volume เดิม ข้อมูลก็กลับมาเหมือนเดิม&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 Volume ไม่ใช่ตัวฐานข้อมูลนะ ตัวฐานข้อมูลคือ container ของ MySQL ส่วน volume คือ "ที่เก็บไฟล์" ของฐานข้อมูลนั้น&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Network: ทางเชื่อมระหว่าง Container
&lt;/h2&gt;

&lt;p&gt;ตัวที่สองคือ &lt;strong&gt;Network&lt;/strong&gt; ทำหน้าที่เชื่อม web, api และ database ให้อยู่วงเดียวกัน เรียกใช้งานกันได้เหมือนอยู่ในเครื่องเดียวกัน&lt;/p&gt;

&lt;p&gt;ข้อดีคือ container ใน network เดียวกันเรียกหากันด้วย &lt;strong&gt;ชื่อ service&lt;/strong&gt; ได้เลย เช่น API ต่อฐานข้อมูลด้วย host &lt;code&gt;db&lt;/code&gt; ไม่ต้องไปจำ IP&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;จุดที่มือใหม่พลาดบ่อย:&lt;/strong&gt; ชื่อ &lt;code&gt;db&lt;/code&gt; หรือ &lt;code&gt;api&lt;/code&gt; ใช้ได้เฉพาะตอน container คุยกันเองเท่านั้น หน้าเว็บที่รันอยู่บน browser ของผู้ใช้ไม่ได้อยู่ใน network นี้ เวลาหน้าเว็บเรียก API ต้องเรียกผ่าน port ที่เปิดออกมา เช่น &lt;code&gt;http://localhost:3000&lt;/code&gt; (หรือโดเมนจริงตอน deploy)&lt;/p&gt;
&lt;/blockquote&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%2Fnpy522ky7rmmycpwu5lk.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%2Fnpy522ky7rmmycpwu5lk.png" alt="การเชื่อมต่อ container ผ่าน network และ volume" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  สรุป 5 คำที่ต้องรู้
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;คำ&lt;/th&gt;
&lt;th&gt;คืออะไร&lt;/th&gt;
&lt;th&gt;เปรียบเทียบ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ไฟล์สูตร บอกว่าใช้ image อะไร รันคำสั่งอะไร&lt;/td&gt;
&lt;td&gt;สูตรอาหาร&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;แม่แบบที่ build จาก Dockerfile พร้อมทุกอย่างที่แอปต้องใช้&lt;/td&gt;
&lt;td&gt;class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;image ที่ถูกรันแล้ว คือแอปที่ทำงานอยู่จริง&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ที่เก็บข้อมูลถาวร ไม่หายแม้ลบ container&lt;/td&gt;
&lt;td&gt;external hard disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;วงเชื่อมให้ container เรียกหากันด้วยชื่อได้&lt;/td&gt;
&lt;td&gt;วง LAN ในบ้าน&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  คำสั่งพื้นฐานที่ใช้บ่อย
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;เช็กว่าติดตั้งแล้ว&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Image&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull mysql:8.4              &lt;span class="c"&gt;# ดึง image สำเร็จรูปจาก Docker Hub&lt;/span&gt;
docker images                      &lt;span class="c"&gt;# ดู image ที่มีในเครื่อง&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-api ./api       &lt;span class="c"&gt;# build image จาก Dockerfile ในโฟลเดอร์ api&lt;/span&gt;
docker rmi my-api                  &lt;span class="c"&gt;# ลบ image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Container&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; api &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 my-api   &lt;span class="c"&gt;# รัน container (-d = รันเบื้องหลัง, -p = เปิด port เครื่อง:container)&lt;/span&gt;
docker ps                          &lt;span class="c"&gt;# ดู container ที่กำลังรัน&lt;/span&gt;
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;                       &lt;span class="c"&gt;# ดูทั้งหมด รวมตัวที่หยุดแล้ว&lt;/span&gt;
docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; api                 &lt;span class="c"&gt;# ดู log แบบ real-time&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; api sh             &lt;span class="c"&gt;# เข้าไปใน container&lt;/span&gt;
docker stop api                    &lt;span class="c"&gt;# หยุด&lt;/span&gt;
docker start api                   &lt;span class="c"&gt;# เริ่มใหม่&lt;/span&gt;
docker &lt;span class="nb"&gt;rm &lt;/span&gt;api                      &lt;span class="c"&gt;# ลบ container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Volume และ Network&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;                   &lt;span class="c"&gt;# ดู volume ทั้งหมด&lt;/span&gt;
docker volume create db_data       &lt;span class="c"&gt;# สร้าง volume&lt;/span&gt;
docker network &lt;span class="nb"&gt;ls&lt;/span&gt;                  &lt;span class="c"&gt;# ดู network ทั้งหมด&lt;/span&gt;
docker network create app-net      &lt;span class="c"&gt;# สร้าง network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ปัญหา: ถ้าทำทุกอย่างด้วยมือ คำสั่งจะยาวมาก
&lt;/h2&gt;

&lt;p&gt;ที่เล่ามาทั้งหมด ถ้าต้องพิมพ์ใน terminal เองทีละคำสั่ง ระบบที่มี database, phpMyAdmin, api และ web จะออกมาหน้าตาแบบนี้&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create app-net
docker volume create db_data

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; db &lt;span class="nt"&gt;--network&lt;/span&gt; app-net &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;changeme &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; db_data:/var/lib/mysql mysql:8.4

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; phpmyadmin &lt;span class="nt"&gt;--network&lt;/span&gt; app-net &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PMA_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 phpmyadmin

docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-api ./api
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; api &lt;span class="nt"&gt;--network&lt;/span&gt; app-net &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;changeme &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DB_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 my-api

docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-web ./web
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; web &lt;span class="nt"&gt;--network&lt;/span&gt; app-net &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 my-web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ยาวใช่ไหม แถมพลาดนิดเดียว เช่น ลืมใส่ &lt;code&gt;--network&lt;/code&gt; หรือพิมพ์ชื่อ volume ผิด ระบบก็พังทันที แล้วทุกครั้งที่ย้ายเครื่องก็ต้องมานั่งพิมพ์ใหม่อีก&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker Compose: เขียนครั้งเดียว รันคำสั่งเดียว
&lt;/h2&gt;

&lt;p&gt;ปัญหานี้เลยเป็นที่มาของ &lt;strong&gt;Docker Compose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker Compose คือไฟล์ตั้งค่า (YAML) ที่เราเขียนโครงสร้างระบบไว้ว่ามี service อะไรบ้าง ใช้ image ไหน เปิด port อะไร ต่อ volume กับ network ยังไง จากนั้นสั่ง &lt;strong&gt;คำสั่งเดียว&lt;/strong&gt; ระบบก็ขึ้นครบทั้งชุด&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%2Fyhwes7q6p5crevwjb26y.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%2Fyhwes7q6p5crevwjb26y.png" alt="Docker Compose: ไฟล์เดียว คำสั่งเดียว" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ตัวอย่างโครงสร้างโปรเจกต์
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-project/
├── api/
│   ├── src/index.js
│   ├── package.json
│   ├── Dockerfile
│   └── .dockerignore
├── web/
│   ├── src/...
│   ├── package.json
│   ├── Dockerfile
│   └── .dockerignore
├── .env
└── compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;api/Dockerfile&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;ใช้ไฟล์เดียวกับตัวอย่างด้านบนได้เลย&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;web/Dockerfile&lt;/code&gt; (เช่น React/Vue ที่ build ด้วย Vite)
&lt;/h3&gt;

&lt;p&gt;หน้าเว็บแบบนี้ใช้ Node แค่ตอน build พอได้ไฟล์ static แล้วก็ส่งต่อให้ Nginx เสิร์ฟ เรียกว่า &lt;strong&gt;multi-stage build&lt;/strong&gt; ทำให้ image สุดท้ายเล็กมาก&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="c"&gt;# Stage 1: build หน้าเว็บ&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Stage 2: เอาเฉพาะไฟล์ที่ build แล้วไปเสิร์ฟด้วย Nginx&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist /usr/share/nginx/html&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;.dockerignore&lt;/code&gt; (ใส่ทั้งใน api และ web)
&lt;/h3&gt;

&lt;p&gt;กันไม่ให้ไฟล์ไม่จำเป็นถูก copy เข้าไปใน image&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
dist
.env
.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;เก็บรหัสผ่านแยกไว้ &lt;strong&gt;อย่า commit ขึ้น Git&lt;/strong&gt; (ใส่ไว้ใน &lt;code&gt;.gitignore&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_ROOT_PASSWORD=changeme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;compose.yaml&lt;/code&gt;
&lt;/h3&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="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.4&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/mysql&lt;/span&gt;        &lt;span class="c1"&gt;# เก็บข้อมูลไว้ใน volume&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="c1"&gt;# เช็กว่า MySQL พร้อมรับ connection แล้ว&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysqladmin"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-h"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&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="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-net&lt;/span&gt;

  &lt;span class="na"&gt;phpmyadmin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phpmyadmin&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PMA_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;                     &lt;span class="c1"&gt;# เรียก database ด้วยชื่อ service&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-net&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;                       &lt;span class="c1"&gt;# build จาก api/Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
      &lt;span class="na"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;     &lt;span class="c1"&gt;# รอจน db พร้อมจริง ๆ ค่อยเริ่ม&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-net&lt;/span&gt;

  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./web&lt;/span&gt;                       &lt;span class="c1"&gt;# build จาก web/Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-net&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  คำสั่ง Docker Compose ที่ใช้บ่อย
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;               &lt;span class="c"&gt;# build (ถ้ายังไม่มี) + รันทั้งระบบ&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;       &lt;span class="c"&gt;# build image ใหม่ แล้วรัน (ใช้หลังแก้โค้ด)&lt;/span&gt;
docker compose ps                  &lt;span class="c"&gt;# ดูสถานะทุก service&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; api         &lt;span class="c"&gt;# ดู log ของ api&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;db mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;   &lt;span class="c"&gt;# เข้า MySQL ใน container&lt;/span&gt;
docker compose restart api         &lt;span class="c"&gt;# restart เฉพาะ api&lt;/span&gt;
docker compose down                &lt;span class="c"&gt;# หยุดและลบ container + network (ข้อมูลใน volume ยังอยู่)&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;             &lt;span class="c"&gt;# ⚠️ ลบ volume ด้วย = ข้อมูลหายหมด!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;รันแล้วเปิดดูได้เลย&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;หน้าเว็บ → &lt;code&gt;http://localhost&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API → &lt;code&gt;http://localhost:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;phpMyAdmin → &lt;code&gt;http://localhost:8080&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ข้อควรระวังเล็ก ๆ ที่ช่วยได้เยอะ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker compose down -v&lt;/code&gt; จะลบ volume&lt;/strong&gt; ข้อมูลในฐานข้อมูลหายหมด ใช้ตอนอยากล้างระบบจริง ๆ เท่านั้น&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;depends_on&lt;/code&gt; เฉย ๆ แค่รอให้ container เริ่ม&lt;/strong&gt; ไม่ได้รอให้ฐานข้อมูลพร้อมรับ connection เลยต้องใช้ &lt;code&gt;healthcheck&lt;/code&gt; คู่กับ &lt;code&gt;condition: service_healthy&lt;/code&gt; แบบในตัวอย่าง&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ใช้ &lt;code&gt;docker compose&lt;/code&gt; (มีเว้นวรรค)&lt;/strong&gt; เป็นเวอร์ชันปัจจุบัน ส่วน &lt;code&gt;docker-compose&lt;/code&gt; (มีขีด) เป็นตัวเก่า และไม่ต้องใส่ &lt;code&gt;version:&lt;/code&gt; บนหัวไฟล์แล้ว&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;บน Windows ใช้ Docker Desktop (WSL 2)&lt;/strong&gt; container ที่รันอยู่ก็คือ Linux container เหมือนบนเซิร์ฟเวอร์ ปัญหา "Windows รันได้ Linux รันไม่ได้" เลยหายไปเกือบหมด&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ระวังเรื่องสถาปัตยกรรม CPU&lt;/strong&gt; ถ้า build บนเครื่อง ARM (เช่น Mac ชิป M) แล้วจะเอา image ไปรันบนเซิร์ฟเวอร์ x86 ให้ build ด้วย &lt;code&gt;docker buildx build --platform linux/amd64 ...&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  สรุป
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfile&lt;/strong&gt; → build ได้ &lt;strong&gt;Image&lt;/strong&gt; → run ได้ &lt;strong&gt;Container&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume&lt;/strong&gt; เก็บข้อมูลไว้ไม่ให้หายไปกับ container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network&lt;/strong&gt; เชื่อม container ให้เรียกหากันด้วยชื่อได้&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose&lt;/strong&gt; รวมทุกอย่างไว้ในไฟล์เดียว สั่ง &lt;code&gt;docker compose up -d&lt;/code&gt; คำสั่งเดียวจบ&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ครั้งหน้าส่งโปรเจกต์ให้เพื่อน แค่แนบ &lt;code&gt;compose.yaml&lt;/code&gt; ไปด้วย เพื่อนก็รันได้เหมือนเครื่องเราเป๊ะ ไม่ต้องมานั่งถามว่า "ใช้ Node เวอร์ชันไหน" อีกแล้ว 🐳&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Docker คืออะไร? ทำไม dev ทั้งโลกถึงชอบเอาโค้ดไปใส่กล่อง (ฉบับเข้าใจง่าย)</title>
      <dc:creator>reerii code</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:49:28 +0000</pubDate>
      <link>https://dev.to/reeriicode/docker-khuueaair-thamaim-dev-thangolkthuengchbeaaokhdaipaisklng-chbabekhaaaicchngaay-3l88</link>
      <guid>https://dev.to/reeriicode/docker-khuueaair-thamaim-dev-thangolkthuengchbeaaokhdaipaisklng-chbabekhaaaicchngaay-3l88</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;สรุปสั้นๆ ก่อนอ่าน (TL;DR)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ปัญหา: "ในเครื่องผมรันได้นะ!" แต่ที่อื่นพัง เพราะ &lt;strong&gt;สภาพแวดล้อมไม่เหมือนกัน&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Docker = เอาแอป &lt;strong&gt;พร้อมทุกอย่างที่แอปต้องใช้&lt;/strong&gt; ใส่กล่องใบเดียว ไปรันที่ไหนก็เหมือนเดิม&lt;/li&gt;
&lt;li&gt;จำแค่ 3 คำก่อน: &lt;strong&gt;Dockerfile&lt;/strong&gt; (สูตร) → &lt;strong&gt;Image&lt;/strong&gt; (แม่พิมพ์) → &lt;strong&gt;Container&lt;/strong&gt; (ขนมที่รันอยู่จริง)&lt;/li&gt;
&lt;li&gt;มีหลายกล่องเมื่อไหร่ ใช้ &lt;strong&gt;Docker Compose&lt;/strong&gt; ไฟล์เดียว คำสั่งเดียว &lt;code&gt;docker compose up -d&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🎬 ดูแบบวิดีโอ: DEPLOY 101 EP.6 (ฉบับเต็ม) บน YouTube&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. "ในเครื่องผมรันได้นะ!" ปัญหาที่ dev ทุกคนเคยเจอ
&lt;/h2&gt;

&lt;p&gt;ประโยคนี้ใครเคยพูด ยกมือขึ้น! ผมเองก็เคย และประโยคนี้แหละ คือจุดเริ่มต้นของ Docker&lt;/p&gt;

&lt;p&gt;ทำ API เสร็จ เทสในเครื่องผ่านหมด มั่นใจเต็มร้อย แต่พอเอาไปให้เพื่อนรัน หรือขึ้น server จริง… พังแบบงงๆ ทั้งที่โค้ดเดียวกันเป๊ะ&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%2Fldeg66shv5y9orraqwdi.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%2Fldeg66shv5y9orraqwdi.png" alt="เครื่องเรา vs เครื่องเพื่อน vs server" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;เครื่อง&lt;/th&gt;
&lt;th&gt;OS&lt;/th&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;ผลลัพธ์&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;เครื่องเรา&lt;/td&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;✅ รันได้ สบายใจ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;เครื่องเพื่อน&lt;/td&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;❌ บางฟีเจอร์พัง (Node เก่ากว่า)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server จริง&lt;/td&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;❌ พังตอน migrate (DB เก่า)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ปัญหาไม่ได้อยู่ที่โค้ด แต่อยู่ที่ "สภาพแวดล้อม" (environment)&lt;/strong&gt; — OS, เวอร์ชัน runtime, เวอร์ชัน database, library ที่ลงไว้ ไม่เหมือนกันสักเครื่อง&lt;/p&gt;

&lt;p&gt;แถมยังมีอีก 2 ปัญหาที่ตามมาเสมอ&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;น้องใหม่เข้าทีม&lt;/strong&gt; ต้องนั่งลง Node, PostgreSQL, Redis ตามเอกสารเมื่อสองปีก่อน กว่าจะรันได้ หมดไปหนึ่งวัน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;เครื่องรก&lt;/strong&gt; โปรเจกต์ A ใช้ PostgreSQL 14 โปรเจกต์ B ใช้ 16 ลงพร้อมกันก็ตีกันเรื่องพอร์ต เลิกทำแล้วก็ยังค้างอยู่ในเครื่อง&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🤔 "งั้นก็ยกคอมเครื่องที่รันได้ ไปตั้งไว้ที่ server เลยสิ!"&lt;/p&gt;

&lt;p&gt;ฟังดูบ้า แต่&lt;strong&gt;ถูกครึ่งนึง&lt;/strong&gt; — Docker ก็คือการ "ยกเครื่องเราไปด้วย" แค่ไม่ต้องยกทั้งเครื่อง ยกไปแค่ &lt;strong&gt;กล่องใบเดียว&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Docker คืออะไร?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docker คือเครื่องมือที่แพ็กแอปของเรา พร้อมทุกอย่างที่แอปต้องใช้ ลงกล่องใบเดียว&lt;/strong&gt; (Node เวอร์ชันที่ถูก, library, ไฟล์ตั้งค่า, คำสั่งรัน) กล่องนี้เรียกว่า &lt;strong&gt;container&lt;/strong&gt;&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%2Frktn38l3dqcllawvjojs.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%2Frktn38l3dqcllawvjojs.png" alt="Docker = ตู้คอนเทนเนอร์" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;นึกถึง &lt;strong&gt;ตู้คอนเทนเนอร์บนเรือสินค้า&lt;/strong&gt; ข้างในเป็นทุเรียนหรือทีวี เรือไม่สนใจ ขอแค่เป็นตู้ขนาดมาตรฐาน ยกขึ้นเรือ ขึ้นรถ ลงท่าไหนก็ได้&lt;/p&gt;

&lt;p&gt;Docker ก็เหมือนกัน แอปจะเขียนด้วย Node, Python หรือ Go ไม่สำคัญ ขอแค่อยู่ในกล่อง &lt;strong&gt;รันเครื่องไหนก็ได้ผลเหมือนกัน&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ได้อะไรจาก Docker?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;รันที่ไหนก็เหมือนกัน&lt;/strong&gt; เครื่องเรารันได้ server ก็รันได้&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;น้องใหม่คำสั่งเดียวจบ&lt;/strong&gt; มีแค่ Docker ไม่ต้องลง Node/DB เอง&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;หลายเวอร์ชันอยู่ด้วยกันได้&lt;/strong&gt; PostgreSQL 14 กับ 16 อยู่คนละกล่อง ไม่ตีกัน&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;เครื่องสะอาด&lt;/strong&gt; เลิกใช้ก็ลบกล่องทิ้ง&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;deploy ง่าย&lt;/strong&gt; VPS, EC2, Cloud Run ก็แค่เอากล่องนี้ไปรัน&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Container ไม่ใช่ VM นะ
&lt;/h3&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%2Fvptgcnx8oxv4fzvsopt8.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%2Fvptgcnx8oxv4fzvsopt8.png" alt="VM vs Container" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;VM&lt;/th&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;เปรียบเหมือน&lt;/td&gt;
&lt;td&gt;🏠 บ้านทั้งหลัง&lt;/td&gt;
&lt;td&gt;🏢 ห้องในคอนโด&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;มี OS ของตัวเอง&lt;/td&gt;
&lt;td&gt;มีครบชุด&lt;/td&gt;
&lt;td&gt;ไม่มี ใช้ kernel ร่วมกับเครื่องแม่&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ขนาด&lt;/td&gt;
&lt;td&gt;หลาย GB&lt;/td&gt;
&lt;td&gt;หลักสิบ–ร้อย MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;เวลาเปิด&lt;/td&gt;
&lt;td&gt;เป็นนาที&lt;/td&gt;
&lt;td&gt;ไม่กี่วินาที&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 บน Windows / Mac ตัว &lt;strong&gt;Docker Desktop&lt;/strong&gt; จะแอบรัน Linux ตัวเล็กๆ ให้เบื้องหลัง เราไม่ต้องยุ่งเลย&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. ศัพท์ 7 คำที่ต้องรู้ (เปรียบกับการทำขนม 🍮)
&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%2Fbqnglpdoifq0hylzlcyk.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%2Fbqnglpdoifq0hylzlcyk.png" alt="ศัพท์ 7 คำ" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) Dockerfile = สูตรอาหาร&lt;/strong&gt; — ไฟล์ข้อความที่จดว่ากล่องต้องมีอะไร ทำทีละขั้นยังไง (ก็คือขั้นตอนตอนเรา deploy บน EC2 เองนั่นแหละ แค่จดให้ Docker ทำแทน)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Image = แม่พิมพ์&lt;/strong&gt; — ได้จาก &lt;code&gt;docker build&lt;/code&gt; พร้อมรัน และ &lt;strong&gt;แก้ไขไม่ได้&lt;/strong&gt; อยากเปลี่ยนต้อง build ใหม่&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Container = ขนมจากแม่พิมพ์&lt;/strong&gt; — ได้จาก &lt;code&gt;docker run&lt;/code&gt; คือของที่กำลังรันจริง แม่พิมพ์อันเดียวปั๊มได้หลายชิ้น&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%2Fg1jkgfzn8bh9pk4f1k9z.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%2Fg1jkgfzn8bh9pk4f1k9z.png" alt="Dockerfile → Image → Container" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) Registry = ร้านขายแม่พิมพ์&lt;/strong&gt; — ที่เก็บ image เช่น &lt;strong&gt;Docker Hub&lt;/strong&gt; (มี PostgreSQL, Redis, Nginx สำเร็จรูป) หรือ &lt;strong&gt;ghcr.io&lt;/strong&gt; ของ GitHub ไว้ฝาก image ของเราให้ server ดึงไปรัน&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Port = เจาะรูกล่อง&lt;/strong&gt; — กล่องปิดสนิท ข้างนอกเรียกไม่ได้ ต้องเจาะด้วย &lt;code&gt;-p 5000:5000&lt;/code&gt; (อ่านว่า &lt;strong&gt;พอร์ตเครื่องเรา : พอร์ตในกล่อง&lt;/strong&gt;)&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%2Fovwd0ir8qe5nyttu6god.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%2Fovwd0ir8qe5nyttu6god.png" alt="Port" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) Volume = ตู้เซฟนอกกล่อง (สำคัญมาก!)&lt;/strong&gt; — container เหมือนห้องโรงแรม เช็คเอาท์ปุ๊บ ของโดนเคลียร์ ลบกล่องเมื่อไหร่ข้อมูลหาย volume คือที่เก็บข้อมูล &lt;strong&gt;นอกกล่อง&lt;/strong&gt; กล่องพังกี่รอบข้อมูลก็อยู่ ใช้กับไฟล์ database และไฟล์ที่ผู้ใช้อัปโหลด&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%2F0tmnstfghiees7ac3442.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%2F0tmnstfghiees7ac3442.png" alt="Volume" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7) Network = สายโทรศัพท์ระหว่างกล่อง&lt;/strong&gt; — กล่องที่อยู่ network เดียวกันถึงจะคุยกันได้ และ &lt;strong&gt;เรียกกันด้วยชื่อกล่องได้เลย&lt;/strong&gt; ไม่ต้องจำ IP&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%2Fryedllgio9s6ab6k5ay4.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%2Fryedllgio9s6ab6k5ay4.png" alt="Network" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  สรุป 7 คำในตารางเดียว
&lt;/h3&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%2F8111azg8xq796ymwl916.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%2F8111azg8xq796ymwl916.png" alt="สรุป 7 คำ" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ศัพท์&lt;/th&gt;
&lt;th&gt;เปรียบเหมือน&lt;/th&gt;
&lt;th&gt;คำสั่งที่เกี่ยวข้อง&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dockerfile&lt;/td&gt;
&lt;td&gt;สูตรอาหาร&lt;/td&gt;
&lt;td&gt;(เขียนไฟล์เอง)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image&lt;/td&gt;
&lt;td&gt;แม่พิมพ์&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker build&lt;/code&gt;, &lt;code&gt;docker pull&lt;/code&gt;, &lt;code&gt;docker images&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;ขนมจากแม่พิมพ์&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker run&lt;/code&gt;, &lt;code&gt;docker ps&lt;/code&gt;, &lt;code&gt;docker stop&lt;/code&gt;, &lt;code&gt;docker rm&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;ร้านขายแม่พิมพ์&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker pull&lt;/code&gt;, &lt;code&gt;docker push&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;รูที่เจาะเข้ากล่อง&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-p เครื่องเรา:ในกล่อง&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume&lt;/td&gt;
&lt;td&gt;ตู้เซฟนอกกล่อง&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker volume create&lt;/code&gt;, &lt;code&gt;-v&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;สายโทรศัพท์&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker network create&lt;/code&gt;, &lt;code&gt;--network&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;จำประโยคเดียว: &lt;strong&gt;สูตรถูก build เป็นแม่พิมพ์ → แม่พิมพ์ถูก run เป็นกล่อง → กล่องใน network เดียวกันเรียกกันด้วยชื่อ → ข้อมูลที่ห้ามหายอยู่ใน volume&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Image มี 2 แบบ (จุดที่มือใหม่งงที่สุด)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  แบบที่ 1: สำเร็จรูป — มีคนทำให้แล้ว
&lt;/h3&gt;

&lt;p&gt;PostgreSQL, MySQL, Redis, Nginx, MongoDB มี image บน Docker Hub ให้แล้ว &lt;strong&gt;ไม่ต้องเขียน Dockerfile ไม่ต้อง build&lt;/strong&gt; ดึงมารันแล้วใส่ค่าตั้งต้นที่มันขอ&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%2Fmqrg8cjdb7z9k1yp5f8d.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%2Fmqrg8cjdb7z9k1yp5f8d.png" alt="Image สำเร็จรูป" width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# คำสั่งเดียว ได้ database พร้อมใช้&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; my_db &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password &lt;span class="nt"&gt;-p&lt;/span&gt; 5432:5432 postgres:16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;เลข &lt;code&gt;16&lt;/code&gt; คือ &lt;strong&gt;tag&lt;/strong&gt; = เวอร์ชัน (&lt;code&gt;postgres:14&lt;/code&gt;, &lt;code&gt;mysql:8.4&lt;/code&gt;, &lt;code&gt;nginx:alpine&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alpine&lt;/code&gt; = รุ่นที่ใช้ Linux ตัวเล็กมาก image เลยเบา&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  แบบที่ 2: ของเรา — ต้องทำเอง
&lt;/h3&gt;

&lt;p&gt;API / Web ที่เราเขียน ไม่มีใครทำ image ให้แน่นอน ต้องลงมือเอง 5 ขั้น&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%2Frx1xb1t3gcpeswrj8xw3.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%2Frx1xb1t3gcpeswrj8xw3.png" alt="Image ของเรา" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;เขียน &lt;strong&gt;Dockerfile&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker build&lt;/code&gt; → ได้ image&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker run&lt;/code&gt; → ได้ container&lt;/li&gt;
&lt;li&gt;จัดการ &lt;strong&gt;network&lt;/strong&gt; ให้ API คุยกับ DB ได้&lt;/li&gt;
&lt;li&gt;จัดการ &lt;strong&gt;volume&lt;/strong&gt; ให้ข้อมูลไม่หาย&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;สำเร็จรูป&lt;/th&gt;
&lt;th&gt;ของเรา&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ตัวอย่าง&lt;/td&gt;
&lt;td&gt;postgres, mysql, redis, nginx&lt;/td&gt;
&lt;td&gt;API (Express, Prisma), Web (React)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ต้องเขียน Dockerfile&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ได้มาด้วย&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker pull&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker build&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  5. เขียน Dockerfile ของ API ทีละบรรทัด
&lt;/h2&gt;

&lt;h3&gt;
  
  
  คำสั่งหลัก มีไม่กี่ตัว
&lt;/h3&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%2F0niw5m8x4u0rnqstd4st.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%2F0niw5m8x4u0rnqstd4st.png" alt="คำสั่งใน Dockerfile" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;คำสั่ง&lt;/th&gt;
&lt;th&gt;ทำอะไร&lt;/th&gt;
&lt;th&gt;เทียบกับลงเครื่องเอง&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;เลือก image ฐาน&lt;/td&gt;
&lt;td&gt;ลง Node ในเครื่องเปล่า&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WORKDIR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;โฟลเดอร์ทำงานในกล่อง&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cd /app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COPY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;copy ไฟล์เข้ากล่อง&lt;/td&gt;
&lt;td&gt;อัปโหลดโค้ดขึ้น server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;รันคำสั่งตอน build&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;npx tsc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ตั้งค่า environment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export NODE_ENV=production&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXPOSE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;บอกว่าแอปใช้พอร์ตไหน&lt;/td&gt;
&lt;td&gt;จดว่าแอปฟัง 5000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;เปลี่ยนผู้ใช้ที่รันแอป&lt;/td&gt;
&lt;td&gt;ไม่รันด้วย root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CMD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;คำสั่งตอนกล่องเริ่ม&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node dist/server.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;🔑 &lt;strong&gt;RUN = ทำตอน build (ครั้งเดียว)&lt;/strong&gt; · &lt;strong&gt;CMD = ทำตอน run (ทุกครั้งที่เปิดกล่อง)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dockerfile ของจริง (multi-stage 3 ชั้น)
&lt;/h3&gt;

&lt;p&gt;เหมือน &lt;strong&gt;ครัว 3 โต๊ะ&lt;/strong&gt;: โต๊ะเตรียมของ → โต๊ะปรุง → จานเสิร์ฟ แล้วเอาไปให้ลูกค้าแค่ &lt;strong&gt;จานเสิร์ฟ&lt;/strong&gt; เศษผักทิ้งไว้ในครัว&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%2Fwlfu6g9144dqe8yhe3ld.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%2Fwlfu6g9144dqe8yhe3ld.png" alt="multi-stage" width="800" height="429"&gt;&lt;/a&gt;&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="c"&gt;# ── Stage 1: เตรียมวัตถุดิบ (deps) ──&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;deps&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; prisma ./prisma/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# ── Stage 2: ปรุง (builder) ──&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/package.json ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/prisma ./prisma/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; tsconfig.json ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src ./src/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npx prisma generate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx tsc &lt;span class="nt"&gt;-p&lt;/span&gt; tsconfig.json

&lt;span class="c"&gt;# ── Stage 3: จานเสิร์ฟ (runner) = image ที่ใช้จริง ──&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; openssl
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/prisma ./prisma&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; uploads &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; node:node /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["sh", "-c", "npx prisma migrate deploy &amp;amp;&amp;amp; node dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stage 1 (deps)&lt;/strong&gt; — ทำไม copy แค่ &lt;code&gt;package.json&lt;/code&gt; กับ lock ก่อน ยังไม่ copy โค้ด?&lt;/p&gt;

&lt;p&gt;เพราะ Docker &lt;strong&gt;จำผลแต่ละบรรทัดไว้ (layer cache)&lt;/strong&gt; ถ้าสองไฟล์นี้ไม่เปลี่ยน รอบหน้าจะ &lt;strong&gt;ข้าม &lt;code&gt;npm ci&lt;/code&gt;&lt;/strong&gt; ไปเลย แก้โค้ดบ่อยแค่ไหนก็ไม่ต้องรอลง library ใหม่&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%2Fn783ccbt8zucue1cltm1.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%2Fn783ccbt8zucue1cltm1.png" alt="layer cache" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ส่วน &lt;code&gt;npm ci&lt;/code&gt; ลง library &lt;strong&gt;ตาม lock เป๊ะๆ&lt;/strong&gt; ไม่แอบอัปเดตแบบ &lt;code&gt;npm install&lt;/code&gt; → ทุกเครื่องได้ผลเหมือนกัน (หัวใจของ Docker)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2 (builder)&lt;/strong&gt; — หยิบ &lt;code&gt;node_modules&lt;/code&gt; จากชั้นแรกมาใช้ ไม่ต้องลงใหม่ แล้วค่อย copy โค้ด สร้าง Prisma Client และแปลง TypeScript → JavaScript ได้โฟลเดอร์ &lt;code&gt;dist&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3 (runner)&lt;/strong&gt; — กล่องใบสุดท้ายที่เอาไปใช้จริง&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%2Fsbr4kxejv5ogwrhlmnty.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%2Fsbr4kxejv5ogwrhlmnty.png" alt="runner" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apk add openssl&lt;/code&gt; เพราะ Prisma ต้องใช้ แต่ alpine ไม่มีมาให้&lt;/li&gt;
&lt;li&gt;หยิบมาแค่ที่จำเป็น &lt;strong&gt;ไม่มีโค้ด TypeScript ต้นฉบับ&lt;/strong&gt; → image เล็ก + ปลอดภัยขึ้น&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USER node&lt;/code&gt; ไม่รันด้วย root ถ้าโดนเจาะ คนร้ายก็ไม่ได้สิทธิ์ root&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXPOSE&lt;/code&gt; เป็นแค่ป้ายบอก ยังไม่ได้เปิดพอร์ตจริง (เปิดจริงตอน &lt;code&gt;-p&lt;/code&gt; หรือใน compose)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CMD&lt;/code&gt; ทุกครั้งที่กล่องเริ่ม: migrate database ก่อน แล้วค่อยเปิด server&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 ชั้น runner ยังหยิบ &lt;code&gt;node_modules&lt;/code&gt; ชุดเต็มมา เพราะ &lt;code&gt;prisma migrate deploy&lt;/code&gt; ต้องใช้ Prisma CLI (มักอยู่ใน devDependencies) อยากให้เล็กลงอีก ค่อยย้าย prisma ไป dependencies แล้วใช้ &lt;code&gt;npm ci --omit=dev&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  อย่าลืม &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/h3&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%2Fvq3cgh1m69zm52r2lf0d.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%2Fvq3cgh1m69zm52r2lf0d.png" alt="dockerignore" width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
dist
.env
.git
uploads
*.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;ลืม &lt;code&gt;node_modules&lt;/code&gt; → ของที่ build มาสำหรับ Windows ติดเข้ากล่อง build ช้าและอาจพัง&lt;/li&gt;
&lt;li&gt;ลืม &lt;code&gt;.env&lt;/code&gt; → &lt;strong&gt;รหัสผ่านหลุดเข้า image&lt;/strong&gt; ใครได้ image ไปก็เห็นหมด 🚨&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Build และรัน
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ปั๊มแม่พิมพ์ (จุด . = โฟลเดอร์ที่มี Dockerfile)&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; upro-pos-api &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# ดู image ที่มี&lt;/span&gt;
docker images

&lt;span class="c"&gt;# รันเป็น container (ค่าลับส่งตอนรัน ไม่ฝังใน image)&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; upro_api &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env upro-pos-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-t&lt;/code&gt; ชื่อ image · &lt;code&gt;-d&lt;/code&gt; รันเบื้องหลัง · &lt;code&gt;--name&lt;/code&gt; ชื่อกล่อง · &lt;code&gt;--env-file&lt;/code&gt; ส่งค่าลับเข้าไปตอนรัน&lt;/p&gt;




&lt;h2&gt;
  
  
  6. แต่! ของจริงไม่ได้มีกล่องเดียว
&lt;/h2&gt;

&lt;p&gt;ระบบ POS ของผมมีอย่างน้อย &lt;strong&gt;3 กล่อง&lt;/strong&gt;: API (image ของเรา) + PostgreSQL + Nginx (สำเร็จรูป) ถ้าทำด้วยมือ…&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%2F84i1zm1h804bcp69vtl4.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%2F84i1zm1h804bcp69vtl4.png" alt="หลายกล่อง" width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create internal
docker volume create postgres_data
docker volume create uploads_data

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; pos_postgres &lt;span class="nt"&gt;--network&lt;/span&gt; internal &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pos_db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; postgres_data:/var/lib/postgresql/data postgres:16

docker build &lt;span class="nt"&gt;-t&lt;/span&gt; upro-pos-api &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; upro_api &lt;span class="nt"&gt;--network&lt;/span&gt; internal &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="nt"&gt;-v&lt;/span&gt; uploads_data:/app/uploads upro-pos-api

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; upro_nginx &lt;span class="nt"&gt;--network&lt;/span&gt; internal &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="nt"&gt;-v&lt;/span&gt; ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro nginx:alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;😵 "ต้องสร้าง network เอง volume เอง เชื่อมเองหมด ใครจะไปจำไหว!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ลืม flag ตัวเดียวก็พัง&lt;/strong&gt; — ลืม &lt;code&gt;--network&lt;/code&gt; API หา DB ไม่เจอ / ลืม &lt;code&gt;-v&lt;/code&gt; ข้อมูลหาย&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ต้องรันตามลำดับ&lt;/strong&gt; — DB ต้องขึ้นก่อน API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;แก้ทีต้อง&lt;/strong&gt; &lt;code&gt;stop&lt;/code&gt; → &lt;code&gt;rm&lt;/code&gt; → พิมพ์ใหม่ทั้งชุด&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;คำสั่งพื้นฐานที่ยังใช้ทุกวัน (เวลาเช็คปัญหา):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps                    &lt;span class="c"&gt;# กล่องที่รันอยู่&lt;/span&gt;
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;                 &lt;span class="c"&gt;# ทั้งหมด รวมที่หยุดแล้ว&lt;/span&gt;
docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; upro_api      &lt;span class="c"&gt;# ดู log สดๆ&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; upro_api sh  &lt;span class="c"&gt;# เข้าไปในกล่อง&lt;/span&gt;
docker stop upro_api         &lt;span class="c"&gt;# หยุด&lt;/span&gt;
docker &lt;span class="nb"&gt;rm &lt;/span&gt;upro_api           &lt;span class="c"&gt;# ลบ&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. ไม่ต้องห่วง! Docker Compose ช่วยได้
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docker Compose = จดทุกกล่อง ทุกการเชื่อมต่อ ลงไฟล์เดียว&lt;/strong&gt; (&lt;code&gt;compose.yaml&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;ถ้า Dockerfile คือ &lt;strong&gt;สูตรขนมหนึ่งชิ้น&lt;/strong&gt; Compose ก็คือ &lt;strong&gt;เมนูจัดเลี้ยงทั้งโต๊ะ&lt;/strong&gt;&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%2Fm36z4kk0zthy5q6qly82.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%2Fm36z4kk0zthy5q6qly82.png" alt="Docker Compose" width="800" height="429"&gt;&lt;/a&gt;&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 up &lt;span class="nt"&gt;-d&lt;/span&gt;        &lt;span class="c"&gt;# ขึ้นทุกกล่อง (network/volume สร้างให้เอง)&lt;/span&gt;
docker compose ps           &lt;span class="c"&gt;# ดูสถานะ&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; api  &lt;span class="c"&gt;# ดู log กล่อง api&lt;/span&gt;
docker compose pull         &lt;span class="c"&gt;# ดึง image เวอร์ชันใหม่&lt;/span&gt;
docker compose down         &lt;span class="c"&gt;# ปิดและลบทุกกล่อง (volume ยังอยู่)&lt;/span&gt;
&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%2F7n8h7fxo7xrsycadmhr9.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%2F7n8h7fxo7xrsycadmhr9.png" alt="ทำด้วยมือ vs Compose" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ทำด้วยมือ&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;สร้าง network / volume&lt;/td&gt;
&lt;td&gt;พิมพ์เอง&lt;/td&gt;
&lt;td&gt;อัตโนมัติ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ลำดับการขึ้น&lt;/td&gt;
&lt;td&gt;จำเอง&lt;/td&gt;
&lt;td&gt;&lt;code&gt;depends_on&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ค่าตั้งค่า&lt;/td&gt;
&lt;td&gt;กระจายในคำสั่งยาวๆ&lt;/td&gt;
&lt;td&gt;ไฟล์เดียว เก็บใน git ได้&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;น้องใหม่เข้าทีม&lt;/td&gt;
&lt;td&gt;ก๊อปทีละบรรทัด&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git clone&lt;/code&gt; + &lt;code&gt;docker compose up -d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;🤔 "แล้ว API จะรู้ได้ไงว่า database อยู่ IP ไหน?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ไม่ต้องรู้!&lt;/strong&gt; ใน Compose &lt;strong&gt;ชื่อ service = hostname&lt;/strong&gt; เรียก &lt;code&gt;postgres:5432&lt;/code&gt; ก็เจอ เหมือนโทรหาเพื่อนด้วยชื่อ ไม่ต้องจำเบอร์&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  8. ก่อนเขียน compose.yaml ต้องเข้าใจอะไร
&lt;/h2&gt;

&lt;p&gt;Compose ไม่มีอะไรใหม่ — มันคือ &lt;strong&gt;flag ของ &lt;code&gt;docker run&lt;/code&gt; ที่ย้ายมาเขียนเป็นไฟล์ YAML&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  YAML ซีเรียสเรื่องเว้นวรรค
&lt;/h3&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%2Fgjah2bzr3f19m49yjb0h.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%2Fgjah2bzr3f19m49yjb0h.png" alt="YAML" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ย่อหน้าด้วย &lt;strong&gt;space 2 ช่อง ห้ามใช้ tab&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key: value&lt;/code&gt; ต้องมี &lt;strong&gt;space หลัง &lt;code&gt;:&lt;/code&gt;&lt;/strong&gt; เสมอ&lt;/li&gt;
&lt;li&gt;รายการหลายตัวใช้ &lt;code&gt;-&lt;/code&gt; นำหน้า&lt;/li&gt;
&lt;li&gt;เช็คไฟล์ได้ด้วย &lt;code&gt;docker compose config&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  จับคู่ docker run ↔ compose
&lt;/h3&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%2F3mrz65b0g9dhajdirva6.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%2F3mrz65b0g9dhajdirva6.png" alt="run vs compose" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;docker run&lt;/th&gt;
&lt;th&gt;compose.yaml&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--name upro_api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;container_name:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ชื่อ image&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;image:&lt;/code&gt; หรือ &lt;code&gt;build: .&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-p 80:80&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ports:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-e KEY=value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;environment:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--env-file .env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;env_file: .env&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v ชื่อ:/path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;volumes:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--network internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;networks:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--restart always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;restart:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(ไม่มี ต้องจำเอง)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;depends_on:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--health-cmd …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;healthcheck:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  เรื่องที่ต้องเข้าใจให้ขาด
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ชื่อ service = hostname&lt;/strong&gt; → &lt;code&gt;DATABASE_URL=postgresql://postgres:password@postgres:5432/pos_db&lt;/code&gt; (คำว่า &lt;code&gt;postgres&lt;/code&gt; หลัง &lt;code&gt;@&lt;/code&gt; คือชื่อ service ไม่ใช่ localhost)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ports&lt;/code&gt; = เปิดให้ "ข้างนอก" เข้า&lt;/strong&gt; กล่องที่คุยกันเองใน network ไม่ต้องเปิด ยิ่งเปิดน้อยยิ่งปลอดภัย&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;volume 2 แบบ&lt;/strong&gt;: named volume (&lt;code&gt;postgres_data:/var/lib/...&lt;/code&gt;) ให้ Docker ดูแล เหมาะกับ DB / bind mount (&lt;code&gt;./nginx/default.conf:/etc/...&lt;/code&gt;) เสียบไฟล์จากเครื่องเรา เหมาะกับ config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;restart&lt;/strong&gt;: &lt;code&gt;no&lt;/code&gt; · &lt;code&gt;always&lt;/code&gt; · &lt;code&gt;unless-stopped&lt;/code&gt; (ลุกเสมอ ยกเว้นเราสั่ง stop เอง) · &lt;code&gt;on-failure&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;depends_on&lt;/code&gt; บอกแค่ลำดับ ไม่ได้รอให้พร้อมจริง&lt;/strong&gt; ต้องใช้คู่กับ &lt;code&gt;healthcheck&lt;/code&gt; + &lt;code&gt;condition: service_healthy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;รหัสผ่านไม่ควรอยู่ในไฟล์ compose&lt;/strong&gt; ย้ายไป &lt;code&gt;.env&lt;/code&gt; แล้วใส่ &lt;code&gt;.env&lt;/code&gt; ใน &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  9. ตัวอย่าง: PostgreSQL (สำหรับคนที่รัน DB เอง)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ถ้าใช้ DB ภายนอกอยู่แล้ว (Supabase, Neon, AWS RDS) &lt;strong&gt;ข้ามไฟล์นี้ได้เลย&lt;/strong&gt; แค่ใส่ &lt;code&gt;DATABASE_URL&lt;/code&gt; ใน &lt;code&gt;.env&lt;/code&gt; ของ API&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&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="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;            &lt;span class="c1"&gt;# ระบุเวอร์ชันเสมอ อย่าใช้ latest กับ DB&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pos_postgres&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;               &lt;span class="c1"&gt;# DB ต้องอยู่ตลอด&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt; &lt;span class="c1"&gt;# ของจริงย้ายไป .env → ${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pos_db&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:5432:5432"&lt;/span&gt;     &lt;span class="c1"&gt;# ต่อได้เฉพาะจากเครื่องตัวเอง&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;   &lt;span class="c1"&gt;# บรรทัดนี้คือชีวิต!&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pos_db"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&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="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&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%2Fxgp3lm9su06rqsvah9r1.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%2Fxgp3lm9su06rqsvah9r1.png" alt="healthcheck" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ ค่า &lt;code&gt;POSTGRES_*&lt;/code&gt; ใช้ &lt;strong&gt;แค่ตอนสร้างครั้งแรก&lt;/strong&gt; ถ้า volume มีข้อมูลแล้ว เปลี่ยนตรงนี้จะไม่มีผล ต้องไปแก้ใน database เอง&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  10. ตัวอย่าง: API + Nginx (ไฟล์จริงที่ใช้ deploy)
&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%2Fmw37hs9qcl78adsmfo7w.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%2Fmw37hs9qcl78adsmfo7w.png" alt="compose ของจริง" width="800" height="429"&gt;&lt;/a&gt;&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="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/reeriicode/upro-pos-api:latest&lt;/span&gt;   &lt;span class="c1"&gt;# ดึง image สำเร็จจาก registry&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upro_api&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;                                  &lt;span class="c1"&gt;# ค่าลับอยู่บน server&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uploads_data:/app/uploads&lt;/span&gt;                   &lt;span class="c1"&gt;# รูปที่อัปโหลดไม่หาย&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="c1"&gt;# ไม่มี ports — ตั้งใจ! ข้างนอกต้องผ่าน Nginx เท่านั้น&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upro_nginx&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;                                     &lt;span class="c1"&gt;# กล่องเดียวที่เปิดออกโลก&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;uploads_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ทำไมเขียนแบบนี้?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;image:&lt;/code&gt; แทน &lt;code&gt;build: .&lt;/code&gt;&lt;/strong&gt; — server ไม่ต้องมีโค้ด ไม่ต้องลง Node ไม่ต้องเสียแรง build แค่ดึงแม่พิมพ์มารัน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt;&lt;/strong&gt; — ล้มก็ลุก reboot ก็ลุก แต่ถ้าเราตั้งใจ stop จะไม่ดื้อลุกเอง&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;uploads_data&lt;/code&gt;&lt;/strong&gt; — รูปสินค้าอยู่ในตู้เซฟ อัปเดตกี่รอบก็ไม่หาย (ปัญหารูปหายใน EP แรก จบตรงนี้)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;api ไม่มี &lt;code&gt;ports&lt;/code&gt;&lt;/strong&gt; — API ปลอดภัยขึ้น&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;:ro&lt;/code&gt;&lt;/strong&gt; — กล่องแก้ไฟล์ config เราไม่ได้ (read-only)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  nginx/default.conf
&lt;/h3&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%2Fab4ayltmmrn6xpjc5b87.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%2Fab4ayltmmrn6xpjc5b87.png" alt="nginx conf" width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;10M&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;# อัปโหลดได้ถึง 10MB&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://api:5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;# api = ชื่อ service ใน compose&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;เห็น &lt;code&gt;http://api:5000&lt;/code&gt; ไหม? นี่แหละ &lt;strong&gt;ชื่อ service ใช้แทน IP&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  อยากรวม PostgreSQL ไว้ในไฟล์เดียวกัน
&lt;/h3&gt;

&lt;p&gt;แยกไฟล์ compose = คนละ network เรียกชื่อกันไม่เจอ วิธีง่ายสุดคือรวมไฟล์เดียว แล้วให้ API &lt;strong&gt;รอจน DB พร้อมจริง&lt;/strong&gt;&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;   &lt;span class="c1"&gt;# รอจน pg_isready ผ่านก่อน&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy ทั้งวงจร
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ── ที่เครื่องเรา ──&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; ghcr.io/reeriicode/upro-pos-api:latest &lt;span class="nb"&gt;.&lt;/span&gt;
docker push ghcr.io/reeriicode/upro-pos-api:latest

&lt;span class="c"&gt;# ── ที่ server ── (มีแค่ compose.yaml, .env, nginx/default.conf)&lt;/span&gt;
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🍎 เครื่องเป็น Mac ชิป M แต่ server เป็น Linux x86 → &lt;code&gt;docker build --platform linux/amd64 ...&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  11. Nginx ต้องมีไหม?
&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%2Fl2bux73zb1sxpp7tqy2w.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%2Fl2bux73zb1sxpp7tqy2w.png" alt="Nginx ต้องมีไหม" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;สถานการณ์&lt;/th&gt;
&lt;th&gt;ต้องมี Nginx ไหม&lt;/th&gt;
&lt;th&gt;กล่อง API เปิด ports ไหม&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;พัฒนาในเครื่อง&lt;/td&gt;
&lt;td&gt;❌ ไม่ต้อง&lt;/td&gt;
&lt;td&gt;เปิด &lt;code&gt;5000:5000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deploy ขึ้น VPS / EC2&lt;/td&gt;
&lt;td&gt;✅ แนะนำ&lt;/td&gt;
&lt;td&gt;ไม่เปิด ให้ Nginx ส่งต่อ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run / แพลตฟอร์มที่มี proxy&lt;/td&gt;
&lt;td&gt;❌ ไม่ต้อง&lt;/td&gt;
&lt;td&gt;ตามแพลตฟอร์ม&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ตอน deploy Nginx ช่วย: ซ่อน API ไว้ข้างหลัง · ทำ HTTPS ที่เดียว · จำกัดขนาดไฟล์อัปโหลด · เสิร์ฟไฟล์ static เร็ว · หลายแอปบน server เดียว (&lt;code&gt;api.example.com&lt;/code&gt;, &lt;code&gt;admin.example.com&lt;/code&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  12. มือใหม่ชอบพลาดตรงไหน?
&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%2Fr6p47p0zmtwg0dyx9oan.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%2Fr6p47p0zmtwg0dyx9oan.png" alt="มือใหม่ชอบพลาด" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ลืม &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/strong&gt; → &lt;code&gt;node_modules&lt;/code&gt; ติดเข้ากล่อง build ช้า/พัง&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ยัดรหัสผ่านไว้ใน image&lt;/strong&gt; → ห้ามเด็ดขาด! ส่งผ่าน &lt;code&gt;env_file&lt;/code&gt; ตอนรัน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ใช้ &lt;code&gt;localhost&lt;/code&gt; ในกล่อง&lt;/strong&gt; → localhost = ตัวกล่องเอง ต้องเรียกชื่อ service (&lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;api&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB ไม่ใช้ volume&lt;/strong&gt; → ลบกล่องทีเดียว ข้อมูลหายทั้งก้อน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker compose down -v&lt;/code&gt;&lt;/strong&gt; → &lt;code&gt;-v&lt;/code&gt; ลบ volume ด้วย ข้อมูลหายหมด กดเล่นไม่ได้!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;เปิด ports ทุกกล่อง&lt;/strong&gt; → DB เปิด 5432 ให้ทั้งโลก = เชิญมาเดารหัส&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;แก้ env ของ PostgreSQL แล้วไม่เปลี่ยน&lt;/strong&gt; → ใช้แค่ตอนสร้างครั้งแรก&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;build บน Mac ชิป M แล้ว server รันไม่ได้&lt;/strong&gt; → ใส่ &lt;code&gt;--platform linux/amd64&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YAML ย่อหน้าเพี้ยน&lt;/strong&gt; → เช็คด้วย &lt;code&gt;docker compose config&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&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%2Fo1u00g8lj1q52tij7ebp.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%2Fo1u00g8lj1q52tij7ebp.png" alt="พลาดอีก 4 ข้อ" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  13. สรุป
&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%2Ffn6ccwzxv1hgcutl11hz.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%2Ffn6ccwzxv1hgcutl11hz.png" alt="สรุป" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ปัญหา &lt;strong&gt;"ในเครื่องเรารันได้ ที่อื่นพัง"&lt;/strong&gt; เพราะสภาพแวดล้อมไม่เหมือนกัน&lt;/li&gt;
&lt;li&gt;แก้ด้วย &lt;strong&gt;Docker&lt;/strong&gt; แพ็กแอปพร้อมทุกอย่างลงกล่อง ไปรันที่ไหนก็เหมือนกัน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfile&lt;/strong&gt; สูตร → &lt;strong&gt;Image&lt;/strong&gt; แม่พิมพ์ → &lt;strong&gt;Container&lt;/strong&gt; ของจริงที่รัน + &lt;strong&gt;Volume&lt;/strong&gt; ตู้เซฟ + &lt;strong&gt;Network&lt;/strong&gt; สายโทรศัพท์&lt;/li&gt;
&lt;li&gt;Image สำเร็จรูป (PostgreSQL, Nginx) ดึงมาใช้ได้เลย ส่วน &lt;strong&gt;API ของเราต้องเขียน Dockerfile แล้ว build เอง&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;หลายกล่องพิมพ์ยาวเหยียด → &lt;strong&gt;Docker Compose&lt;/strong&gt; ไฟล์เดียว คำสั่งเดียว&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx&lt;/strong&gt;: ตอนพัฒนาไม่ต้องมี ตอน deploy แนะนำให้มี&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EP หน้า มาลองจับ API ของเรายัดลงกล่องกันจริงๆ deploy รอบหน้าไม่ต้องนั่งลง Node ใหม่อีกแล้ว ฮ่าๆ 🐳📦&lt;/p&gt;




&lt;p&gt;&lt;em&gt;DEPLOY 101 — สอน deploy แบบภาษาคน · ติดตามตอนต่อไปได้ที่ &lt;a href="https://dev.to/reeriicode"&gt;dev.to/reeriicode&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>beginners</category>
      <category>thai</category>
    </item>
    <item>
      <title>เลิกให้ API นอนแก้ผ้ารอหน้าบ้าน! ซ่อนหลัง Nginx ซ่ะ</title>
      <dc:creator>reerii code</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:12:05 +0000</pubDate>
      <link>https://dev.to/reeriicode/tidtang-nginx-eelatham-reverse-proxy-ain-aws-3nnf</link>
      <guid>https://dev.to/reeriicode/tidtang-nginx-eelatham-reverse-proxy-ain-aws-3nnf</guid>
      <description>&lt;p&gt;บทความนี้เราจะมาทำ Reverse Proxy ด้วย Nginx กัน&lt;/p&gt;

&lt;p&gt;โปรเจกต์ของผม frontend deploy ด้วย Vercel ได้ &lt;a href="https://uprodd.vercel.app" rel="noopener noreferrer"&gt;https://uprodd.vercel.app&lt;/a&gt; อันนี้ง่าย จบแล้ว &lt;a href="https://dev.to/reeriicode/deploy-web-eebb-paas-dwy-vercel-free-3loe"&gt;อ่าน deploy FE&lt;/a&gt; ส่วน backend deploy บน AWS EC2 แบบ VPS &lt;a href="https://dev.to/reeriicode/deploy-api-eebb-vps-odyaich-aws-ec2free-express-prisma-google-login-superbase-45j1"&gt;อ่าน deploy BE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;สิ่งที่ผมได้คือ &lt;code&gt;http://เลขไอพี:5000&lt;/code&gt; ซึ่งก็คือ API ของผมนั่นเอง ผมต้องเปิดพอร์ต 5000 ให้ frontend เข้ามาเรียกได้ ซึ่งไม่ค่อยแนะนำเท่าไหร่&lt;/p&gt;

&lt;p&gt;สิ่งที่ผมจะทำคือให้เรียก API ผ่าน &lt;code&gt;http://เลขไอพี/&lt;/code&gt; แทน ปกติแล้ว &lt;code&gt;/&lt;/code&gt; ควรเป็นหน้าเว็บ และ &lt;code&gt;/api&lt;/code&gt; ควรเป็น API เผื่ออนาคตจะ deploy frontend ไว้ที่ server เดียวกัน แต่ในบทความนี้ผมจะให้ &lt;code&gt;/&lt;/code&gt; เป็นของ API ไปก่อน&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไมน่ะเหรอ? ก็เพราะมันทำได้ไงล่ะ hahaha
&lt;/h2&gt;

&lt;p&gt;อันนี้คือจะให้เห็นว่า Nginx สามารถกำหนดให้ชี้ไปไหนก็ได้&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Login เข้า server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"ชื่อไฟล์.pem"&lt;/span&gt; ec2-user@เลขไอพี
&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%2Fo6ehjcno04jmx1y0gfqy.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%2Fo6ehjcno04jmx1y0gfqy.png" alt=" " width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. เช็กเวอร์ชัน Linux
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/os-release | &lt;span class="nb"&gt;grep &lt;/span&gt;PRETTY
&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%2Fbnk6z2lzzq4rv6tn68um.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%2Fbnk6z2lzzq4rv6tn68um.png" alt=" " width="798" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ของผมเป็น Amazon Linux 2023 ซึ่งใช้ &lt;code&gt;dnf&lt;/code&gt; ในการติดตั้งโปรแกรม&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ติดตั้ง Nginx
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx
&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%2F8u2eauduewg7g7lovt4o.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%2F8u2eauduewg7g7lovt4o.png" alt=" " width="800" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nginx จะติดตั้งตัวโปรแกรมและสร้างโฟลเดอร์ตั้งค่าไว้ที่ &lt;code&gt;/etc/nginx/&lt;/code&gt; ให้เอง&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;เปิดใช้งาน Nginx ทันที และตั้งให้ทำงานเองทุกครั้งที่เปิดเครื่อง&lt;/p&gt;

&lt;h2&gt;
  
  
  4. สร้างไฟล์ตั้งค่า
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;sudo&lt;/span&gt; &lt;span class="s"&gt;tee&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/conf.d/upro-api.conf&lt;/span&gt; &lt;span class="s"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;/dev/null&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;'EOF'&lt;/span&gt;
&lt;span class="c1"&gt;# จำกัดจำนวน request กันยิงถล่ม&lt;/span&gt;
&lt;span class="s"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api_limit:10m&lt;/span&gt; &lt;span class="s"&gt;rate=20r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="s"&gt;default_server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;ใส่IPเรา&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;# ซ่อนเวอร์ชัน Nginx&lt;/span&gt;
    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;# จำกัดขนาด upload&lt;/span&gt;

    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"DENY"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"no-referrer"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# กันบอทที่สแกนหา .env .git&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api_limit&lt;/span&gt; &lt;span class="s"&gt;burst=40&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;แบบง่าย ๆ คำสั่งนี้คือ &lt;strong&gt;สร้างไฟล์ &lt;code&gt;upro-api.conf&lt;/code&gt;&lt;/strong&gt; (ตั้งชื่ออะไรก็ได้ ขอให้ลงท้ายด้วย &lt;code&gt;.conf&lt;/code&gt;) ไว้ในโฟลเดอร์ &lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt; ที่ Nginx สร้างไว้ให้ โดยเอาข้อความทั้งหมดจนถึงคำว่า &lt;code&gt;EOF&lt;/code&gt; ไปเป็นเนื้อหาไฟล์&lt;/p&gt;

&lt;p&gt;ให้นึกภาพว่า Nginx เป็น &lt;strong&gt;ยามหน้าบ้าน&lt;/strong&gt; ส่วน API ที่พอร์ต 5000 เป็น &lt;strong&gt;เจ้าของบ้านที่อยู่ข้างใน&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ข้างล่างนี้คือเนื้อหาไฟล์เดียวกัน แต่ใส่คำอธิบายไว้ทุกบรรทัด เอาไว้อ่านทำความเข้าใจ ตอนสร้างจริงใช้ฉบับข้างบนก็พอ&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ── กฎกลาง: ตั้งโควตาก่อน ──&lt;/span&gt;
&lt;span class="c1"&gt;# จำว่าแต่ละ IP ยิงมากี่ครั้ง ให้ได้ไม่เกิน 20 ครั้งต่อวินาที&lt;/span&gt;
&lt;span class="c1"&gt;# จองหน่วยความจำ 10 MB ไว้จด (จดได้ราวแสนกว่า IP)&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api_limit:10m&lt;/span&gt; &lt;span class="s"&gt;rate=20r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# ยามยืนเฝ้าประตูพอร์ต 80 (HTTP)&lt;/span&gt;
    &lt;span class="c1"&gt;# default_server = ถ้าคำขอไม่ตรงกับ server อื่น ให้มาที่นี่&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="s"&gt;default_server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# ใช้กับคนที่เข้ามาทาง IP นี้ (ยังไม่มีโดเมน)&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;ใส่IPเรา&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# ไม่บอกคนนอกว่าใช้ Nginx เวอร์ชันไหน คนร้ายจะได้หาช่องโหว่ยากขึ้น&lt;/span&gt;
    &lt;span class="kn"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# อัปโหลดไฟล์ได้ไม่เกิน 10 MB ต่อครั้ง เกินนี้ไม่รับ&lt;/span&gt;
    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# ── ส่งคำเตือนความปลอดภัยไปให้เบราว์เซอร์ ──&lt;/span&gt;
    &lt;span class="c1"&gt;# ห้ามเบราว์เซอร์เดาชนิดไฟล์เอง (กันไฟล์ปลอมตัว)&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ห้ามเว็บอื่นเอาหน้าเราไปฝังใน iframe (กันหลอกให้กด)&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"DENY"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# เวลากดลิงก์ออกไปเว็บอื่น ไม่ต้องบอกว่ามาจากหน้าไหน&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"no-referrer"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# ── ด่านที่ 1: ไล่บอทที่มาหาไฟล์ลับ ──&lt;/span&gt;
    &lt;span class="c1"&gt;# ใครขอ path ที่มีจุดนำหน้า เช่น /.env หรือ /.git&lt;/span&gt;
    &lt;span class="c1"&gt;# ให้ตอบว่า "ไม่มีหน้านี้" (404)&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# ── ด่านที่ 2: คำขอทั่วไปทั้งหมด ──&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# ใช้โควตา 20 ครั้งต่อวินาทีที่ตั้งไว้ข้างบน&lt;/span&gt;
        &lt;span class="c1"&gt;# ยอมให้พุ่งเกินได้อีก 40 ครั้ง (เช่นตอนเปิดหน้าเว็บที่เรียกหลายอย่างพร้อมกัน)&lt;/span&gt;
        &lt;span class="c1"&gt;# เกินจากนั้นปฏิเสธทันที&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api_limit&lt;/span&gt; &lt;span class="s"&gt;burst=40&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# ★ หัวใจของ Reverse Proxy ★&lt;/span&gt;
        &lt;span class="c1"&gt;# ส่งคำขอต่อไปให้ API ที่อยู่ในเครื่องเดียวกัน พอร์ต 5000&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# คุยกับ API ด้วย HTTP เวอร์ชัน 1.1&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# ── แนบข้อมูลของผู้ใช้จริงไปให้ API ──&lt;/span&gt;
        &lt;span class="c1"&gt;# (ไม่งั้น API จะเห็นทุกคนเป็น "ยาม" คือ 127.0.0.1 หมด)&lt;/span&gt;
        &lt;span class="c1"&gt;# ผู้ใช้พิมพ์ชื่อเว็บอะไรเข้ามา&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;# IP จริงของผู้ใช้&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;# เส้นทาง IP ที่ผ่านมาทั้งหมด&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;# ผู้ใช้เข้ามาด้วย http หรือ https&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# ── เวลารอ ──&lt;/span&gt;
        &lt;span class="c1"&gt;# ต่อหา API ไม่ติดภายใน 10 วินาที ให้ยกเลิก&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;# รอ API ตอบได้นานสุด 60 วินาที&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;จะเห็นว่าเรากำหนดได้ทั้งว่าจะชี้ไปทางไหน และตั้งกฎให้คนที่เข้ามาเรียก API ของเราได้ด้วย&lt;/p&gt;

&lt;h2&gt;
  
  
  5. ตรวจไฟล์ แล้วสั่งให้ Nginx ใช้ค่าใหม่
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nginx -t&lt;/code&gt; เช็กว่าไฟล์ตั้งค่าเขียนถูกไหม ถ้าผ่านค่อย &lt;code&gt;reload&lt;/code&gt; ให้ Nginx อ่านค่าใหม่ โดยไม่ต้องปิดเว็บ&lt;/p&gt;

&lt;h2&gt;
  
  
  6. ทดสอบ
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://localhost/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ลองยิงเข้า &lt;code&gt;/&lt;/code&gt; ที่เครื่องตัวเอง คำขอจะผ่าน Nginx แล้วถูกส่งต่อไปที่ API พอร์ต 5000&lt;/p&gt;

&lt;p&gt;ผลที่ได้&lt;br&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%2F38zhzwxm822l1u63ssh7.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%2F38zhzwxm822l1u63ssh7.png" alt=" " width="799" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;API ของเราใช้งานผ่าน Nginx ได้แล้ว&lt;/p&gt;

&lt;h2&gt;
  
  
  7. ปิดพอร์ต 5000
&lt;/h2&gt;

&lt;p&gt;ตอนนี้พอร์ต 5000 ที่เคยเปิดไว้ต้องปิดได้แล้ว เพราะทุกคนต้องเข้าผ่านพอร์ต 80/443 เท่านั้น ถึงตอนนี้จะยังไม่ได้จดโดเมนก็ตาม แต่ใน EP นี้อยากให้เห็นการตั้งค่า Nginx แบบง่าย ๆ ก่อน งั้นไปปิดพอร์ตกัน&lt;/p&gt;

&lt;p&gt;ไปที่ AWS → Security Group&lt;br&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%2Fg3jj5pz7181h4fviwqa5.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%2Fg3jj5pz7181h4fviwqa5.png" alt=" " width="799" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;กดที่ Security Group ID ของเรา&lt;br&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%2Fwgvhc7t1ndmaihiqbflo.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%2Fwgvhc7t1ndmaihiqbflo.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ไปที่ Edit inbound rules&lt;br&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%2Fu7l1r48la9e5dliwk9ys.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%2Fu7l1r48la9e5dliwk9ys.png" alt=" " width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;แก้ rule ที่เคยเป็นพอร์ต 5000 ให้เป็นพอร์ต 80 ตามรูป ตอนนี้ที่เปิดไว้คือ&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;22 สำหรับ SSH&lt;/li&gt;
&lt;li&gt;80 สำหรับ HTTP&lt;/li&gt;
&lt;li&gt;443 สำหรับ HTTPS (เปิดเผื่อไว้ตอนมีโดเมน)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;แล้วกด Save&lt;br&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%2Fsklvylyr0d2owf5upr67.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%2Fsklvylyr0d2owf5upr67.png" alt=" " width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ปิดแค่ที่ Security Group นะ ตัว API ยังรันอยู่ที่พอร์ต 5000 เหมือนเดิม แค่คนข้างนอกเข้าตรงไม่ได้แล้ว แต่ Nginx ที่อยู่เครื่องเดียวกันยังส่งต่อไปได้ปกติ&lt;/p&gt;

&lt;p&gt;ตอนนี้เข้าแบบ &lt;code&gt;:5000&lt;/code&gt; ไม่ได้แล้ว ต้องเรียก &lt;code&gt;http://เลขไอพี/&lt;/code&gt; แทน อย่าลืมไปแก้ URL ของ API ฝั่ง frontend ให้ตัด &lt;code&gt;:5000&lt;/code&gt; ออกด้วย&lt;br&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%2Ffmamndv0fp713n8gsan7.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%2Ffmamndv0fp713n8gsan7.png" alt=" " width="799" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;หน้าเว็บดึงข้อมูลได้ปกติ แค่นี้ก็เรียบร้อยแล้ว ได้โดเมนเมื่อไหร่เดี๋ยวมาทำ HTTPS ต่อนะครับ&lt;/p&gt;

</description>
      <category>aws</category>
      <category>backend</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>จัดการ Server เองอาจจะโดนซอยตูดได้ เพราะงันต้องทำ Nginx ด้วยน่ะจ่ะ ทำไงมาดูกัน</title>
      <dc:creator>reerii code</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:52:12 +0000</pubDate>
      <link>https://dev.to/reeriicode/cchadkaar-server-engaacchcchaodnchytuudaid-ephraaangantngtham-nginx-dwynaccha-thamaingmaaduukan-2b23</link>
      <guid>https://dev.to/reeriicode/cchadkaar-server-engaacchcchaodnchytuudaid-ephraaangantngtham-nginx-dwynaccha-thamaingmaaduukan-2b23</guid>
      <description>&lt;h2&gt;
  
  
  ปัญหาของการเปิดพอร์ตตรง ๆ
&lt;/h2&gt;

&lt;p&gt;ลองนึกภาพว่าเราทำเว็บเสร็จแล้ว บนเซิร์ฟเวอร์มีโปรแกรม 2 ตัว รันอยู่คนละพอร์ต&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend (หน้าเว็บ) รันที่พอร์ต 3000&lt;/li&gt;
&lt;li&gt;Backend (API) รันที่พอร์ต 8000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;วิธีที่ง่ายที่สุดคือเปิดพอร์ตทั้งสองให้คนนอกเข้าได้ตรง ๆ ผู้ใช้ก็เข้าเว็บได้ทันที แต่ทำแบบนี้ก็เหมือนบ้านที่เปิดประตูทุกห้องออกสู่ถนน โดยไม่มียามเฝ้าสักคน&lt;/p&gt;

&lt;p&gt;ปัญหาที่จะตามมา&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URL จำยาก ดูไม่น่าเชื่อถือ&lt;/strong&gt; ผู้ใช้ต้องพิมพ์ &lt;code&gt;http://203.0.113.10:3000&lt;/code&gt; หรือ &lt;code&gt;http://203.0.113.10:8000&lt;/code&gt; ถึงจะมีโดเมนก็ยังต้องต่อท้ายด้วยพอร์ต เช่น &lt;code&gt;http://example.com:3000&lt;/code&gt; แถมยังไม่มี HTTPS เบราว์เซอร์จะขึ้นเตือนว่า "ไม่ปลอดภัย"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;โดนบอตสแกนพอร์ต&lt;/strong&gt; เซิร์ฟเวอร์ที่เพิ่งเปิดอาจโดนสแกนภายในไม่กี่นาที ยิ่งเปิดพอร์ตไว้มาก ก็ยิ่งมีประตูให้ลองงัดมาก&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ไม่มีการจำกัดจำนวน request (Rate Limit)&lt;/strong&gt; ใครจะยิงเข้ามาเป็นหมื่นครั้ง หรือเดารหัสผ่านซ้ำไปเรื่อย ๆ ก็ทำได้ จนเซิร์ฟเวอร์รับไม่ไหว&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;หน้าผู้ดูแลระบบเปิดโล่ง&lt;/strong&gt; path อย่าง &lt;code&gt;/api/admin/users&lt;/code&gt; ใครรู้ URL ก็ลองเข้าได้ ไม่มีด่านกั้นก่อนถึงตัว API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;อัปโหลดไฟล์ใหญ่แค่ไหนก็ได้&lt;/strong&gt; ถ้าไม่จำกัดขนาด มีคนส่งไฟล์ใหญ่ ๆ เข้ามาไม่กี่ครั้ง ดิสก์หรือหน่วยความจำก็เต็มได้&lt;/li&gt;
&lt;/ul&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%2Fer2tr1kvk7phwxp9cv3x.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%2Fer2tr1kvk7phwxp9cv3x.png" alt=" " width="460" height="180"&gt;&lt;/a&gt;&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%2Fbprehg3crgwiah04ko33.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%2Fbprehg3crgwiah04ko33.png" alt=" " width="467" height="197"&gt;&lt;/a&gt;&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%2Fhghcu4bh9zfwwb5ave7x.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%2Fhghcu4bh9zfwwb5ave7x.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  แก้ปัญหาเหล่านี้ด้วย Nginx
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nginx คืออะไร?&lt;/strong&gt;&lt;br&gt;
Nginx คือโปรแกรมเว็บเซิร์ฟเวอร์ที่ติดตั้งบนเครื่องเซิร์ฟเวอร์ คอยรับคำขอ (request) จากเบราว์เซอร์ แล้วส่งข้อมูลกลับไป (response) ผ่าน HTTP หรือ HTTPS&lt;/p&gt;

&lt;p&gt;จริง ๆ แล้ว Nginx ทำได้หลายหน้าที่&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web Server&lt;/strong&gt; เสิร์ฟไฟล์หน้าเว็บ (HTML, CSS, JS, รูปภาพ) ให้ผู้ใช้โดยตรง&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse Proxy&lt;/strong&gt; ยืนอยู่หน้าแอปของเรา รับคำขอแทน แล้วส่งต่อไปยังแอปที่รันอยู่ข้างใน เช่น API ที่ &lt;code&gt;localhost:8000&lt;/code&gt; พอได้ผลลัพธ์ก็ส่งกลับให้ผู้ใช้

&lt;ul&gt;
&lt;li&gt;รวมหลายบริการไว้ใต้โดเมนเดียวได้ เช่น &lt;code&gt;/api&lt;/code&gt; ไปที่ API ส่วน &lt;code&gt;/&lt;/code&gt; ไปที่หน้าเว็บ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancer&lt;/strong&gt; แบ่งคำขอไปให้เซิร์ฟเวอร์หลายตัวที่ทำงานเหมือนกัน จะได้ไม่มีตัวไหนรับงานหนักเกินไป วิธีแบ่งที่ใช้บ่อย

&lt;ul&gt;
&lt;li&gt;Round Robin: แจกวนไปทีละตัวตามลำดับ&lt;/li&gt;
&lt;li&gt;Least Connections: ส่งให้ตัวที่มีงานค้างน้อยที่สุด&lt;/li&gt;
&lt;li&gt;IP Hash: ผู้ใช้คนเดิมได้ไปที่ตัวเดิมทุกครั้ง&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;อื่น ๆ&lt;/strong&gt; เช่น ทำ HTTPS, จำกัดจำนวน request, จำกัดขนาดไฟล์อัปโหลด, บล็อกบาง path หรือบาง IP และเก็บ cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;แบบเดิม (เปิดพอร์ตตรง)&lt;br&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%2Fjmcoyrsg4pbdgzn8ougc.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%2Fjmcoyrsg4pbdgzn8ougc.png" alt=" " width="335" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;แต่ถ้ามี Nginx คั่นกลาง&lt;br&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%2Fo8je6tpb2v52ox4pe3vt.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%2Fo8je6tpb2v52ox4pe3vt.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;จะเห็นว่าผู้ใช้เข้ามาทางประตูเดียว (พอร์ต 80/443) แล้ว Nginx จะพาไปที่ไหนก็ได้ตามที่เราตั้งไว้ ถ้ามีโดเมนเดียวก็แบ่งตาม path&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;example.com/&lt;/code&gt; → &lt;code&gt;localhost:3000&lt;/code&gt; (หน้าเว็บ)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;example.com/api&lt;/code&gt; → &lt;code&gt;localhost:8000&lt;/code&gt; (API)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ถ้ามีหลายโดเมนหรือ subdomain ก็แยกตามชื่อได้ เช่น&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;n8n.example.com&lt;/code&gt; → &lt;code&gt;localhost:5678&lt;/code&gt; (n8n)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;จากนั้นใช้ firewall (เช่น ufw) ปิดพอร์ต 3000, 8000, 5678 ไม่ให้คนนอกเข้าตรง ๆ เหลือเปิดแค่ 80 กับ 443 ทุกคำขอจึงต้องผ่าน Nginx ก่อนเสมอ&lt;/p&gt;

&lt;h2&gt;
  
  
  ถึงตรงนี้หลายคนอาจสงสัยว่า ทำไม Nginx ต้องเปิดถึง 2 พอร์ต?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;พอร์ต 80&lt;/strong&gt; ใช้กับ HTTP (ไม่เข้ารหัส)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;พอร์ต 443&lt;/strong&gt; ใช้กับ HTTPS (เข้ารหัส มีรูปแม่กุญแจ)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;การจะใช้ HTTPS ได้ต้องมีใบรับรอง SSL ซึ่งใบฟรีอย่าง Let's Encrypt ปกติจะออกให้กับโดเมน ถ้ายังไม่มีโดเมนก็ใช้ HTTP ผ่านพอร์ต 80 ไปก่อน&lt;/p&gt;

&lt;p&gt;พอมีโดเมนและได้ใบรับรองแล้ว ก็ตั้งค่าให้ใครที่เข้ามาทาง HTTP (พอร์ต 80) ถูกส่งเตะไปที่ HTTPS (พอร์ต 443) ทันที แต่ยังต้องเปิดพอร์ต 80 ไว้นะ เพราะต้องใช้รับคนที่พิมพ์ &lt;code&gt;http://&lt;/code&gt; เข้ามา ก็ใช้พอร์ต 80 ตรวจสอบโดเมนตอนต่ออายุใบรับรองด้วย&lt;/p&gt;

&lt;p&gt;สรุปคือ ทุกคนต้องเข้ามาทาง 80/443 เท่านั้น แล้วให้ Nginx เป็นด่านตรวจคำขอตามกฎที่เราตั้งไว้ เช่น จำกัดจำนวน request จำกัดขนาดไฟล์ หรือบล็อกบาง path&lt;/p&gt;

&lt;p&gt;ส่วนพอร์ต 3000, 8000, 5678 ที่บอกว่า "ปิด" ไม่ได้แปลว่าปิดแอปนะ แค่ปิดที่ firewall ไม่ให้คนข้างนอกเข้าตรง ๆ ได้ แต่ในเครื่องเดียวกันยังใช้งานได้ปกติ Nginx จึงยังส่งต่อไปที่ &lt;code&gt;localhost:3000&lt;/code&gt; ได้เหมือนเดิม&lt;/p&gt;

&lt;p&gt;ในบทความนี้จะเน้นเรื่อง Reverse Proxy เป็นหลักน่ะ บางคนอาจไม่เห็นภาพ เรามาทำ Reverse Proxy ง่ายๆ โดยยังไม่มี Domain น่ะ hahaha ว่าทำยังไง จะได้เห็นภาพมากขึ้น อ่านต่อ&lt;a href="https://dev.to/reeriicode/tidtang-nginx-eelatham-reverse-proxy-ain-aws-3nnf"&gt;เลิกให้ API นอนแก้ผ้ารอหน้าบ้าน! ซ่อนหลัง Nginx ซ่ะ&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nginx</category>
      <category>linux</category>
    </item>
    <item>
      <title>Deploy API เเบบ VPS โดยใช้ AWS EC2(free) Express, Prisma</title>
      <dc:creator>reerii code</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:01:41 +0000</pubDate>
      <link>https://dev.to/reeriicode/deploy-api-eebb-vps-odyaich-aws-ec2free-express-prisma-google-login-superbase-45j1</link>
      <guid>https://dev.to/reeriicode/deploy-api-eebb-vps-odyaich-aws-ec2free-express-prisma-google-login-superbase-45j1</guid>
      <description>&lt;p&gt;หลังจากที่เราได้เข้าใจการ deploy คร่าว ๆ แล้ว จาก &lt;a href="https://dev.to/reeriicode/deploy-khuueaair--2a9p"&gt;deploy คืออะไร?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;เรามาลองทำจริงกันเลยดีกว่า ก่อนอื่นเราจะใช้เจ้าไหนดี อันนี้แล้วแต่ชอบเลย มีทั้ง Oracle Cloud, Google Cloud, AWS&lt;/p&gt;

&lt;p&gt;แน่นอนผมต้องใช้ของฟรีอยู่แล้ว เลือกมาหนึ่งเจ้า ผมเลือก AWS ละกัน ต้องบอกก่อนว่าเป้าหมายของเราคือ ต้องการเครื่อง server หรือคอมพิวเตอร์ 1 เครื่องที่อยู่บน cloud ซึ่งจะได้เลข IP Public มา นั่นคือที่อยู่ของ server เรา และนั่นแหละคือสิ่งที่เราต้องการ&lt;/p&gt;

&lt;p&gt;ไปที่เว็บ AWS&lt;br&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%2Fe73k5g2uy4jfguw5fbzc.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%2Fe73k5g2uy4jfguw5fbzc.png" alt=" " width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ก่อนอื่นต้องสมัครก่อนเว้ย&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%2F3xpun0xgvgksbqts0xfc.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%2F3xpun0xgvgksbqts0xfc.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;สิ่งที่เขาให้เราคือเครดิต 100 ดอลลาร์ตอนสมัคร และได้เพิ่มอีกสูงสุด 100 ดอลลาร์เมื่อลองใช้บริการพื้นฐานของ AWS (ไปอ่านเพิ่มเติมได้นะ) หลังจากนั้นก็ยืนยัน Gmail ให้เรียบร้อย แล้วก็ใส่เลขบัตรให้เรียบร้อยด้วย ไม่ต้องกลัวโดนหักเงินนะจ๊ะ&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Free Plan: ไม่มีค่าใช้จ่ายจนกว่าจะเปลี่ยนเป็น Paid Plan แต่ใช้ได้ไม่ครบทุกบริการ ถ้าลืมปิดเครื่อง ค่าใช้จ่ายจะหักจากเครดิต ไม่ใช่บัตรเครดิต ข้อควรระวังคือ AWS จะปิดบัญชีอัตโนมัติเมื่อครบ 6 เดือน หรือเมื่อเครดิตหมด แล้วแต่อย่างไหนถึงก่อน และขยายเกิน 6 เดือนไม่ได้ เว้นแต่จะอัปเกรดเป็น Paid Plan (หลังบัญชีปิด ยังมีเวลา 90 วันให้อัปเกรดเพื่อเปิดบัญชีกลับมาได้)&lt;/em&gt;&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%2Fhreona8bn5kzl6ploy2g.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%2Fhreona8bn5kzl6ploy2g.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;เลือก Free แล้วกด Complete sign up ได้เลย&lt;br&gt;
หลังจากนั้นจะมาที่หน้านี้&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%2F0ks33kha2qwkrdmiv4pk.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%2F0ks33kha2qwkrdmiv4pk.png" alt=" " width="799" height="341"&gt;&lt;/a&gt;&lt;br&gt;
แน่นอนว่าต้องไปที่ Console&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%2F6zgyrn1vlvnn9f60b7ru.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%2F6zgyrn1vlvnn9f60b7ru.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;br&gt;
เปิด sidebar&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%2Fygwrch3aaat9ntifajrm.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%2Fygwrch3aaat9ntifajrm.png" alt=" " width="799" height="330"&gt;&lt;/a&gt;&lt;br&gt;
ไปที่ EC2&lt;/p&gt;
&lt;h2&gt;
  
  
  EC2 คืออะไร?
&lt;/h2&gt;

&lt;p&gt;EC2 (Elastic Compute Cloud) คือบริการเช่าเครื่องเซิร์ฟเวอร์เสมือน (virtual machine) บน AWS เทียบได้กับ VPS ทั่วไป คือได้เครื่อง Linux มา 1 เครื่อง&lt;br&gt;
ไปที่ Instances (instance = เครื่องเซิร์ฟเวอร์หนึ่งเครื่อง)&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%2Fxfxvsdhn3j58az4qfa33.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%2Fxfxvsdhn3j58az4qfa33.png" alt=" " width="800" height="301"&gt;&lt;/a&gt;&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%2Fjebjcidgqq9t2i2h5c47.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%2Fjebjcidgqq9t2i2h5c47.png" alt=" " width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;แน่นอนว่าเราเลือกระบบปฏิบัติการที่ถนัดได้เลย&lt;br&gt;
&lt;em&gt;ปกติก็จะเลือก Ubuntu&lt;/em&gt;&lt;br&gt;
คิดว่าผมจะเลือก Ubuntu ล่ะสิ ไม่! ผมจะเลือก Amazon Linux ทำไมน่ะหรอ ก็เพราะไม่เคยลองไง แค่อยากลองเฉย ๆ hahaha!&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%2Fxzkbk4ke5t5xkndb2uqc.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%2Fxzkbk4ke5t5xkndb2uqc.png" alt=" " width="797" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;เออ! ตามนี้ละกัน ที่จริงไม่ต้องตั้งค่าอะไรเลย ใช้ค่าเริ่มต้นได้ แต่ที่เราต้องสนใจคือ Key pair&lt;/p&gt;
&lt;h2&gt;
  
  
  Key pair คืออะไร?
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Key pair คือ "กุญแจ" สำหรับเข้าเครื่อง instance ผ่าน SSH ใช้แทนรหัสผ่าน มี 2 ชิ้นที่ทำงานคู่กัน&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Public key (กุญแจสาธารณะ): AWS ฝังไว้ในเครื่อง instance ให้ เปรียบเหมือนแม่กุญแจที่ติดไว้หน้าประตู&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Private key (กุญแจส่วนตัว): ไฟล์ที่เราดาวน์โหลดเก็บไว้ เปรียบเหมือนลูกกุญแจ ใครมีไฟล์นี้ก็เข้าเครื่องได้&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;หยุด ๆ ๆ ไม่ต้องอธิบายยาวขนาดนั้นก็ได้ ที่จริงมันคือไฟล์ที่เอาไว้ login แค่มีไฟล์นี้ก็เข้าไปใช้งานเครื่อง server ได้แล้ว (เพราะงั้นห้ามทำหาย และห้ามส่งให้ใครเด็ดขาด)&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%2F1n83z30c6n9pu1nq1ybh.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%2F1n83z30c6n9pu1nq1ybh.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;br&gt;
ตั้งชื่อไฟล์ แล้วเลือก type จะเป็น RSA หรือ ED25519 ก็ได้ ED25519 ใหม่กว่าและสั้นกว่า แต่ RSA รองรับโปรแกรมได้กว้างกว่า ผมเลือกตามนี้ ส่วนนามสกุลไฟล์แล้วแต่เลย (ใช้ CMD/Terminal ให้เลือก .pem ถ้าใช้ PuTTY ให้เลือก .ppk) พอกด Create key pair มันก็จะดาวน์โหลดไฟล์ให้ทันที ง่ายยยยย&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%2Fxhitlfv5izwvdz7swpd6.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%2Fxhitlfv5izwvdz7swpd6.png" alt=" " width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ส่วนอันนี้คือ&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%2F0bll64u7jwbzozzjua0m.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%2F0bll64u7jwbzozzjua0m.png" alt=" " width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Allow SSH traffic from (พอร์ต 22):&lt;/strong&gt; ช่องสำหรับเข้าไปสั่งงานเครื่องผ่าน terminal ต้องติ๊กไว้ ไม่อย่างนั้นเข้าเครื่องไม่ได้นะเว้ย ส่วนช่องข้าง ๆ แนะนำให้เลือก &lt;strong&gt;My IP&lt;/strong&gt; (เฉพาะ IP เราเข้าได้) ถ้าเลือก Anywhere บอตทั่วโลกจะมาสุ่มเจาะพอร์ตนี้ทั้งวัน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow HTTPS traffic from the internet (พอร์ต 443):&lt;/strong&gt; สำหรับเว็บแบบ https:// ใช้ตอนเรามี domain แล้ว&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow HTTP traffic from the internet (พอร์ต 80):&lt;/strong&gt; สำหรับเว็บแบบ http://&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ที่จริงอันนี้ต้องอธิบายอีกทีในหัวข้อ Nginx ว่าทำไมต้องเปิดพอร์ตเหล่านี้ด้วย ตอนนี้ติ๊กแค่ข้อแรกก่อน&lt;br&gt;
ที่เหลือใช้ค่าเริ่มต้นไปเลย ไว้มาแก้ทีหลังได้&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%2F218p74cz7kdmcx7jzsg4.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%2F218p74cz7kdmcx7jzsg4.png" alt=" " width="800" height="298"&gt;&lt;/a&gt;&lt;br&gt;
กดสร้างเครื่องที่ Launch instance แค่นี้เราก็ได้เครื่อง server มาแล้ว ง่ายยยยยยยใช่ไหมล่ะ&lt;/p&gt;
&lt;h2&gt;
  
  
  EZ
&lt;/h2&gt;

&lt;p&gt;หลังจากนั้นกด View all instances&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%2Fbae76lhz6pg0b4lgpnxm.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%2Fbae76lhz6pg0b4lgpnxm.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;br&gt;
และนี่คือเครื่องที่เราสร้างเมื่อกี้ มีสถานะเป็น Running คือกำลังทำงานอยู่&lt;br&gt;
กดเข้าไปก็จะเห็นรายละเอียดต่าง ๆ ที่เรากำหนดไว้&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%2F7h9wnshdje04txb1ehck.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%2F7h9wnshdje04txb1ehck.png" alt=" " width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  และแน่นอนว่าสิ่งที่เราอยากได้ก็คือเลข IP Public นั่นเอง
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;ระวัง:&lt;/strong&gt; ถ้าเรากด &lt;strong&gt;Stop&lt;/strong&gt; แล้ว &lt;strong&gt;Start&lt;/strong&gt; เครื่องใหม่ เลข IP Public จะเปลี่ยน (แค่ Reboot ไม่เปลี่ยน) ถ้าอยากได้ IP ถาวรให้ไปขอ &lt;strong&gt;Elastic IP&lt;/strong&gt; มาผูกกับเครื่อง (มีค่าใช้จ่าย หักจากเครดิต)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;หลังจากนั้นเลื่อนลงมา&lt;br&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%2Ft25svszwydcia2lvk6jf.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%2Ft25svszwydcia2lvk6jf.png" alt=" " width="799" height="298"&gt;&lt;/a&gt;&lt;br&gt;
กดที่ Security เพื่อดูว่าเราเปิดพอร์ตอะไรไว้บ้าง&lt;/p&gt;

&lt;p&gt;พอร์ต 22 คือที่เราจะ login เข้าไปแบบ SSH ส่วน 443 กับ 80 เป็นเรื่องของ Nginx นะจ๊ะ hahaha&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%2F5omlngevv6ni34v0acjw.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%2F5omlngevv6ni34v0acjw.png" alt=" " width="800" height="304"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  การ login ด้วยไฟล์ .pem
&lt;/h2&gt;

&lt;p&gt;เปิด CMD ในโฟลเดอร์ที่มีไฟล์ .pem (ของผมอยู่ที่ Downloads) แล้วใช้คำสั่ง&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ชื่อไฟล์.pem ec2-user@IP-Public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;ใครใช้ Mac หรือ Linux ต้องสั่ง &lt;code&gt;chmod 400 ชื่อไฟล์.pem&lt;/code&gt; ก่อน ไม่งั้น ssh จะไม่ยอมให้เข้า พร้อมขึ้นข้อความ UNPROTECTED PRIVATE KEY FILE&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;เข้าเครื่องได้แล้ว ต่อไปเราต้องการ clone project ของเราลงมา ซึ่งต้องใช้คำสั่ง Git ใช่ไหมล่ะ ก็ต้องติดตั้ง git ก่อน แล้วก็ Node ด้วย เพราะเราจะรัน API Express ของเรา&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git
&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%2Fibaywb0el12uwjawcdjs.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%2Fibaywb0el12uwjawcdjs.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://rpm.nodesource.com/setup_22.x | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash -
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;เลือกเวอร์ชันให้ตรงกับที่ API เราใช้ (ตัวอย่างนี้คือ 22)&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%2Fdczdi4cbrt4eqdcor7mn.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%2Fdczdi4cbrt4eqdcor7mn.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;เช็กเวอร์ชันของ Node กับ npm&lt;br&gt;
แค่นี้ก็เรียบร้อย ไปขั้นตอนถัดไปกัน&lt;/p&gt;
&lt;h2&gt;
  
  
  โหลด Project ลงมา
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;ถ้า project เป็น Public ก็ clone ได้เลย แต่ถ้าเป็น Private ล่ะ เราต้องยืนยันว่า project นี้เป็นของเราจริง ๆ ทำยังไง? ก็เอา Token จาก GitHub มายืนยัน ง่ายมาก ไปที่&lt;/p&gt;

&lt;p&gt;Settings → Developer settings → Personal access tokens → Tokens (classic)&lt;/p&gt;

&lt;p&gt;กด Generate new token (classic) เพื่อสร้าง token&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%2Fx9yj1ozsl6xpfbmgvxuh.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%2Fx9yj1ozsl6xpfbmgvxuh.png" alt=" " width="800" height="345"&gt;&lt;/a&gt;&lt;br&gt;
ตั้งชื่อ แล้วติ๊กสิทธิ์ที่ต้องใช้ ผมติ๊กแค่ repo แล้วกดสร้าง&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ทางเลือกที่ปลอดภัยกว่า: ใช้ &lt;strong&gt;Fine-grained token&lt;/strong&gt; แล้วให้สิทธิ์แค่ repo นี้ repo เดียว แบบ Contents: Read-only พอ ถ้า token หลุดก็เสียหายน้อยกว่า&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;อย่าลืมนะฮะ ต้อง copy เก็บไว้ให้ดี เพราะพอปิดหน้านั้นไป token จะไม่โชว์อีกแล้ว ฟิ้ววว&lt;br&gt;
ได้มาแล้วก็กลับไปที่ server&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;มันจะให้ใส่ Username ของ GitHub ส่วนช่อง Password ให้วาง Token ที่เพิ่งได้มา (ไม่ใช่รหัสผ่าน GitHub นะ)&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%2Fiwhchsuah90zco5frl1g.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%2Fiwhchsuah90zco5frl1g.png" alt=" " width="800" height="312"&gt;&lt;/a&gt;&lt;br&gt;
แค่นี้ก็เรียบร้อยแล้ววววว&lt;/p&gt;
&lt;h2&gt;
  
  
  การตั้งค่า .env
&lt;/h2&gt;

&lt;p&gt;เข้าไปในโฟลเดอร์ project แล้วสร้างไฟล์ .env&lt;br&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%2Fkjpl9yoxl2dqz2t7n1ow.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%2Fkjpl9yoxl2dqz2t7n1ow.png" alt=" " width="721" height="272"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;ชื่อProject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;วางค่า env ของ Production ลงไป เสร็จแล้วกด Ctrl + X เพื่อออก มันจะถามว่า Save modified buffer? ให้กด Y แล้ว Enter&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%2Fbe1tiimmv7igzbtluq4w.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%2Fbe1tiimmv7igzbtluq4w.png" alt=" " width="800" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ติดตั้ง package, migrate และ build
&lt;/h2&gt;

&lt;p&gt;ก่อนอื่นต้องติดตั้ง package ก่อน (เครื่องใหม่ยังไม่มี node_modules)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;แล้วค่อยรันต่อ&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run prisma:deploy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run db:seed &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prisma migrate deploy&lt;/code&gt; สร้างหรืออัปเดตตารางใน Supabase ตาม migration ที่มีอยู่&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db:seed&lt;/code&gt; ใส่ข้อมูลตั้งต้น&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build&lt;/code&gt; คอมไพล์โค้ด API&lt;/li&gt;
&lt;/ul&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%2Fpq56amtsy92ntfgkxpoy.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%2Fpq56amtsy92ntfgkxpoy.png" alt=" " width="800" height="508"&gt;&lt;/a&gt;&lt;br&gt;
ขึ้นแบบนี้ก็แสดงว่าถูกต้องเรียบร้อย&lt;br&gt;
ลองเช็กดู&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ถ้าเห็นโฟลเดอร์ชื่อ &lt;code&gt;dist&lt;/code&gt; คือไฟล์ที่ build แล้ว ที่เหลือแค่สั่ง run&lt;/p&gt;

&lt;h2&gt;
  
  
  รันด้วย PM2
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;ติดตั้ง PM2 แบบ global (ใช้ได้ทั้งเครื่อง) PM2 เป็นตัวจัดการโปรเซสของ Node.js ทำหน้าที่&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;รันแอปเป็นเบื้องหลัง ปิด terminal แล้วแอปยังทำงานต่อ&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;ถ้าแอป crash จะรีสตาร์ตให้เอง&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;เก็บ log ไว้ดูย้อนหลัง&lt;/em&gt;&lt;/li&gt;
&lt;/ul&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%2Fxog6t41rjzpv6tkk9q5s.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%2Fxog6t41rjzpv6tkk9q5s.png" alt=" " width="798" height="148"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start dist/server.js &lt;span class="nt"&gt;--name&lt;/span&gt; upro-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;สั่งรันไฟล์ dist/server.js ที่ได้มาจาก npm run build เมื่อกี้ และตั้งชื่อโปรเซสว่า upro-api ไว้เรียกใช้ทีหลัง&lt;/em&gt;&lt;br&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%2F9ckcbxzn8thnuy3glfto.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%2F9ckcbxzn8thnuy3glfto.png" alt=" " width="800" height="127"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;บันทึกรายการโปรเซสที่รันอยู่ตอนนี้&lt;/em&gt;&lt;br&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%2F4b6vbchag8nwtxpcmix7.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%2F4b6vbchag8nwtxpcmix7.png" alt=" " width="800" height="52"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;แต่ &lt;code&gt;pm2 save&lt;/code&gt; อย่างเดียวยังไม่พอนะ ถ้าเครื่องรีบูต API จะไม่เปิดขึ้นมาเอง ต้องสั่งอีกคำสั่ง&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;มันจะพิมพ์คำสั่งที่ขึ้นต้นด้วย &lt;code&gt;sudo env PATH=...&lt;/code&gt; ออกมา ให้ copy คำสั่งนั้นไปรัน แล้วตามด้วย &lt;code&gt;pm2 save&lt;/code&gt; อีกรอบ แค่นี้รีบูตกี่ทีแอปก็กลับมาเอง&lt;/p&gt;

&lt;p&gt;คำสั่ง PM2 ที่ใช้บ่อย&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pm2 list&lt;/code&gt; ดูสถานะ&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pm2 logs upro-api&lt;/code&gt; ดู log ตอนหา error&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pm2 restart upro-api&lt;/code&gt; รีสตาร์ตหลัง build ใหม่&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;และแน่นอน เช็กว่า API ทำงานได้ไหม&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;6 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:5000/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ถ้าได้ &lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt; ก็เสร็จแล้ววววว เย้!&lt;/p&gt;

&lt;h2&gt;
  
  
  แต่ ๆ ๆ ลองไปเช็กที่ Browser ดูสิ
&lt;/h2&gt;

&lt;p&gt;ลองเปิด &lt;a href="http://IP:5000/health" rel="noopener noreferrer"&gt;http://IP:5000/health&lt;/a&gt; ในเบราว์เซอร์&lt;br&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%2Fw9kkrxrgwwvhlpyist1n.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%2Fw9kkrxrgwwvhlpyist1n.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;br&gt;
มันไม่ได้ใช่ไหมล่ะ&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไมใน server ได้ แต่นอก server ไม่ได้?
&lt;/h2&gt;

&lt;p&gt;ก็เพราะเรายังไม่ได้เปิด firewall หรือพูดง่าย ๆ คือยังไม่ได้เปิดประตูนั่นเอง ซึ่งเปิดง่ายมาก&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%2Fjvz66ijz9h58hhh47qst.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%2Fjvz66ijz9h58hhh47qst.png" alt=" " width="800" height="300"&gt;&lt;/a&gt;&lt;br&gt;
ไปที่ Security Group → Edit inbound rules เพื่อไปเปิดพอร์ต&lt;br&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%2Fhelsm0x6hikwqvwkwvy8.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%2Fhelsm0x6hikwqvwkwvy8.png" alt=" " width="800" height="296"&gt;&lt;/a&gt;&lt;br&gt;
กด Add rule&lt;br&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%2Fhbvb5ptbthu6rxwj6uvs.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%2Fhbvb5ptbthu6rxwj6uvs.png" alt=" " width="800" height="138"&gt;&lt;/a&gt;&lt;br&gt;
ตั้งค่าประมาณนี้ คือ Type: Custom TCP, Port: 5000 (พอร์ต API ของเรา), Source: 0.0.0.0/0 (Vercel ต้องเรียกเข้ามาได้) แต่แนะนำให้ทำ Nginx นะจ๊ะ อันนี้แค่ deploy ให้ใช้งานได้ก่อนเท่านั้น&lt;br&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%2Fety164juplzw0trbpylq.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%2Fety164juplzw0trbpylq.png" alt=" " width="772" height="347"&gt;&lt;/a&gt;&lt;br&gt;
ลองเปิด &lt;a href="http://IP:5000/health" rel="noopener noreferrer"&gt;http://IP:5000/health&lt;/a&gt; ในเบราว์เซอร์อีกที ขึ้น OK ก็เรียบร้อยแล้ววว แค่นี้ API ของเราก็ใช้งานได้แล้ว&lt;/p&gt;

&lt;h2&gt;
  
  
  เเต่นี้คือเราเปิดประตูหลัง อาจจะโดนซอยตูดได้ เพราะงันต้องทำ Nginx น่ะจ่ะ ทำไงติดตามน่ะจ่ะ
&lt;/h2&gt;

</description>
      <category>aws</category>
      <category>backend</category>
      <category>cloud</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploy Web เเบบ PaaS ด้วย Vercel (Free)</title>
      <dc:creator>reerii code</dc:creator>
      <pubDate>Sat, 17 Oct 2026 13:33:44 +0000</pubDate>
      <link>https://dev.to/reeriicode/deploy-web-eebb-paas-dwy-vercel-free-3loe</link>
      <guid>https://dev.to/reeriicode/deploy-web-eebb-paas-dwy-vercel-free-3loe</guid>
      <description>&lt;p&gt;ไปที่ vercel&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%2Fobzs3adedf7balrlt15q.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%2Fobzs3adedf7balrlt15q.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;br&gt;
ไป login ให้เสร็จเรียบร้อย เเนะนำ login ด้วย Github&lt;br&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%2Fatm8ovvi2d875k4s7rc0.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%2Fatm8ovvi2d875k4s7rc0.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;br&gt;
เมือขึ้นหน้านี้ เราจะเห็นได้ว่ามี Project ของเรา ที่เราเชื่อมกับ Github&lt;br&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%2Fsu83qpjf3e8tuo7gu7xe.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%2Fsu83qpjf3e8tuo7gu7xe.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;br&gt;
กด import website ของเรา&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%2Fpbx73xyqech0tmtks3ei.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%2Fpbx73xyqech0tmtks3ei.png" alt=" " width="799" height="375"&gt;&lt;/a&gt;&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%2Flm8mpt40i4lwrii5pelk.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%2Flm8mpt40i4lwrii5pelk.png" alt=" " width="799" height="344"&gt;&lt;/a&gt;&lt;br&gt;
ตั้งชื่อ subdomain เช่น ถ้าเราตั้ง uprodd จะได้ subdomain คือ uprodd.vercel.app&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%2Fvf1dpz7oi91t0kyhdf1x.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%2Fvf1dpz7oi91t0kyhdf1x.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;br&gt;
สวนคำสั่ง build ไม่ต้อง เพราะว่ามันรู้อยู่เเล้วว่าเป็น React มันจัดการให้อยู่เเล้ว สวน env ก็ใส่ตามที่เรา ตั้งค่า &lt;br&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%2Fo2tznok1tp3j1ewujphy.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%2Fo2tznok1tp3j1ewujphy.png" alt=" " width="487" height="381"&gt;&lt;/a&gt;&lt;br&gt;
เเต่ของผมตั้ง set การคุยกับ API คือ /api เพราะผมได้ตั้งค่าใน versel.json เป็นเเบบนี้&lt;br&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%2F2mbzyc36y7r7f2lb0w17.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%2F2mbzyc36y7r7f2lb0w17.png" alt=" " width="799" height="399"&gt;&lt;/a&gt;&lt;br&gt;
ความหมายคือ &lt;br&gt;
Web = &lt;a href="https://uprodd.vercel.app" rel="noopener noreferrer"&gt;https://uprodd.vercel.app&lt;/a&gt;&lt;br&gt;
API = &lt;a href="https://uprodd.vercel.app/api" rel="noopener noreferrer"&gt;https://uprodd.vercel.app/api&lt;/a&gt; จะใช้เเทน &lt;a href="http://ip:port" rel="noopener noreferrer"&gt;http://ip:port&lt;/a&gt; ของ server api&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไม่ต้องใช้เเบบนี้
&lt;/h2&gt;

&lt;p&gt;เพาะผมใช้ Login ของ Google ซึ่งมันรองรับเเค่ https:// ที่จริงไป deploy api เจ้าอื่นที่ให้ Subdomain เลยก็ได้ เเต่ api ของผม ผม deploy เเบบ VPS (free)ซึงมันได้ IP public มา&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%2Fvgfntdaaklygyxoiewly.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%2Fvgfntdaaklygyxoiewly.png" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
สำหรับใครที่ไม่ได้ใช้ Google หรือ มี Domain API อยู่เเล้ว ไม่ต้องทำขั้นตอนนี้เลยก็ได้&lt;br&gt;
 เเค่นี้ก็ สามารถ deploy ได้เเล้ว &lt;br&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%2Fo47c2bddi1zviaima1ki.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%2Fo47c2bddi1zviaima1ki.png" alt=" " width="800" height="372"&gt;&lt;/a&gt;&lt;br&gt;
รอให้มัน build เเค่นี้ก็สามารถ ใช้งาน Website ได้เเล้ว&lt;/p&gt;

</description>
      <category>deployment</category>
      <category>github</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Deploy คืออะไร ?</title>
      <dc:creator>reerii code</dc:creator>
      <pubDate>Thu, 15 Oct 2026 07:44:25 +0000</pubDate>
      <link>https://dev.to/reeriicode/deploy-khuueaair--2a9p</link>
      <guid>https://dev.to/reeriicode/deploy-khuueaair--2a9p</guid>
      <description>&lt;p&gt;Deploy คือ ง่ายๆเลยคือการเอาเว็บของเราไปวางใว้บน server ให้คนอื่นเข้ามาใช้งานได้&lt;/p&gt;

&lt;h2&gt;
  
  
  เเล้วทำไม่ต้อง Deploy ล่ะ?
&lt;/h2&gt;

&lt;p&gt;ก็เพราะถ้าไม่ deploy คนอื่นเข้ามาใช้เว็บเราไม่ได้ไงล่ะ hahaha &lt;br&gt;
อ่าวกวนตีนนี้วาาา &lt;br&gt;
โอเครๆ เดียวอธิบายดีๆ ตอนเรา dev เว็บอยู่เนี่ย เว็บมันรันอยู่บน local หรือก็คือในเครื่องเราเอง เราเปิด localhost ได้ แต่เพื่อนเราเปิดไม่ได้ เพราะมันอยู่ในเครื่องเรา ไม่ได้อยู่บนอินเทอร์เน็ต&lt;/p&gt;
&lt;h2&gt;
  
  
  อยากให้คนอื่นดูเว็บเดี๋ยวนี้ ทำไงดี?
&lt;/h2&gt;

&lt;p&gt;มีทางลัดอยู่ทางนึง เรียกว่า Tunneling ก็คือเปิดอุโมงค์จากเครื่องเราออกไปอินเทอร์เน็ต แล้วมันจะสร้าง URL สาธารณะชั่วคราวให้ ส่งลิงก์ให้เพื่อนเปิดได้เลย เครื่องมือที่นิยมก็ ngrok หรือ Cloudflare Tunnel วิธีนี้เหมาะกับตอนทำ prototype ให้ลูกค้าดูระบบคร่าว ๆ&lt;/p&gt;
&lt;h2&gt;
  
  
  แต่!
&lt;/h2&gt;

&lt;p&gt;ครื่องเราต้องเปิดไว้ตลอดนะจ๊ะ ปิดฝาโน้ตบุ๊กไปกินข้าวเมื่อไหร่ เว็บดับเมื่อนั้น &lt;br&gt;
ถ้าอยากให้คนใช้ได้ยาว ๆ ทั้งวันทั้งคืน ยังไงก็ต้อง deploy&lt;/p&gt;
&lt;h2&gt;
  
  
  Deploy ยากไหม?
&lt;/h2&gt;

&lt;p&gt;บอกเลย ง่ายกว่าที่คิด  ปกติบน local เราทำยังไง บน server ก็ทำแบบนั้นแหละ&lt;br&gt;
&lt;em&gt;เอ้ย จริงดิ?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;จริง…สัก 90% อีก 10% ต้องแก้นิดหน่อย เพราะเครื่องเราอาจเป็น Windows หรือ Mac แต่ server ส่วนใหญ่เป็น Linux &lt;/p&gt;
&lt;h2&gt;
  
  
  แล้วทำไมไม่ใช้ Docker ล่ะ มันแก้ปัญหา OS ไม่ตรงกันได้นี่?
&lt;/h2&gt;

&lt;p&gt;ใช่ ใช้ได้ แต่ก่อนจะไป Docker ต้องเข้าใจ deploy แบบพื้นฐานก่อน ว่าเบื้องหลังมันทำอะไรบ้าง ไม่งั้นวันไหน Docker พัง จะงงว่าพังตรงไหน&lt;/p&gt;
&lt;h2&gt;
  
  
  Deploy มีกี่แบบ?
&lt;/h2&gt;

&lt;p&gt;หลัก ๆ มี 3 แบบ&lt;/p&gt;
&lt;h2&gt;
  
  
  แบบที่ 1 VPS
&lt;/h2&gt;

&lt;p&gt;แบบที่ 1 VPS คือเช่าคอมพิวเตอร์ 1 เครื่องบน cloud แล้วเข้าไปผ่าน SSH ใช้ terminal เป็นหลัก ก็เหมือนทำบน local แค่เปลี่ยนจาก Windows เป็น Linux&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ข้อดีคือคุมได้ทุกอย่าง ข้อเสียคือต้องทำเองทุกอย่าง ตั้งแต่ติดตั้ง Node เปิด firewall ไปจนถึงตั้ง Nginx  เจ้าดัง ๆ ก็ AWS EC2, Google Cloud, Oracle Cloud&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  แบบที่ 2 PaaS (Platform as a Service)
&lt;/h2&gt;

&lt;p&gt;แบบที่ 2 PaaS (Platform as a Service) สำหรับสายขี้เกียจ…เอ้ย สายที่ไม่อยากดูแล server เอง  แค่เชื่อม GitHub พอ push ขึ้น main ปุ๊บ มัน deploy ให้อัตโนมัติปั๊บ เช่น Vercel, Render  ง่ายใช่ไหมล่ะ&lt;/p&gt;
&lt;h2&gt;
  
  
  แบบที่ 3 Docker
&lt;/h2&gt;

&lt;p&gt;แบบที่ 3 Docker แพ็กโปรเจกต์เราเป็น image แล้วเอาไปรันที่ไหนก็ได้ จะเป็น Google Cloud Run หรือเอาไปรันบน VPS ก็ได้&lt;/p&gt;
&lt;h2&gt;
  
  
  ปัญหาคือ API ผมดันเก็บไฟล์รูปไว้ใน folder ของ API เองน่ะสิ แถมยังใช้ WebSocket อีก
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;แล้วมันผิดตรงไหน?&lt;/em&gt;&lt;br&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%2Fd71ljdsa3a3lu1d3si9r.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%2Fd71ljdsa3a3lu1d3si9r.png" alt=" " width="447" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Vercel เป็น serverless คือตื่นมาทำงานก็ต่อเมื่อมี request เข้ามา ทำเสร็จก็ทิ้ง เหมือนห้องที่โดนรีเซ็ตทุกครั้ง รูปที่อัปโหลดไว้ก็หายเกลี้ยง แล้วก็ไม่รองรับ WebSocket ด้วย เพราะมันไม่ได้เปิดค้างไว้ให้เชื่อมต่อตลอด&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ส่วน Render กับ Cloud Run ถึงจะรัน server ค้างไว้ได้ แต่พื้นที่เก็บไฟล์เป็นแบบชั่วคราว restart หรือ deploy ใหม่เมื่อไหร่ รูปก็หายเหมือนกัน&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  ทางแก้มี 2 ทาง
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ทางที่ 1 แก้โค้ด ย้ายรูปไปเก็บข้างนอก เช่น Supabase Storage หรือ AWS S3 ฐานข้อมูลก็ใช้ของภายนอก ให้ API เป็นแค่ทางผ่านของข้อมูลเฉย ๆ ไม่เก็บอะไรไว้กับตัว แบบนี้ใช้ PaaS ได้สบาย&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ทางที่ 2 ไม่อยากแก้โค้ด ก็ใช้ VPS ไปเลย เพราะมันคือคอมพิวเตอร์จริง ๆ 1 เครื่อง ไฟล์ไม่หาย WebSocket ก็เปิดค้างได้&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;เเต่ในครั้งนี้จะ deploy เริ่มต้นใช้เเบบ VPS เพราะจะได้เข้าใจ การ deploy พื้นฐานก่อน&lt;/p&gt;

&lt;p&gt;Stack ผมคือ หน้าเว็บใช้ React, API ใช้ Express&lt;br&gt;
Web → Vercel ง่ายสุด จบ&lt;br&gt;
API → VPS&lt;/p&gt;
&lt;h2&gt;
  
  
  ก่อน deploy ต้องเข้าใจกระบวนการหลักก่อน
&lt;/h2&gt;

&lt;p&gt;นึกถึงตอนเริ่มพัฒนาโปรเจกต์ เราทำอะไรก่อน?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;git clone โค้ดลงมา&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ติดตั้ง dependency&lt;/p&gt;

&lt;h2&gt;
  
  
  อ้าว ทำไมต้อง install อีก โค้ดก็โหลดมาแล้วนี่?
&lt;/h2&gt;

&lt;p&gt;เพราะของที่เราติดตั้งใน Node.js มันไปกองอยู่ใน folder node_modules ซึ่งหนักมาก ไม่มีใครบ้าโยนขึ้น GitHub หรอก ปกติจะใส่ไว้ใน .gitignore&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%2Fkcs0fyumhsc2apu3zqgp.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%2Fkcs0fyumhsc2apu3zqgp.png" alt=" " width="442" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;สิ่งที่เราดึงโค้ดจาก Github มีเเต่โค้ด ไม่มี dependency ซึ่งเราต้องติดตั้ง &lt;/p&gt;

&lt;h2&gt;
  
  
  แล้วเครื่องใหม่จะรู้ได้ไงว่าต้องติดตั้งอะไรบ้าง?
&lt;/h2&gt;

&lt;p&gt;ดูจาก package.json ไง มันคือรายการที่ติดตั้งใว้ จดไว้ว่าโปรเจกต์ใช้อะไร เวอร์ชันไหน สั่ง npm install ทีเดียว คือจบ&lt;/p&gt;

&lt;p&gt;สวนใครที่ใช้ ฝั่ง Python ก็คล้ายกัน ใช้ไฟล์ requirements.txt ในการจด&lt;/p&gt;

&lt;h2&gt;
  
  
  รันโปรเจกต์
&lt;/h2&gt;

&lt;p&gt;ตอน dev เราใช้ npm run dev แต่ตอน deploy จะใช้ npm start หรือคำสั่งสำหรับรันจริง&lt;/p&gt;

&lt;h2&gt;
  
  
  แต่ ๆ ๆ ๆ ช้าก่อน!
&lt;/h2&gt;

&lt;p&gt;ใครเขียน JavaScript, HTML, CSS ล้วน ๆ รันได้เลย  แต่ถ้าใช้ TypeScript หรือ framework อย่าง React…พวกนายต้อง build ก่อนเว้ย!&lt;/p&gt;

&lt;h2&gt;
  
  
  Build คืออะไร?
&lt;/h2&gt;

&lt;p&gt;Build ก็คือการแปลงโค้ดที่เราเขียน ให้กลายเป็นไฟล์ที่พร้อมใช้งานจริง&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไมต้องแปลง?
&lt;/h2&gt;

&lt;p&gt;เพราะตัวที่รันโค้ดมันไม่รู้จัก TypeScript หรือ JSX&lt;br&gt;
ฝั่งเว็บ บราวเซอร์อ่านได้แค่ HTML, CSS, JavaScript&lt;br&gt;
ฝั่ง API ตัว Node.js ก็อ่านได้แค่ JavaScript&lt;/p&gt;

&lt;p&gt;พอสั่ง&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;มันจะสร้าง folder dist ขึ้นมา ข้างในคือโค้ดที่แปลงเป็น JavaScript หมดแล้ว  จากนั้นก็รัน API ด้วย node dist/server.js (ชื่อไฟล์แล้วแต่คนตั้ง อาจเป็น index.js หรือ main.js)  แค่นี้ API ก็ทำงานแล้ว&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%2Flvbnytyofu4lgb4f00ez.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%2Flvbnytyofu4lgb4f00ez.png" alt=" " width="477" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  แต่ ๆ ๆ ใครใช้ ORM อย่าง Prisma ต้องรันคำสั่งเพิ่มอีกนิด ไม่งั้น API จะคุยกับ database ไม่รู้เรื่อง
&lt;/h2&gt;

&lt;p&gt;พูดมาเยอะ บางคนอาจยังไม่เห็นภาพ  งั้นมาลงมือทำจริงกันเลยดีกว่า! &lt;a href="https://dev.to/reeriicode/deploy-api-eebb-vps-odyaich-aws-ec2free-express-prisma-google-login-superbase-45j1"&gt;Deploy API ด้วย AWS free&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>deploy</category>
      <category>aws</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
