Core Instructions
เรียนรู้คำสั่งหลัก 6 ตัวใน Dockerfile ได้แก่ FROM, WORKDIR, COPY, RUN, CMD, และ EXPOSE พร้อมตัวอย่างจริง ความต่างที่มักสับสน และข้อผิดพลาดที่ควรหลีกเลี่ยง
ภาพจำสำคัญบทนี้
FROM/WORKDIR/COPY/RUN ทำงานตอน build image — CMD ทำงานตอน container รัน — EXPOSE แค่ metadata ไม่ได้ publish port | ใช้ docker run -p เพื่อ publish จริง | CMD ใช้ exec form (array) เสมอ
ส่วนที่ 1
ภาพรวม instructions หลักใน Dockerfile
Dockerfile ประกอบด้วย "คำสั่ง" (instructions) ที่เขียนเป็นตัวพิมพ์ใหญ่ แต่ละบรรทัดคือคำสั่งที่บอก Docker ให้ทำอะไรสักอย่างระหว่างการ build image คำสั่งหลักที่ต้องรู้จักมี 6 ตัว แต่ละตัวมีหน้าที่ต่างกันชัดเจน — บางตัวทำงานตอน build image บางตัวทำงานตอน container รัน ในบทนี้เราจะเรียนทีละตัว พร้อมตัวอย่าง และสิ่งที่ผู้เริ่มต้นมักเข้าใจผิด
- FROM — ระบุ base image ที่ใช้เริ่มต้น build (บรรทัดแรกของ Dockerfile เสมอ)
- WORKDIR — กำหนด working directory ภายใน container (เหมือน cd เข้าโฟลเดอร์ก่อนทำอะไร)
- COPY — คัดลอกไฟล์จากเครื่อง host เข้าไปใน image ระหว่าง build
- RUN — รันคำสั่ง shell ระหว่างขั้นตอน build เช่น ติดตั้ง package หรือ compile code
- CMD — กำหนดคำสั่งเริ่มต้นที่รันเมื่อสั่ง docker run (ไม่ใช่ตอน build)
- EXPOSE — ประกาศให้ Docker รู้ว่า container จะใช้ port ใด (เป็นข้อมูล ไม่ได้ publish port จริง)
ภาพที่ 1 — ลำดับ instructions ใน Dockerfile
ภาพที่ 1: FROM → WORKDIR → COPY → RUN เกิดขึ้นตอน build image — EXPOSE เป็นแค่ metadata — CMD ทำงานตอน container รัน
ส่วนที่ 2
FROM — จุดเริ่มต้นของทุก Dockerfile
ลองนึกภาพว่าคุณจะทำอาหาร ก่อนจะเริ่มทำอะไร คุณต้องมีวัตถุดิบตั้งต้นก่อน — FROM ก็คือการบอก Docker ว่า "เริ่มจากวัตถุดิบชิ้นนี้" FROM ระบุ base image ที่ Docker จะใช้เป็นจุดเริ่มต้นในการ build คิดว่ามันเหมือน OS + runtime สำเร็จรูปที่เราต่อยอดขึ้นไป แทนที่จะต้องสร้างทุกอย่างตั้งแต่ศูนย์ ทุก Dockerfile ต้องมี FROM เป็นคำสั่งแรก (ยกเว้นบรรทัด comment ที่ขึ้นต้นด้วย #) ถ้าไม่มี FROM Docker จะไม่รู้จะเริ่ม build จากอะไร
ระบุ image:tag เสมอ — ถ้าไม่ระบุ tag Docker จะใช้ :latest ซึ่งอาจเปลี่ยนเวอร์ชันโดยไม่ตั้งใจ
- FROM ต้องอยู่บรรทัดแรกเสมอ — ก่อนคำสั่งอื่นทุกตัว (เว้นแต่บรรทัด comment)
- ควรระบุ tag เสมอ เช่น node:20-alpine แทนที่จะเขียน node เฉย ๆ เพื่อ lock เวอร์ชัน
- เลือก base image ขนาดเล็กเมื่อทำได้ เช่น -alpine หรือ -slim ทำให้ image สุดท้ายเบากว่า
- ถ้าเลือก FROM scratch หมายความว่าเริ่มจาก image ว่างเปล่าจริง ๆ (ใช้ใน advanced cases เช่น Go binary)
ส่วนที่ 3
WORKDIR — กำหนด working directory ภายใน container
เมื่อ Docker build image ทุกคำสั่งที่รันจะทำงานจากตำแหน่งหนึ่งใน filesystem ของ container โดย default คือ root directory (/) WORKDIR คือการบอก Docker ว่า "นับจากนี้ ให้ทำงานจากโฟลเดอร์นี้" เหมือนการพิมพ์ cd /app ก่อนทำอะไร แต่ WORKDIR ยังสร้างโฟลเดอร์ให้อัตโนมัติถ้ายังไม่มีด้วย ถ้าไม่มี WORKDIR คำสั่ง COPY และ RUN ทั้งหมดจะทำงานที่ root (/) ซึ่งอาจทับไฟล์ระบบของ OS ที่อยู่ใน base image ได้
WORKDIR สร้างโฟลเดอร์อัตโนมัติ — ไม่ต้อง RUN mkdir /app ก่อน
| ไม่มี WORKDIR | มี WORKDIR /app |
|---|---|
| COPY . . → ไฟล์กระจายที่ / (root) | COPY . . → ไฟล์อยู่ที่ /app ทั้งหมด |
| อาจทับไฟล์ระบบ เช่น /etc, /bin | ไฟล์แยกจากระบบ OS ชัดเจน |
| RUN ls จะแสดงไฟล์ปนกับของ OS | RUN ls จะแสดงแค่ไฟล์ของเรา |
| ยากต่อการ debug และ maintain | สะอาด อ่านง่าย และปลอดภัยกว่า |
ส่วนที่ 4
COPY — นำไฟล์จาก host เข้าไปใน image
COPY ทำงานง่ายมาก: คัดลอกไฟล์หรือโฟลเดอร์จากเครื่องของเรา (host machine) เข้าไปใน image ระหว่างขั้นตอน build รูปแบบคือ COPY <source> <destination> โดย source คือ path บนเครื่อง host และ destination คือ path ภายใน image สิ่งสำคัญ: COPY ทำงานตอน build เท่านั้น ไม่ใช่ตอน container รัน และ source path จะถูกนับจาก build context (โดยปกติคือโฟลเดอร์ที่รันคำสั่ง docker build)
ลำดับ COPY มีผลต่อ Docker cache — COPY ไฟล์ที่เปลี่ยนบ่อยทีหลัง
- COPY <src> <dest> — src คือ path บน host, dest คือ path ใน image
- . (จุด) ในตำแหน่ง destination หมายถึง WORKDIR ปัจจุบัน
- COPY . . — คัดลอกทุกอย่างจาก build context เข้า WORKDIR
- ไฟล์ที่อยู่ใน .dockerignore จะไม่ถูก COPY — ใช้เพื่อแยก node_modules, .git ออก
- ลำดับสำคัญ: COPY ไฟล์ที่เปลี่ยนน้อยก่อน (เช่น package.json) เพื่อใช้ประโยชน์จาก layer cache
ส่วนที่ 5
RUN — รันคำสั่งระหว่าง build
RUN คือคำสั่งที่ทำให้ Docker รัน shell command ระหว่างขั้นตอน build image ผลลัพธ์ของคำสั่งนั้น (เช่น ไฟล์ที่ถูกสร้าง หรือ package ที่ถูกติดตั้ง) จะถูกบันทึกลงใน image เป็น layer ใหม่ ใช้ RUN สำหรับทุกอย่างที่ต้องเตรียมก่อนที่แอปจะรัน เช่น ติดตั้ง dependencies, compile code, หรือสร้างโฟลเดอร์ สิ่งสำคัญ: แต่ละ RUN สร้าง layer ใหม่ใน image ดังนั้น ถ้ามีหลายคำสั่งที่เกี่ยวกัน ควรรวมไว้ใน RUN เดียวด้วย &&
รวมคำสั่งด้วย && เพื่อลดจำนวน layer และลดขนาด image
ประเด็นที่ 1
RUN รันตอน build เท่านั้น — ไม่ใช่ตอน container เริ่มทำงาน
ประเด็นที่ 2
แต่ละ RUN สร้าง layer ใหม่ — รวมคำสั่งที่เกี่ยวกันด้วย && เพื่อประหยัด layer
ประเด็นที่ 3
ผลลัพธ์ของ RUN ถูกบันทึกลง image — ไฟล์ที่สร้างจาก RUN จะอยู่ใน image เสมอ
ประเด็นที่ 4
ใช้ RUN npm ci แทน RUN npm install ใน production เพราะ ci ติดตั้งตาม lockfile เป๊ะ ๆ
ส่วนที่ 6
CMD — คำสั่งเริ่มต้นเมื่อ container รัน
CMD แตกต่างจากคำสั่งก่อนหน้าทั้งหมด — มันไม่ได้ทำงานตอน build image แต่เป็นการบอก Docker ว่า "เมื่อมีการสั่ง docker run บน image นี้ ให้รันคำสั่งอะไรเป็นอันแรก" คิดง่าย ๆ ว่า CMD คือ "default startup command" ของ container มีรูปแบบเขียนได้ 2 แบบ: shell form และ exec form ซึ่งทำงานต่างกัน และ exec form เป็นแบบที่แนะนำให้ใช้
ใช้ exec form (array) เสมอ — รับ signal ได้ถูกต้อง ทำให้ docker stop ทำงานได้เร็ว
| รูปแบบ | ตัวอย่าง | ข้อดี | ข้อเสีย |
|---|---|---|---|
| Shell form | CMD node server.js | เขียนสั้น คุ้นเคย | รับ signal ไม่ได้ (docker stop ช้า), PID 1 คือ sh |
| Exec form | CMD ["node", "server.js"] | รับ SIGTERM ได้โดยตรง, graceful shutdown | เขียนยาวกว่าเล็กน้อย |
ส่วนที่ 7
EXPOSE — ประกาศ port (ไม่ใช่การ publish จริง)
EXPOSE เป็นคำสั่งที่ผู้เริ่มต้นมักเข้าใจผิดมากที่สุด — มันไม่ได้ "เปิด" port ให้ภายนอกเข้าถึงได้จริง ๆ EXPOSE ทำหน้าที่เป็นแค่ "เอกสาร" (documentation) ที่บอกว่า container นี้ออกแบบมาให้ฟังที่ port นี้ เหมือนการเขียนป้ายบอกข้อมูล ถ้าต้องการให้ port เข้าถึงได้จากภายนอก ต้องใช้ flag -p ตอนสั่ง docker run เช่น docker run -p 8080:3000 my-app
EXPOSE เป็นแค่ metadata — ต้องใช้ -p ตอน docker run เพื่อ publish จริง
- EXPOSE ไม่ได้ publish port จริง — เป็นแค่ข้อมูลบอกว่า container ใช้ port อะไร
- ต้องใช้ -p <host_port>:<container_port> ตอนสั่ง docker run เพื่อ publish จริง
- EXPOSE ช่วยให้คนอื่นรู้ว่าต้อง -p ที่ port ไหนเมื่ออ่าน Dockerfile
- Docker Compose อ่าน EXPOSE เพื่อใช้ในการ link container ภายใน network อัตโนมัติ
- สามารถ EXPOSE หลาย port ได้: EXPOSE 3000 8080 หรือ EXPOSE 3000/udp
ภาพที่ 2 — หน้าที่ของแต่ละ instruction
FROM
เลือก base
BuildWORKDIR
ตั้ง directory
BuildCOPY
ย้ายไฟล์เข้า
BuildRUN
รันคำสั่ง
BuildEXPOSE
บันทึก port
MetadataCMD
เริ่มแอป
Runภาพที่ 2: FROM / WORKDIR / COPY / RUN ทำงานตอน build — EXPOSE เป็น metadata เฉย ๆ — CMD ทำงานตอน run
ส่วนที่ 8
ตัวอย่าง Dockerfile ครบชุด (แอป Node.js)
ลองดูตัวอย่างจริงที่ใช้ทุก instruction ครบทั้ง 6 ตัว โดยใช้แอป Node.js HTTP server เป็นตัวอย่าง สมมติว่า project มีไฟล์: package.json, package-lock.json, และ server.js
ลำดับที่เขียนสำคัญมาก — เรียงจาก 'เปลี่ยนน้อย' ไป 'เปลี่ยนบ่อย' เพื่อใช้ประโยชน์จาก cache
ส่วนที่ 9
อธิบายแต่ละบรรทัดอย่างละเอียด
มาทำความเข้าใจทีละบรรทัดว่าทำอะไร ทำไมเขียนแบบนี้ และมีผลอะไรต่อ image สุดท้าย
| บรรทัด | คำสั่ง | สิ่งที่เกิดขึ้น | ทำไมเขียนแบบนี้ |
|---|---|---|---|
| 1 | FROM node:20-alpine | ดาวน์โหลด image node:20-alpine เป็นจุดเริ่มต้น — มี OS + Node.js พร้อมแล้ว | Alpine เล็กมาก (~5MB) + มี Node.js 20 ติดมา ไม่ต้องติดตั้งเอง |
| 2 | WORKDIR /app | สร้างโฟลเดอร์ /app ใน image และเปลี่ยน working dir มาที่นี่ | ไฟล์ทุกอย่างจะอยู่ที่ /app — ป้องกันปนกับไฟล์ระบบ |
| 3 | COPY package.json package-lock.json ./ | คัดลอก 2 ไฟล์นี้จาก host → /app/ ใน image | COPY เฉพาะ dependency files ก่อน เพื่อให้ cache ของ RUN npm ci คงอยู่นานกว่า |
| 4 | RUN npm ci | ติดตั้ง node_modules ตาม package-lock.json ลงใน /app | npm ci ล้างแล้วติดตั้งใหม่ทุกครั้ง ผลลัพธ์เหมือนกันเสมอ เหมาะกับ CI/CD |
| 5 | COPY . . | คัดลอกทุกอย่างจาก build context (host) → /app ใน image | ทำหลัง npm ci เพราะ source code เปลี่ยนบ่อย ทำให้ cache ของ npm ci ยังคงอยู่ |
| 6 | EXPOSE 3000 | บันทึก metadata ว่า container นี้ใช้ port 3000 | เป็นเอกสารบอกคนอื่น + ใช้กับ Docker Compose ได้ แต่ไม่ publish port จริง |
| 7 | CMD ["node", "server.js"] | กำหนดว่าตอนสั่ง docker run จะรัน node server.js | exec form ทำให้ Node.js เป็น PID 1 รับ SIGTERM ได้ → graceful shutdown |
ส่วนที่ 10
COPY vs RUN — ความต่างที่มักสับสน
COPY และ RUN เป็น 2 คำสั่งที่ทำงานต่างกันโดยสิ้นเชิง แต่ผู้เริ่มต้นมักสับสนระหว่างกัน เพราะทั้งคู่ทำงานตอน build กุญแจสำคัญ: COPY ย้ายไฟล์ — RUN รันคำสั่ง
| ประเด็น | COPY | RUN |
|---|---|---|
| หน้าที่ | คัดลอกไฟล์จาก host เข้า image | รันคำสั่ง shell ภายใน image |
| ทำงานเมื่อไหร่ | ตอน build image | ตอน build image |
| Input | ไฟล์บนเครื่อง host (source code, config) | คำสั่ง shell เช่น npm install, apt-get |
| Output ที่เกิดขึ้น | ไฟล์ถูกวางใน image | ผลลัพธ์ของคำสั่ง (ไฟล์ใหม่, package ที่ติดตั้ง) |
| ตัวอย่าง | COPY package.json ./ | RUN npm ci |
| ถามตัวเอง | ฉันต้องการ 'ย้าย' ไฟล์จาก host ไหม? | ฉันต้องการ 'รัน' บางอย่างใน container ไหม? |
ส่วนที่ 11
CMD vs docker run command
CMD ไม่ใช่คำสั่งเดียวที่กำหนดได้ — ตอนสั่ง docker run เราสามารถ override CMD ได้โดยส่งคำสั่งต่อท้าย ความเข้าใจนี้ช่วยให้ใช้งาน Docker ได้ยืดหยุ่นกว่ามาก
| สถานการณ์ | คำสั่ง | สิ่งที่รัน |
|---|---|---|
| รัน container ปกติ | docker run my-app | ใช้ CMD ที่กำหนดใน Dockerfile เช่น node server.js |
| Override CMD ชั่วคราว | docker run my-app sh | เปิด shell แทน — ใช้ debug ได้ |
| Override CMD ชั่วคราว | docker run my-app npm test | รัน test แทนการ start server |
| Override CMD ชั่วคราว | docker run my-app node -e "console.log('hello')" | รัน Node.js แบบ one-liner |
ภาพที่ 3 — EXPOSE vs -p port mapping
ภาพที่ 3: EXPOSE 3000 แค่บันทึกข้อมูล — ต้องใช้ docker run -p 8080:3000 เพื่อ map port จากเครื่อง host เข้า container จริง ๆ
ส่วนที่ 12
EXPOSE vs -p port mapping
นี่คือความเข้าใจผิดที่พบบ่อยที่สุด: "เขียน EXPOSE แล้วแอปควรเข้าถึงได้จากภายนอกเลย" — ซึ่งไม่ใช่ความจริง EXPOSE และ -p ทำงานร่วมกันแต่เป็นคนละอย่างกันโดยสิ้นเชิง
| ประเด็น | EXPOSE 3000 (ใน Dockerfile) | -p 8080:3000 (ตอน docker run) |
|---|---|---|
| ประเภท | Metadata / เอกสาร | Network configuration จริง |
| เปิด port ให้ภายนอกได้ไหม? | ไม่ — แค่บันทึกข้อมูล | ใช่ — map port จริง |
| เขียนที่ไหน | ใน Dockerfile | ใน terminal ตอนสั่ง docker run |
| ถาวรหรือชั่วคราว | ถาวร (อยู่ใน image) | ชั่วคราว (เฉพาะ container นั้น) |
| ใช้ทำอะไรได้ | บอกคนอื่น + Docker Compose ใช้อ้างอิง | ให้ browser/client เข้าถึง container ได้ |
| ตัวอย่าง | EXPOSE 3000 | docker run -p 8080:3000 my-app |
ส่วนที่ 13
ข้อผิดพลาดที่ผู้เริ่มต้นมักเจอ
รวบรวมข้อผิดพลาดที่พบบ่อยที่สุด พร้อมคำอธิบายและวิธีแก้ไข
| ข้อผิดพลาด | อาการที่เจอ | สาเหตุ | วิธีแก้ |
|---|---|---|---|
| ลืม WORKDIR | ไฟล์กระจายอยู่ใน / ปนกับไฟล์ระบบ | ไม่ได้กำหนด working directory ทำให้ทุกอย่างไปที่ root | เพิ่ม WORKDIR /app ก่อน COPY และ RUN |
| COPY path ผิด | Error: COPY failed: file not found | path ที่ระบุไม่มีจริงใน build context หรือสะกดผิด | ตรวจสอบ path สัมพัทธ์จากโฟลเดอร์ที่รัน docker build |
| เข้าใจว่า EXPOSE เปิด port แล้ว | เข้าถึง localhost:3000 ไม่ได้แม้มี EXPOSE 3000 | EXPOSE แค่บันทึก metadata ไม่ได้ publish port จริง | ใช้ docker run -p 3000:3000 my-app เพื่อ map port |
| ใช้ CMD shell form | docker stop ช้ามาก หรือ app ไม่ shutdown gracefully | CMD node server.js รันผ่าน /bin/sh ทำให้ signal ไม่ถึง node | เปลี่ยนเป็น CMD ["node", "server.js"] (exec form) |
| COPY source code ก่อน RUN install | แก้ code ทีไร ต้อง npm install ใหม่ทุกครั้ง ช้ามาก | COPY . . ก่อน RUN npm ci ทำให้ cache layer ของ npm ci ถูก invalidate | COPY package.json ก่อน → RUN npm ci → COPY . . ทีหลัง |
| RUN หลายบรรทัดแยกกัน | Image มีขนาดใหญ่กว่าที่ควรจะเป็น | แต่ละ RUN สร้าง layer ใหม่ เก็บ cache ของขั้นตอนก่อนหน้าไว้ | รวมคำสั่งด้วย && เช่น RUN apt update && apt install -y curl |
ส่วนที่ 14
สรุปท้ายบทแบบจำง่าย
บทนี้ครอบคลุม 6 instructions หลักของ Dockerfile จำประเด็นสำคัญเหล่านี้ไว้: 1. FROM — เริ่มต้นเสมอ, ระบุ tag เสมอ (เช่น node:20-alpine ไม่ใช่ node เฉย ๆ) 2. WORKDIR — ป้องกันไฟล์กระจัดกระจาย, ใช้ก่อน COPY และ RUN เสมอ 3. COPY — ย้ายไฟล์จาก host เข้า image, COPY เปลี่ยนน้อยก่อน เปลี่ยนบ่อยทีหลัง 4. RUN — รันตอน build, รวมคำสั่งด้วย && เพื่อลด layer 5. CMD — รันตอน container เริ่ม, ใช้ exec form (array) เสมอ 6. EXPOSE — แค่เอกสาร ไม่ได้ publish port จริง, ต้องใช้ -p ตอน docker run
- FROM ต้องเป็นบรรทัดแรก — เลือก base image ที่เล็กที่สุดที่ใช้งานได้
- WORKDIR /app ก่อนเสมอ — ป้องกันไฟล์ปนกับระบบ
- COPY + RUN ทำงานตอน build — CMD ทำงานตอน container รัน
- COPY เปลี่ยนน้อยก่อน (package.json) → RUN install → COPY source code — ใช้ cache ได้เต็มที่
- CMD ใช้ exec form เสมอ: CMD ["node", "server.js"] ไม่ใช่ CMD node server.js
- EXPOSE เป็นแค่ข้อมูล — ต้องใช้ docker run -p เพื่อ publish port จริง
แบบฝึกหัด
ทดสอบความเข้าใจ
ลองตอบคำถามต่อไปนี้ด้วยตัวเอง แล้วกดดูแนวเฉลยเพื่อเปรียบเทียบ