Docker for beginnersDocker สำหรับผู้เริ่มต้น
Docker Beginner Guide
Package an app once, run it the same way everywhereแพ็กแอปครั้งเดียว แล้วรันแบบเดียวกันได้ทุกที่
"It works on my machine" happens because every machine has different versions of everything. Docker packs an app together with the exact system files, runtime and libraries it needs into an image, and runs it as an isolated container that behaves the same on your laptop, a server or the cloud. In this guide a small Node.js API, hello-api, goes from a single container to an app with a Postgres database, started with one command. ปัญหา "ในเครื่องผมใช้ได้นะ" เกิดเพราะแต่ละเครื่องมีเวอร์ชันของทุกอย่างไม่เหมือนกัน Docker แพ็กแอปพร้อมไฟล์ระบบ runtime และไลบรารีที่ต้องใช้ทั้งหมดไว้ใน image แล้วรันเป็น container ที่แยกตัวออกมา และทำงานเหมือนกันทั้งบนโน้ตบุ๊ก เซิร์ฟเวอร์ และคลาวด์ ในคู่มือนี้ API เล็กๆ ที่เขียนด้วย Node.js ชื่อ hello-api จะเติบโตจาก container เดียวไปเป็นแอปที่มีฐานข้อมูล Postgres และเริ่มทำงานได้ด้วยคำสั่งเดียว
- Run withรันด้วย Docker 29.6
- Compose 5.2
- Node.js 22
- Postgres 17
ContainersContainer
Run, inspect and stop containers; images and your own Dockerfile.รัน ตรวจดู และหยุด container, image และ Dockerfile ของเราเอง
Lessons 1–5 →บทที่ 1–5 →Appsแอป
Data that survives, containers that talk, and Compose.ข้อมูลที่อยู่รอด container ที่คุยกันได้ และ Compose
Lessons 6–8 →บทที่ 6–8 →Shippingส่งขึ้นระบบ
Small images, registries, and finding out why a container died.image ขนาดเล็ก registry และการหาสาเหตุที่ container ตาย
Lessons 9–11 →บทที่ 9–11 →Lesson 1 · Containersบทที่ 1 · Container
What Docker isDocker คืออะไร
An image is a read-only package: a small Linux file system plus a command to run. A container is a running copy of an image, with its own processes, network address and writable layer, isolated from the rest of the machine. Unlike a virtual machine, a container doesn't boot its own operating system; it shares the Linux kernel of its host, so it starts in a fraction of a second. On macOS and Windows, Docker Desktop runs a small Linux virtual machine for this. image คือแพ็กเกจแบบอ่านได้อย่างเดียว ประกอบด้วยระบบไฟล์ Linux ขนาดเล็กและคำสั่งที่จะรัน container คือสำเนาของ image ที่กำลังทำงาน มี process, network address และเลเยอร์ที่เขียนได้เป็นของตัวเอง และแยกออกจากส่วนอื่นของเครื่อง container ต่างจาก virtual machine ตรงที่ไม่ต้องบูตระบบปฏิบัติการของตัวเอง แต่ใช้ kernel ของ Linux ร่วมกับเครื่องหลัก จึงเริ่มทำงานได้ในเสี้ยววินาที บน macOS และ Windows นั้น Docker Desktop จะรัน virtual machine Linux ขนาดเล็กไว้ให้
| Image | Container | |
|---|---|---|
| Is likeเปรียบได้กับ | a class, a recipe, a program fileคลาส สูตรอาหาร หรือไฟล์โปรแกรม | an object, a dish, a running programออบเจกต์ อาหารที่ทำเสร็จ หรือโปรแกรมที่กำลังรัน |
| Changes?เปลี่ยนได้ไหม | never: you build a new oneไม่เลย ต้อง build ใหม่ | yes, but changes are lost when it's removedได้ แต่การเปลี่ยนแปลงหายไปเมื่อถูกลบ |
| How manyจำนวน | oneหนึ่ง | as many as you start from itเท่าที่เราสั่งเริ่มจาก image นั้น |
$ docker version --format 'Client {{.Client.Version}}, Server {{.Server.Version}} ({{.Server.Os}}/{{.Server.Arch}})' Client 29.6.1, Server 29.6.1 (linux/arm64) $ docker run --rm hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 58dee6a49ef1: Pulling fs layer 58dee6a49ef1: Download complete 58dee6a49ef1: Pull complete c3bdf82c34d1: Download complete Digest: sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (arm64v8) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/
Docker is a client and a server. The docker command you type is the client; it asks the Docker server (the daemon) to do the work. The image wasn't on my computer, so Docker downloaded it from Docker Hub, then started a container from it. --rm removes the container when it exits.Docker มีทั้ง client และ server คำสั่ง docker ที่เราพิมพ์คือ client ซึ่งขอให้ Docker server (daemon) ทำงานให้ image ยังไม่มีในเครื่องผม Docker จึงดาวน์โหลดจาก Docker Hub แล้วเริ่ม container จาก image นั้น ส่วน --rm ลบ container เมื่อมันจบการทำงาน
Lesson 2 · Containersบทที่ 2 · Container
Running containersการรัน container
Let's run a real web server, nginx. -d runs the container in the background (detached), --name gives it a name, and -p 8089:80 publishes it: port 8089 on my computer goes to port 80 inside the container.
ลองรันเว็บเซิร์ฟเวอร์จริงอย่าง nginx กัน -d รัน container อยู่เบื้องหลัง (detached) --name ตั้งชื่อให้ และ -p 8089:80 เปิดพอร์ตออกมา คือพอร์ต 8089 บนเครื่องของผมส่งต่อไปที่พอร์ต 80 ภายใน container
$ docker run -d --name web -p 8089:80 nginx:alpine ec1a7e412e28b47276d7bf9ad843afb9af332a99db6d8f9708548014eb1c3235 $ docker ps --filter 'name=^web$' CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ec1a7e412e28 nginx:alpine "/docker-entrypoint.…" 1 second ago Up 1 second 0.0.0.0:8089->80/tcp, [::]:8089->80/tcp web $ curl -s localhost:8089 | head -n 4 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> $ docker logs web /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh /docker-entrypoint.sh: Configuration complete; ready for start up ⋯ 16 lines of nginx start-up notices ⋯⋯ 16 บรรทัด: ข้อความตอนเริ่มทำงานของ nginx ⋯ 192.168.65.1 - - [05/Oct/2026:18:26:55 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.7.1" "-" $ docker exec web nginx -v; docker exec web ls /usr/share/nginx/html nginx version: nginx/1.31.6 50x.html index.html
docker run printed the new container's ID. docker ps lists running containers; I filter by name because my computer runs other containers too. docker logs shows what the program printed: start-up messages and, last, the line for my curl. docker exec runs another command inside the running container.docker run พิมพ์ ID ของ container ใหม่ docker ps แสดงรายการ container ที่กำลังทำงาน ผมกรองด้วยชื่อเพราะในเครื่องผมมี container อื่นทำงานอยู่ด้วย docker logs แสดงสิ่งที่โปรแกรมพิมพ์ ได้แก่ข้อความตอนเริ่มทำงาน และบรรทัดสุดท้ายสำหรับ curl ของผม ส่วน docker exec รันคำสั่งอื่นภายใน container ที่กำลังทำงาน
$ docker stop web web $ docker ps -a --filter 'name=^web$' --format '{{.Names}}: {{.Status}}' web: Exited (0) Less than a second ago $ docker rm web web
A stopped container still exists (ps -a shows all, not only running ones) until you remove it.container ที่หยุดแล้วยังคงอยู่ (ps -a แสดงทั้งหมด ไม่ใช่เฉพาะที่ทำงาน) จนกว่าจะลบ
| Commandคำสั่ง | Doesทำอะไร |
|---|---|
docker run IMAGE [CMD] | create and start a container (pulls the image if needed)สร้างและเริ่ม container (ดึง image ถ้ายังไม่มี) |
-d, --name, --rm | background, a name, delete when it exitsทำงานเบื้องหลัง ตั้งชื่อ ลบเมื่อจบ |
-p HOST:CONTAINER | publish a portเปิดพอร์ตออกมา |
-e NAME=value | set an environment variableกำหนดตัวแปรสภาพแวดล้อม |
-it | interactive with a terminal: docker run -it alpine shโต้ตอบได้พร้อมเทอร์มินัล เช่น docker run -it alpine sh |
docker ps / ps -a | running / all containerscontainer ที่ทำงาน / ทั้งหมด |
docker logs -f NAME | output, -f keeps following itผลลัพธ์ และ -f ติดตามต่อเรื่อยๆ |
docker exec -it NAME sh | a shell inside a running containershell ภายใน container ที่กำลังทำงาน |
docker stop / start / rm | stop, start again, deleteหยุด เริ่มใหม่ ลบ |
Lesson 3 · Containersบทที่ 3 · Container
ImagesImage
An image name has a repository and a tag: alpine:3.22 is version 3.22 of Alpine Linux, a tiny distribution popular for containers. Without a tag Docker uses latest, which changes over time, so pin versions in real projects.
ชื่อ image ประกอบด้วย repository และ tag alpine:3.22 คือ Alpine Linux เวอร์ชัน 3.22 ซึ่งเป็นดิสโทรขนาดเล็กที่นิยมใช้กับ container ถ้าไม่ใส่ tag Docker จะใช้ latest ซึ่งเปลี่ยนไปตามเวลา ในโปรเจกต์จริงจึงควรระบุเวอร์ชัน
$ docker pull alpine:3.22 3.22: Pulling from library/alpine 16fc4f52163f: Pulling fs layer 37a98a5c0ed3: Download complete 237cf59f8e65: Download complete 16fc4f52163f: Download complete 16fc4f52163f: Pull complete Digest: sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 Status: Downloaded newer image for alpine:3.22 docker.io/library/alpine:3.22 $ docker image ls alpine IMAGE ID DISK USAGE CONTENT SIZE EXTRA alpine:3.22 5291449c3df7 13.4MB 4.21MB $ docker run --rm alpine:3.22 cat /etc/os-release NAME="Alpine Linux" ID=alpine VERSION_ID=3.22.6 PRETTY_NAME="Alpine Linux v3.22" HOME_URL="https://alpinelinux.org/" BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
CONTENT SIZE is what gets downloaded (compressed); DISK USAGE is the size once unpacked. The image is a whole small Linux system: the container ran Alpine's own cat on Alpine's own /etc/os-release, not my Mac's.CONTENT SIZE คือขนาดที่ดาวน์โหลด (แบบบีบอัด) ส่วน DISK USAGE คือขนาดเมื่อแตกไฟล์แล้ว image คือระบบ Linux ขนาดเล็กทั้งระบบ container รัน cat ของ Alpine เองกับไฟล์ /etc/os-release ของ Alpine เอง ไม่ใช่ของ Mac ผม
The image never changes. Each container gets a thin writable layer on top, and that layer disappears with the container: image ไม่เคยเปลี่ยน container แต่ละตัวได้เลเยอร์บางๆ ที่เขียนได้วางทับไว้ด้านบน และเลเยอร์นั้นหายไปพร้อมกับ container
$ docker run --name box alpine:3.22 sh -c 'echo hello > /note.txt; cat /note.txt' hello $ docker run --rm alpine:3.22 cat /note.txt cat: can't open '/note.txt': No such file or directory $ docker diff box A /note.txt $ docker rm box box $ docker history alpine:3.22 IMAGE CREATED CREATED BY SIZE COMMENT 5291449c3df7 2 weeks ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0 <missing> 2 weeks ago ADD alpine-minirootfs-3.22.6-aarch64.tar.gz … 9.2MB buildkit.dockerfile.v0
A new container from the same image doesn't have /note.txt. docker diff shows what one container changed: A means added. docker history shows the layers an image was built from; Alpine has one real layer of 9.2 MB.container ใหม่จาก image เดียวกันไม่มี /note.txt docker diff แสดงสิ่งที่ container หนึ่งเปลี่ยนไป A แปลว่าเพิ่ม ส่วน docker history แสดง เลเยอร์ ที่ใช้สร้าง image นั้น Alpine มีเลเยอร์จริงเพียงชั้นเดียว ขนาด 9.2 MB
Lesson 4 · Containersบทที่ 4 · Container
Your own imageImage ของเราเอง
A Dockerfile is the recipe for an image: start FROM an existing image, copy files in, run commands, and say which command starts the app. Here is a tiny Node.js API with no dependencies, and its Dockerfile.
Dockerfile คือสูตรของ image เริ่มจาก (FROM) image ที่มีอยู่แล้ว คัดลอกไฟล์เข้าไป รันคำสั่ง และบอกว่าคำสั่งใดเริ่มแอป นี่คือ API ขนาดเล็กที่เขียนด้วย Node.js ไม่มี dependency และ Dockerfile ของมัน
| Instructionคำสั่ง | Meansความหมาย |
|---|---|
FROM node:22-alpine | start from the official Node.js 22 image on Alpineเริ่มจาก image ทางการของ Node.js 22 บน Alpine |
WORKDIR /app | make /app and work in it (like cd)สร้าง /app แล้วทำงานในนั้น (เหมือน cd) |
COPY src dest | copy files from your folder into the imageคัดลอกไฟล์จากโฟลเดอร์ของเราเข้าไปใน image |
RUN cmd | run a command while building (install packages)รันคำสั่งระหว่าง build (เช่น ติดตั้งแพ็กเกจ) |
EXPOSE 3000 | documents the port; it does not publish it (-p does)บอกให้รู้ว่าใช้พอร์ตนี้ แต่ไม่ได้เปิดพอร์ต (ต้องใช้ -p) |
CMD ["node", "server.js"] | the command a container runs when it startsคำสั่งที่ container รันเมื่อเริ่มทำงาน |
$ docker build --no-cache --progress=plain -t hello-api:1 . #0 building with "desktop-linux" instance using docker driver #1 [internal] load build definition from Dockerfile #1 transferring dockerfile: 139B done #1 DONE 0.0s #2 [internal] load metadata for docker.io/library/node:22-alpine #2 DONE 0.0s #3 [internal] load .dockerignore #3 transferring context: 2B done #3 DONE 0.0s #4 [internal] load build context #4 transferring context: 624B done #4 DONE 0.0s #5 [1/3] FROM docker.io/library/node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 #5 resolve docker.io/library/node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 done #5 DONE 0.0s #6 [2/3] WORKDIR /app #6 CACHED #7 [3/3] COPY package.json server.js ./ #7 DONE 0.0s #8 exporting to image #8 exporting layers 0.0s done #8 exporting manifest sha256:df93c340c9725447f1a01f7fac3717547dffa27de31ebd29f1b6796ba0bdba53 done #8 exporting config sha256:a5781261b1cef204a512e16cb4bcbd9b33bace4948d0e05c709228d81dbf4a6d done #8 exporting attestation manifest sha256:4ae603bdbd7640f857fd919ceec681206d673a686296cdf869e0d8f7b14384ec done #8 exporting manifest list sha256:4db6bcf1e38db855d6b7c74d4067f22cb70db068e7ced45529f8c9df3a187c63 done #8 naming to docker.io/library/hello-api:1 done #8 unpacking to docker.io/library/hello-api:1 done #8 DONE 0.1s View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/jv68v58n1hl0wgvrb785sqhsz
-t names the image hello-api with tag 1; . is the build context, the folder whose files COPY can use. --progress=plain prints every step instead of a live display. I added --no-cache only because I had built this image before; it makes Docker run every step, as your first build will.-t ตั้งชื่อ image ว่า hello-api พร้อม tag 1 ส่วน . คือ build context หรือโฟลเดอร์ที่ COPY ใช้ไฟล์ได้ --progress=plain พิมพ์ทุกขั้นแทนการแสดงผลแบบสด ผมเพิ่ม --no-cache เพียงเพราะเคย build image นี้มาก่อน มันทำให้ Docker รันทุกขั้น เหมือนการ build ครั้งแรกของคุณ
$ docker run -d --name hello -p 8088:3000 hello-api:1 9e270007b37d1776d337099e3a4ea6e7007b9576e93f712368f3213dcd50a7e4 $ curl -s localhost:8088 {"message":"Hello from a container","host":"9e270007b37d"} $ docker logs hello listening on port 3000 GET / $ time docker stop hello hello real 0m10.232s user 0m0.008s sys 0m0.010s $ docker rm hello hello
The host in the reply is the container's hostname, which is the start of its ID. Now look at time: stopping took 10 seconds. docker stop sends the program a SIGTERM signal and waits 10 s for it to exit before killing it. Node.js running as the container's main process ignores SIGTERM unless the code handles it. Lesson 5 fixes that.host ในคำตอบคือ hostname ของ container ซึ่งเป็นส่วนต้นของ ID ต่อมาดูที่ time การหยุดใช้เวลาถึง 10 วินาที docker stop ส่งสัญญาณ SIGTERM ให้โปรแกรม แล้วรอ 10 วินาทีให้มันจบก่อนจะฆ่าทิ้ง Node.js ที่รันเป็น process หลักของ container จะไม่สนใจ SIGTERM ถ้าโค้ดไม่ได้จัดการไว้ บทที่ 5 จะแก้เรื่องนี้
Lesson 5 · Containersบทที่ 5 · Container
Layers and the build cacheเลเยอร์และ build cache
The API now counts visits in Postgres, so it needs the pg package from npm. Each Dockerfile instruction makes a layer, and Docker reuses a layer from its cache as long as the instruction and the files it uses haven't changed. Once one layer changes, every layer after it is rebuilt. So the order matters: copy package.json and install packages first (they rarely change), and copy your code last (it changes all the time).
ตอนนี้ API นับจำนวนการเข้าชมใน Postgres จึงต้องใช้แพ็กเกจ pg จาก npm คำสั่งแต่ละบรรทัดใน Dockerfile สร้าง เลเยอร์ หนึ่งชั้น และ Docker นำเลเยอร์จาก cache มาใช้ซ้ำได้ตราบใดที่คำสั่งและไฟล์ที่ใช้ไม่เปลี่ยน เมื่อเลเยอร์หนึ่งเปลี่ยน เลเยอร์ทั้งหมดหลังจากนั้นจะถูก build ใหม่ ลำดับจึงสำคัญ ให้คัดลอก package.json และติดตั้งแพ็กเกจก่อน (เพราะไม่ค่อยเปลี่ยน) แล้วคัดลอกโค้ดเป็นลำดับสุดท้าย (เพราะเปลี่ยนตลอด)
.dockerignore keeps files out of the build context, like .gitignore: my local node_modules (built for macOS, not Linux) and the .git history. USER node runs the app as an ordinary user instead of root. And the SIGTERM handler lets docker stop finish quickly (Lesson 8).
.dockerignore กันไฟล์ไม่ให้เข้าไปใน build context คล้าย .gitignore ในที่นี้คือ node_modules ในเครื่อง (ซึ่ง build สำหรับ macOS ไม่ใช่ Linux) และประวัติ .git ส่วน USER node รันแอปด้วยผู้ใช้ธรรมดาแทน root และตัวจัดการ SIGTERM ทำให้ docker stop จบได้เร็ว (บทที่ 8)
$ docker build --no-cache --progress=plain -t hello-api:2 . ⋯ 17 more lines ⋯⋯ อีก 17 บรรทัด ⋯ #5 [1/5] FROM docker.io/library/node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 #5 resolve docker.io/library/node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 done #5 DONE 0.0s #6 [2/5] WORKDIR /app #6 CACHED #7 [3/5] COPY package.json package-lock.json ./ #7 DONE 0.0s #8 [4/5] RUN npm ci --omit=dev #8 0.721 #8 0.721 added 14 packages, and audited 15 packages in 567ms #8 0.721 #8 0.721 found 0 vulnerabilities #8 DONE 0.8s #9 [5/5] COPY . . #9 DONE 0.0s ⋯ 12 more lines ⋯⋯ อีก 12 บรรทัด ⋯ $ echo "// lesson 5: a small change" >> server.js $ docker build --progress=plain -t hello-api:2 . ⋯ 17 more lines ⋯⋯ อีก 17 บรรทัด ⋯ #5 [1/5] FROM docker.io/library/node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 #5 resolve docker.io/library/node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 done #5 DONE 0.0s #6 [2/5] WORKDIR /app #6 CACHED #7 [3/5] COPY package.json package-lock.json ./ #7 CACHED #8 [4/5] RUN npm ci --omit=dev #8 CACHED #9 [5/5] COPY . . #9 DONE 0.0s ⋯ 12 more lines ⋯⋯ อีก 12 บรรทัด ⋯
After changing server.js, the package steps say CACHED and only COPY . . ran again. With dozens of packages, that's the difference between seconds and minutes on every build.หลังแก้ server.js ขั้นตอนเรื่องแพ็กเกจขึ้นว่า CACHED และมีเพียง COPY . . ที่รันใหม่ ถ้ามีแพ็กเกจหลายสิบตัว นี่คือความต่างระหว่างไม่กี่วินาทีกับหลายนาทีในทุกครั้งที่ build
$ docker image ls hello-api IMAGE ID DISK USAGE CONTENT SIZE EXTRA hello-api:1 4db6bcf1e38d 234MB 61.1MB hello-api:2 7d72eed71e30 239MB 61.8MB $ docker history hello-api:2 --format 'table {{.CreatedBy}}\t{{.Size}}' | cut -c1-80 CREATED BY SIZE CMD ["node" "server.js"] 0B EXPOSE [3000/tcp] 0B USER node 0B COPY . . # buildkit 24.6kB RUN /bin/sh -c npm ci --omit=dev # buildkit 4.15MB COPY package.json package-lock.json ./ # bui… 20.5kB WORKDIR /app 8.19kB CMD ["node"] 0B ENTRYPOINT ["docker-entrypoint.sh"] 0B COPY docker-entrypoint.sh /usr/local/bin/ # … 20.5kB RUN /bin/sh -c apk add --no-cache --virtual … 5.48MB ENV YARN_VERSION=1.22.22 0B RUN /bin/sh -c addgroup -g 1000 node && … 158MB ENV NODE_VERSION=22.23.3 0B CMD ["/bin/sh"] 0B ADD alpine-minirootfs-3.24.2-aarch64.tar.gz … 9.31MB
Read the history bottom up: Alpine, then Node.js (158 MB, most of the image), then my layers. npm ci added 4.15 MB and my code 24.6 kB. Two images that start FROM node:22-alpine share those base layers on disk.อ่านประวัติจากล่างขึ้นบน: Alpine แล้ว Node.js (158 MB ซึ่งเป็นส่วนใหญ่ของ image) แล้วจึงเป็นเลเยอร์ของผม npm ci เพิ่ม 4.15 MB และโค้ดของผม 24.6 kB image สองตัวที่เริ่มจาก FROM node:22-alpine ใช้เลเยอร์พื้นฐานเหล่านั้นร่วมกันบนดิสก์
Lesson 6 · Appsบทที่ 6 · แอป
Volumes and bind mountsVolume และ bind mount
A container's writable layer is deleted with the container, which is a problem for a database. A volume is storage managed by Docker that lives on after the container. -v notes-data:/var/lib/postgresql/data mounts the volume notes-data where Postgres keeps its files.
เลเยอร์ที่เขียนได้ของ container ถูกลบไปพร้อมกับ container ซึ่งเป็นปัญหาสำหรับฐานข้อมูล volume คือพื้นที่เก็บข้อมูลที่ Docker จัดการให้และยังอยู่หลัง container ถูกลบ -v notes-data:/var/lib/postgresql/data mount volume ชื่อ notes-data ไว้ตรงที่ Postgres เก็บไฟล์ของมัน
$ docker run -d --name pg -e POSTGRES_PASSWORD=secret -v notes-data:/var/lib/postgresql/data postgres:17-alpine ad8ef5fee54750b2abe4184df760634bb6a863d1b13b6b58141c5c85e6da190c $ docker exec pg pg_isready -U postgres /var/run/postgresql:5432 - accepting connections $ docker exec pg psql -U postgres -c "CREATE TABLE notes (body text); INSERT INTO notes VALUES ('saved in a volume');" CREATE TABLE INSERT 0 1 $ docker rm -f pg pg $ docker run -d --name pg2 -e POSTGRES_PASSWORD=secret -v notes-data:/var/lib/postgresql/data postgres:17-alpine fb0a9f03861b24f95e4c1f218c5ac0ec92a4543d618b4740d0629e619ba3a864 $ docker exec pg2 psql -U postgres -c "SELECT * FROM notes;" body ------------------- saved in a volume (1 row) $ docker volume ls --filter name=notes-data DRIVER VOLUME NAME local notes-data
The first container was deleted (-f stops it first), yet a brand-new container found the row, because the data was in the volume. Remove a volume with docker volume rm when you really want the data gone.container ตัวแรกถูกลบไปแล้ว (-f หยุดมันก่อน) แต่ container ตัวใหม่ยังเจอแถวข้อมูล เพราะข้อมูลอยู่ใน volume ถ้าต้องการลบข้อมูลจริงๆ ให้ลบ volume ด้วย docker volume rm
A bind mount puts a folder from your own computer inside the container instead. Edits on your side show up immediately, which is handy in development: bind mount นำโฟลเดอร์จากเครื่องของเราเข้าไปใน container แทน สิ่งที่เราแก้ฝั่งเราจะปรากฏทันที ซึ่งสะดวกตอนพัฒนา
$ docker run -d --name site -p 8089:80 -v "$PWD/site:/usr/share/nginx/html:ro" nginx:alpine 0726c82c05ec134db8e6d9217f0e51b52e28d737f88cf384743209a52488daee $ curl -s localhost:8089 <h1>Hello from a folder on my computer</h1> $ echo '<h1>Edited, no rebuild needed</h1>' > site/index.html $ curl -s localhost:8089 <h1>Edited, no rebuild needed</h1>
:ro makes the mount read-only for the container. Use volumes for data the app owns (databases, uploads) and bind mounts for files you edit.:ro ทำให้ container อ่านได้อย่างเดียว ใช้ volume กับข้อมูลที่แอปเป็นเจ้าของ (ฐานข้อมูล ไฟล์ที่อัปโหลด) และใช้ bind mount กับไฟล์ที่เราแก้ไขเอง
Lesson 7 · Appsบทที่ 7 · แอป
Networksเครือข่าย
Containers on the same user-defined network can reach each other by name: Docker runs a small DNS server for them. Let's put the database and the API on a network called appnet. The API finds the database at the host name db.
container ที่อยู่บน เครือข่าย เดียวกันซึ่งเราสร้างเอง ติดต่อกันได้ด้วยชื่อ เพราะ Docker รัน DNS เซิร์ฟเวอร์ขนาดเล็กไว้ให้ ลองนำฐานข้อมูลและ API ไปไว้บนเครือข่ายชื่อ appnet API จะหาฐานข้อมูลเจอที่ชื่อโฮสต์ db
$ docker network create appnet 1b24e46bfb94b1f2faa977e889ecc8ff687d82e6b4a5e10818bb556d39337627 $ docker run -d --name db --network appnet -e POSTGRES_USER=app -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=app postgres:17-alpine 590ca532fe27231beca6e6ed67806d29e84fe31d9f0abde4cb814b5ecf33c0f4 $ docker run -d --name api --network appnet -p 8088:3000 -e DATABASE_URL=postgres://app:secret@db:5432/app hello-api:2 2da16fffb48abe611e90f9f156d7fa8b4d2f1771fc065401a3916fc9ba18e1cf $ curl -s localhost:8088; curl -s localhost:8088 {"visit":1,"host":"2da16fffb48a"} {"visit":2,"host":"2da16fffb48a"} $ docker run --rm --network appnet curlimages/curl -s http://api:3000/ {"visit":3,"host":"2da16fffb48a"} $ docker run --rm --network appnet alpine:3.22 nslookup db Server: 127.0.0.11 Address: 127.0.0.11:53 Non-authoritative answer: Non-authoritative answer: Name: db Address: 172.19.0.2
From my computer I use the published port, localhost:8088. Another container on appnet uses the name and the container's own port, api:3000. The DNS server at 127.0.0.11 is Docker's; db resolves to the database container's private address.จากเครื่องของผมใช้พอร์ตที่เปิดออกมาคือ localhost:8088 ส่วน container อื่นบน appnet ใช้ชื่อและพอร์ตของ container เองคือ api:3000 DNS เซิร์ฟเวอร์ที่ 127.0.0.11 เป็นของ Docker และ db แปลงเป็น address ส่วนตัวของ container ฐานข้อมูล
Inside a container, localhost means that container, not your computer and not the other containers. Here is the same API told to find its database at localhost:
ภายใน container คำว่า localhost หมายถึง container ตัวนั้นเอง ไม่ใช่เครื่องของเรา และไม่ใช่ container อื่น นี่คือ API ตัวเดิมที่ถูกบอกให้หาฐานข้อมูลที่ localhost
$ docker run -d --name api-local --network appnet -e DATABASE_URL=postgres://app:secret@localhost:5432/app hello-api:2 551cb03b2e6fe92358114e33122b89b23605b7d805ff7204363cf30073c8012b $ docker ps -a --filter 'name=^api-local$' --format '{{.Names}}: {{.Status}}' api-local: Exited (1) 3 seconds ago $ docker logs api-local /app/node_modules/pg-pool/index.js:45 Error.captureStackTrace(err) ^ AggregateError [ECONNREFUSED]: at /app/node_modules/pg-pool/index.js:45:11 at process.processTicksAndRejections (node:internal/process/task_queues:103:5) at async file:///app/server.js:8:1 { code: 'ECONNREFUSED', [errors]: [ Error: connect ECONNREFUSED ::1:5432 at createConnectionError (node:net:1679:14) at afterConnectMultiple (node:net:1709:16) { errno: -111, code: 'ECONNREFUSED', syscall: 'connect', address: '::1', port: 5432 }, Error: connect ECONNREFUSED 127.0.0.1:5432 at createConnectionError (node:net:1679:14) at afterConnectMultiple (node:net:1709:16) { errno: -111, code: 'ECONNREFUSED', syscall: 'connect', address: '127.0.0.1', port: 5432 } ] } Node.js v22.23.3 $ docker rm -f api api-local db; docker network rm appnet api api-local db appnet
ECONNREFUSED 127.0.0.1:5432: nothing listens on port 5432 inside the API's own container, so it crashed with exit code 1.ECONNREFUSED 127.0.0.1:5432 แปลว่าไม่มีอะไร listen บนพอร์ต 5432 ภายใน container ของ API เอง มันจึงพังด้วย exit code 1
Lesson 8 · Appsบทที่ 8 · แอป
Docker ComposeDocker Compose
Typing those docker run lines every time is error-prone. Docker Compose describes the whole app in one YAML file, compose.yaml: its services, how to build them, ports, environment variables, volumes, and the order to start them in. Compose also creates a network for the app, so services reach each other by their service names.
การพิมพ์ docker run ยาวๆ เหล่านั้นทุกครั้งเสี่ยงต่อความผิดพลาด Docker Compose อธิบายแอปทั้งหมดไว้ในไฟล์ YAML ไฟล์เดียวคือ compose.yaml ได้แก่ service ต่างๆ วิธี build พอร์ต ตัวแปรสภาพแวดล้อม volume และลำดับการเริ่มทำงาน Compose ยังสร้างเครือข่ายให้แอป service ต่างๆ จึงติดต่อกันด้วยชื่อ service ได้
depends_on with condition: service_healthy starts the API only after the database's healthcheck passes, so the API doesn't crash on start-up as it did above. pgdata is a named volume that Compose manages.
depends_on ที่มี condition: service_healthy จะเริ่ม API หลังจาก healthcheck ของฐานข้อมูลผ่านแล้วเท่านั้น API จึงไม่พังตอนเริ่มทำงานเหมือนข้างบน ส่วน pgdata คือ volume ที่มีชื่อซึ่ง Compose จัดการให้
$ docker compose build Image hello-api-api Building ⋯ 23 more lines ⋯⋯ อีก 23 บรรทัด ⋯ #7 [2/5] WORKDIR /app #7 CACHED #8 [3/5] COPY package.json package-lock.json ./ #8 CACHED #9 [4/5] RUN npm ci --omit=dev #9 CACHED #10 [5/5] COPY . . #10 CACHED ⋯ 13 more lines ⋯⋯ อีก 13 บรรทัด ⋯ Image hello-api-api Built $ docker compose up -d Network hello-api_default Creating Network hello-api_default Created Volume hello-api_pgdata Creating Volume hello-api_pgdata Created Container hello-api-db-1 Creating Container hello-api-db-1 Created Container hello-api-api-1 Creating Container hello-api-api-1 Created Container hello-api-db-1 Starting Container hello-api-db-1 Started Container hello-api-db-1 Waiting Container hello-api-db-1 Healthy Container hello-api-api-1 Starting Container hello-api-api-1 Started $ docker compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS hello-api-api-1 hello-api-api "docker-entrypoint.s…" api 3 seconds ago Up Less than a second 0.0.0.0:8088->3000/tcp, [::]:8088->3000/tcp hello-api-db-1 postgres:17-alpine "docker-entrypoint.s…" db 3 seconds ago Up 3 seconds (healthy) 5432/tcp $ curl -s localhost:8088; curl -s localhost:8088 {"visit":1,"host":"c72152db377c"} {"visit":2,"host":"c72152db377c"} $ docker compose logs api api-1 | listening on port 3000 api-1 | GET / api-1 | GET / $ docker compose exec db psql -U app -c "SELECT count(*) FROM visits;" count ------- 2 (1 row)
The build reused every layer from Lesson 5. up waited until the database was Healthy before starting the API. Container names are project-service-number; the project name comes from the folder.การ build ใช้ทุกเลเยอร์ซ้ำจากบทที่ 5 คำสั่ง up รอจนฐานข้อมูลเป็น Healthy ก่อนเริ่ม API ชื่อ container อยู่ในรูป project-service-number โดยชื่อโปรเจกต์มาจากชื่อโฟลเดอร์
Remember the 10-second stop in Lesson 4? This API handles SIGTERM: จำการหยุดที่ใช้เวลา 10 วินาทีในบทที่ 4 ได้ไหม API ตัวนี้จัดการ SIGTERM แล้ว
$ time docker compose stop api Container hello-api-api-1 Stopping Container hello-api-api-1 Stopped real 0m0.289s user 0m0.035s sys 0m0.023s $ docker compose logs api | tail -n 2 api-1 | GET / api-1 | SIGTERM received, shutting down $ docker compose down Container hello-api-api-1 Stopping Container hello-api-api-1 Stopped Container hello-api-api-1 Removing Container hello-api-api-1 Removed Container hello-api-db-1 Stopping Container hello-api-db-1 Stopped Container hello-api-db-1 Removing Container hello-api-db-1 Removed Network hello-api_default Removing Network hello-api_default Removed $ docker compose up -d Network hello-api_default Creating Network hello-api_default Created Container hello-api-db-1 Creating Container hello-api-db-1 Created Container hello-api-api-1 Creating Container hello-api-api-1 Created Container hello-api-db-1 Starting Container hello-api-db-1 Started Container hello-api-db-1 Waiting Container hello-api-db-1 Healthy Container hello-api-api-1 Starting Container hello-api-api-1 Started $ curl -s localhost:8088 {"visit":3,"host":"a8877252112e"} $ docker compose down -v Container hello-api-api-1 Stopping Container hello-api-api-1 Stopped Container hello-api-api-1 Removing Container hello-api-api-1 Removed Container hello-api-db-1 Stopping Container hello-api-db-1 Stopped Container hello-api-db-1 Removing Container hello-api-db-1 Removed Network hello-api_default Removing Volume hello-api_pgdata Removing Volume hello-api_pgdata Removed Network hello-api_default Removed
0.3 seconds instead of 10. down removed the containers and the network but kept the volume, so after up the count went on to visit 3. down -v also deletes volumes: use it when you want a clean start.0.3 วินาทีแทนที่จะเป็น 10 down ลบ container และเครือข่าย แต่เก็บ volume ไว้ หลัง up การนับจึงต่อไปเป็นการเข้าชมครั้งที่ 3 ส่วน down -v ลบ volume ด้วย ใช้เมื่อต้องการเริ่มใหม่ทั้งหมด
| Commandคำสั่ง | Doesทำอะไร |
|---|---|
docker compose up -d | build if needed, create and start everythingbuild ถ้าจำเป็น แล้วสร้างและเริ่มทุกอย่าง |
docker compose up -d --build | rebuild images first (after you change code)build image ใหม่ก่อน (หลังแก้โค้ด) |
docker compose ps / logs -f | status / follow the outputสถานะ / ติดตามผลลัพธ์ |
docker compose exec SERVICE cmd | run a command in a serviceรันคำสั่งใน service |
docker compose down / down -v | remove containers and network / and volumesลบ container และเครือข่าย / รวมถึง volume |
The password is written in compose.yaml to keep the example short. In a real project, write ${POSTGRES_PASSWORD} in the file and keep the value in a .env file next to it (which Compose reads automatically) that is never committed.รหัสผ่านถูกเขียนไว้ใน compose.yaml เพื่อให้ตัวอย่างสั้น ในโปรเจกต์จริงให้เขียน ${POSTGRES_PASSWORD} ในไฟล์ แล้วเก็บค่าไว้ในไฟล์ .env ข้างๆ (Compose อ่านให้อัตโนมัติ) และห้าม commit ไฟล์นั้น
Lesson 9 · Shippingบทที่ 9 · ส่งขึ้นระบบ
Small images with multi-stage buildsimage ขนาดเล็กด้วย multi-stage build
Tools you need to build an app (compilers, dev packages) aren't needed to run it. A multi-stage build builds in one stage and copies only the result into a small final stage. Go makes this dramatic: it compiles to a single file, so the final image can start FROM scratch, an image with nothing in it at all.
เครื่องมือที่ต้องใช้ตอน build แอป (คอมไพเลอร์ แพ็กเกจสำหรับพัฒนา) ไม่จำเป็นตอน รัน แอป multi-stage build จะ build ใน stage หนึ่ง แล้วคัดลอกเฉพาะผลลัพธ์ไปไว้ใน stage สุดท้ายที่เล็กกว่า Go ทำให้เห็นผลชัดมาก เพราะคอมไพล์ออกมาเป็นไฟล์เดียว image สุดท้ายจึงเริ่ม FROM scratch ได้ ซึ่งเป็น image ที่ว่างเปล่าโดยสิ้นเชิง
$ docker build --no-cache --progress=plain -t hello-go . ⋯ 13 more lines ⋯⋯ อีก 13 บรรทัด ⋯ #4 [build 1/4] FROM docker.io/library/golang:1.25-alpine@sha256:1ae0735f00daffa3aaf1363a5184c0d2dc55c78e3db4ec70241cdac97bf84b59 #4 resolve docker.io/library/golang:1.25-alpine@sha256:1ae0735f00daffa3aaf1363a5184c0d2dc55c78e3db4ec70241cdac97bf84b59 done #4 DONE 0.0s #5 [build 2/4] WORKDIR /src #5 CACHED #6 [internal] load build context #6 transferring context: 54B done #6 DONE 0.0s #7 [build 3/4] COPY go.mod main.go ./ #7 DONE 0.0s #8 [build 4/4] RUN CGO_ENABLED=0 go build -o /hello . #8 DONE 3.4s #9 [stage-1 1/1] COPY --from=build /hello /hello #9 DONE 0.0s ⋯ 13 more lines ⋯⋯ อีก 13 บรรทัด ⋯ $ docker image ls --format 'table {{.Repository}}:{{.Tag}}\t{{.Size}}' | grep -E 'REPOSITORY|hello-go|hello-api:2|golang:1.25|node:22-alpine' REPOSITORY:TAG SIZE hello-go:latest 11.7MB hello-api:2 239MB node:22-alpine 234MB golang:1.25-alpine 324MB $ docker run -d --name go -p 8090:8080 hello-go b3358afbe452fae4597dfb11c1c5e153314b23c7ae232dcf46d260205379498e $ curl -s localhost:8090 hello from Go on b3358afbe452 $ docker export go | tar -tv -rwxr-xr-x 0 0 0 0 Oct 6 01:30 .dockerenv drwxr-xr-x 0 0 0 0 Oct 6 01:30 dev/ -rwxr-xr-x 0 0 0 0 Oct 6 01:30 dev/console drwxr-xr-x 0 0 0 0 Oct 6 01:30 dev/pts/ drwxr-xr-x 0 0 0 0 Oct 6 01:30 dev/shm/ drwxr-xr-x 0 0 0 0 Oct 6 01:30 etc/ -rwxr-xr-x 0 0 0 0 Oct 6 01:30 etc/hostname -rwxr-xr-x 0 0 0 0 Oct 6 01:30 etc/hosts lrwxrwxrwx 0 0 0 0 Oct 6 01:30 etc/mtab -> /proc/mounts -rwxr-xr-x 0 0 0 0 Oct 6 01:30 etc/resolv.conf -rwxr-xr-x 0 0 0 7501843 Oct 6 01:30 hello drwxr-xr-x 0 0 0 0 Oct 6 01:30 proc/ drwxr-xr-x 0 0 0 0 Oct 6 01:30 sys/
The build stage used golang:1.25-alpine (324 MB); the final image is 11.7 MB. docker export lists every file in the container: the 7.5 MB program hello, plus the few files Docker itself adds. No shell and no package manager, which also means less for an attacker to use.stage สำหรับ build ใช้ golang:1.25-alpine (324 MB) ส่วน image สุดท้ายมีขนาดเพียง 11.7 MB docker export แสดงทุกไฟล์ใน container คือโปรแกรม hello ขนาด 7.5 MB และไฟล์ไม่กี่ไฟล์ที่ Docker เพิ่มเอง ไม่มี shell และไม่มีตัวจัดการแพ็กเกจ ซึ่งหมายความว่าผู้โจมตีมีของให้ใช้น้อยลงด้วย
Node.js and Python apps can't use scratch because they need their runtime, but the idea still helps: build or compile in a full image (with dev dependencies, TypeScript, a C compiler), then copy only the output and production packages into a slim -alpine or "distroless" image.แอป Node.js และ Python ใช้ scratch ไม่ได้ เพราะต้องมี runtime แต่แนวคิดเดียวกันยังช่วยได้ ให้ build หรือคอมไพล์ใน image เต็ม (ที่มี dev dependency, TypeScript หรือคอมไพเลอร์ C) แล้วคัดลอกเฉพาะผลลัพธ์และแพ็กเกจสำหรับ production ไปไว้ใน image แบบ -alpine หรือ "distroless" ที่เล็กกว่า
Lesson 10 · Shippingบทที่ 10 · ส่งขึ้นระบบ
RegistriesRegistry
A registry stores images so other machines can pull them: Docker Hub, GitHub Container Registry (ghcr.io), or your cloud's (Amazon ECR, Azure ACR, Google Artifact Registry). The registry is part of the image name: ghcr.io/me/hello-api:2. To try it without an account, run the open-source registry as a container on port 5001.
registry เก็บ image ไว้ให้เครื่องอื่นดึงไปใช้ได้ เช่น Docker Hub, GitHub Container Registry (ghcr.io) หรือ registry ของคลาวด์ (Amazon ECR, Azure ACR, Google Artifact Registry) ชื่อ registry เป็นส่วนหนึ่งของชื่อ image เช่น ghcr.io/me/hello-api:2 เพื่อทดลองโดยไม่ต้องมีบัญชี ให้รัน registry แบบโอเพนซอร์สเป็น container บนพอร์ต 5001
$ docker run -d --name registry -p 5001:5000 registry:2 Unable to find image 'registry:2' locally 2: Pulling from library/registry ⋯ 17 more lines ⋯⋯ อีก 17 บรรทัด ⋯ Digest: sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373 Status: Downloaded newer image for registry:2 b73b5b2abf1ba60a1cb79c12d9a74d4875cc7f7fea80ba6371bf0fa5d4f3a5d2 $ docker tag hello-api:2 localhost:5001/hello-api:2 $ docker push localhost:5001/hello-api:2 The push refers to repository [localhost:5001/hello-api] ⋯ 54 progress lines (Waiting…) ⋯⋯ 54 บรรทัดแสดงความคืบหน้า (Waiting…) ⋯ c212ba35e03c: Pushed f0e7332f7220: Pushed a9986cd6f37d: Pushed df8b6290136c: Pushed 56e50d1ecc9d: Pushed f0657b07eea7: Pushed 74849c662da7: Pushed 66fa63c7a061: Pushed 87919ed734a3: Pushed 2: digest: sha256:7d72eed71e30a5f956d5d2a59f0673210bc5b1f259c22650b41eb83337fa07c5 size: 856 $ curl -s localhost:5001/v2/_catalog {"repositories":["hello-api"]} $ docker image rm localhost:5001/hello-api:2 Untagged: localhost:5001/hello-api:2 $ docker pull localhost:5001/hello-api:2 2: Pulling from hello-api Digest: sha256:7d72eed71e30a5f956d5d2a59f0673210bc5b1f259c22650b41eb83337fa07c5 Status: Downloaded newer image for localhost:5001/hello-api:2 localhost:5001/hello-api:2
docker tag only adds a second name. push uploaded the image layer by layer, and the digest (sha256:…) is a fingerprint of its exact content. Pulling by digest always gives the same bytes, even if someone moves the tag later.docker tag แค่เพิ่มชื่อที่สอง push อัปโหลด image ทีละเลเยอร์ ส่วน digest (sha256:…) คือลายนิ้วมือของเนื้อหาที่แน่นอน การ pull ด้วย digest จะได้ไบต์เดิมเสมอ แม้ภายหลังจะมีคนย้าย tag ไปที่อื่น
| Real registryregistry จริง | Log in, then pushล็อกอินแล้ว push |
|---|---|
| Docker Hub | docker login, docker push me/hello-api:2 |
| GitHub | docker login ghcr.io, docker push ghcr.io/me/hello-api:2 |
| In CI/CDใน CI/CD | the pipeline builds, tags with the commit hash, and pushes on every mergepipeline จะ build ติด tag ด้วย hash ของ commit แล้ว push ทุกครั้งที่ merge |
Lesson 11 · Shippingบทที่ 11 · ส่งขึ้นระบบ
Debugging containersการดีบัก container
A container lives exactly as long as its main process. When the process exits, the container stops, and the exit code tells you how it ended: 0 is success; anything else is an error code chosen by the program. 137 means it was killed (out of memory, or docker stop ran out of patience).
container มีชีวิตอยู่เท่ากับ process หลักของมันพอดี เมื่อ process จบ container ก็หยุด และ exit code บอกว่าจบอย่างไร 0 คือสำเร็จ ค่าอื่นคือรหัสข้อผิดพลาดที่โปรแกรมเลือก ส่วน 137 แปลว่าถูกฆ่า (หน่วยความจำหมด หรือ docker stop รอจนหมดความอดทน)
$ docker run --name crash alpine:3.22 sh -c 'echo "about to fail"; exit 3' about to fail $ docker ps -a --filter 'name=^crash$' --format '{{.Names}}: {{.Status}}' crash: Exited (3) Less than a second ago $ docker inspect crash --format '{{.State.ExitCode}}' 3 $ docker run -d --name quick alpine:3.22 307ed7db6b3a4600d4b6ff28c9f7dbecd5ef784907813fc5ba69af367dec9299 $ docker ps -a --filter 'name=^quick$' --format '{{.Names}}: {{.Status}}' quick: Exited (0) 1 second ago
quick exited at once with code 0: Alpine's default command is a shell, and a shell with no terminal and nothing to do just ends. A container needs a process that keeps running, like a server.quick จบทันทีด้วย code 0 เพราะคำสั่งเริ่มต้นของ Alpine คือ shell และ shell ที่ไม่มีเทอร์มินัลและไม่มีอะไรให้ทำก็จะจบไปเลย container ต้องมี process ที่ทำงานต่อเนื่อง เช่น เซิร์ฟเวอร์
A restart policy tells Docker what to do when the process dies. on-failure:3 restarts it after an error, at most 3 times:
restart policy บอก Docker ว่าต้องทำอะไรเมื่อ process ตาย on-failure:3 เริ่มใหม่หลังเกิดข้อผิดพลาด ไม่เกิน 3 ครั้ง
$ docker run -d --name flaky --restart on-failure:3 alpine:3.22 sh -c 'date +%T; exit 1' 02f613bb1bca1f73041c78bc76b4311bdfea4e424be7b797646d3fb23ec7d8cc $ docker logs flaky 18:28:10 18:28:10 18:28:10 18:28:11 $ docker inspect flaky --format '{{.RestartCount}} restarts, status {{.State.Status}}, exit code {{.State.ExitCode}}' 3 restarts, status exited, exit code 1
Four runs (the first and 3 restarts), so four times in the log. Servers usually use --restart unless-stopped, which also starts them again after the machine reboots.รันสี่ครั้ง (ครั้งแรกและเริ่มใหม่ 3 ครั้ง) จึงมีเวลาสี่บรรทัดใน log เซิร์ฟเวอร์มักใช้ --restart unless-stopped ซึ่งเริ่มใหม่ให้ด้วยหลังเครื่องรีบูต
For a running container, look inside. In the Compose app from Lesson 8: สำหรับ container ที่กำลังทำงาน ให้เข้าไปดูข้างใน ในแอป Compose จากบทที่ 8
$ docker compose exec api sh -c 'whoami; pwd; ls -a' node /app . .. .dockerignore .gitignore Dockerfile compose.yaml node_modules package-lock.json package.json server.js $ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' hello-api-api-1 hello-api-db-1 NAME CPU % MEM USAGE / LIMIT hello-api-api-1 0.00% 17.12MiB / 7.75GiB hello-api-db-1 0.06% 25.42MiB / 7.75GiB
The app runs as node, not root, in /app. There's no .git thanks to .dockerignore, and node_modules is the Linux one from npm ci. Files like compose.yaml came in with COPY . .; add them to .dockerignore too if you want a cleaner image.แอปรันด้วยผู้ใช้ node ไม่ใช่ root และอยู่ใน /app ไม่มี .git เพราะ .dockerignore และ node_modules คือชุดสำหรับ Linux จาก npm ci ไฟล์อย่าง compose.yaml ติดมากับ COPY . . ถ้าต้องการ image ที่สะอาดกว่าก็เพิ่มไฟล์เหล่านั้นใน .dockerignore ได้
| Questionคำถาม | Commandคำสั่ง |
|---|---|
| What did it print?มันพิมพ์อะไรออกมา | docker logs --tail 50 NAME |
| Why did it stop?ทำไมมันหยุด | docker inspect NAME --format '{{.State.ExitCode}} {{.State.Error}}' |
| What's inside?ข้างในมีอะไร | docker exec -it NAME sh |
| How is it configured?ตั้งค่าไว้อย่างไร | docker inspect NAME |
| Is it using too much?ใช้ทรัพยากรมากเกินไหม | docker stats |
Lesson 12 · Your turnบทที่ 12 · ถึงตาคุณแล้ว
Practiceแบบฝึกหัด
Three tasks. Each solution was run for real; the output is under it. งานสามข้อ ทุกเฉลยรันจริงแล้ว ผลลัพธ์อยู่ด้านล่าง
1. A Redis container1. container ของ Redis
Run Redis (redis:7-alpine) in the background, then use redis-cli inside it to save a key and read it back.รัน Redis (redis:7-alpine) แบบเบื้องหลัง แล้วใช้ redis-cli ข้างในเพื่อบันทึก key และอ่านกลับมา
Show solutionดูเฉลย
$ docker run -d --name redis redis:7-alpine Unable to find image 'redis:7-alpine' locally 7-alpine: Pulling from library/redis ⋯ 23 more lines ⋯⋯ อีก 23 บรรทัด ⋯ Digest: sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499 Status: Downloaded newer image for redis:7-alpine bd00bfb933f4fc3563d4a94b6d513a1bc03ef16696a70c0fd84761e135c48628 $ docker exec redis redis-cli SET greeting "hello from redis"; docker exec redis redis-cli GET greeting OK hello from redis
2. Bake a website into an image2. อบเว็บไซต์ลงใน image
Instead of a bind mount, write a Dockerfile that copies site/index.html into an nginx image, so the site travels with the image.แทนที่จะใช้ bind mount ให้เขียน Dockerfile ที่คัดลอก site/index.html เข้าไปใน image ของ nginx เว็บไซต์จะได้ติดไปกับ image
Show solutionดูเฉลย
$ docker build -q -t my-site site sha256:943d7f087615c89620987dc68dee8d0f4c365d2423b3714db8fa9f7312757c94 $ docker run -d --name web2 -p 8089:80 my-site 67fadd5526025ef70c1c582d80886b2c846a7373966372b2e51432431b69a0db $ curl -s localhost:8089 <h1>Hello from a folder on my computer</h1>
3. Count the visits in the database3. นับการเข้าชมในฐานข้อมูล
Start the Compose app, call the API three times, then ask Postgres directly how many rows visits has.เริ่มแอป Compose เรียก API สามครั้ง แล้วถาม Postgres โดยตรงว่าตาราง visits มีกี่แถว
Show solutionดูเฉลย
$ docker compose exec db psql -U app -c "SELECT count(*) FROM visits;" count ------- 3 (1 row)
After docker compose up -d --wait and three curl -s localhost:8088 calls on a fresh volume.หลัง docker compose up -d --wait และเรียก curl -s localhost:8088 สามครั้งกับ volume ใหม่
Lesson 13 · Debuggingบทที่ 13 · การดีบัก
Common mistakesข้อผิดพลาดที่พบบ่อย
Each mistake was made on purpose and run; the messages are copied from the real output. ข้อผิดพลาดแต่ละข้อถูกทำขึ้นโดยตั้งใจและรันจริง ข้อความถูกคัดลอกจากผลลัพธ์จริง
The port is already takenพอร์ตถูกใช้อยู่แล้ว
Only one container (or program) can publish a host port. Docker even creates the second container before it fails, so remove it too.พอร์ตของเครื่องหนึ่งพอร์ตเปิดได้เพียง container (หรือโปรแกรม) เดียว Docker สร้าง container ตัวที่สองก่อนจะล้มเหลวด้วย จึงต้องลบตัวนั้นด้วย
The app listens on 127.0.0.1 inside the containerแอป listen บน 127.0.0.1 ภายใน container
Published ports arrive on the container's network interface, not its loopback. Many dev servers bind to localhost by default; tell them to use 0.0.0.0 (for example --host 0.0.0.0).พอร์ตที่เปิดออกมาจะเข้ามาทาง network interface ของ container ไม่ใช่ loopback เซิร์ฟเวอร์สำหรับพัฒนาหลายตัวผูกกับ localhost เป็นค่าเริ่มต้น ให้บอกให้ใช้ 0.0.0.0 (เช่น --host 0.0.0.0)
localhost to reach another containerใช้ localhost เพื่อติดต่อ container อื่น
Inside a container, localhost is the container itself. Use the other container's name or Compose service name.ภายใน container localhost คือ container ตัวนั้นเอง ให้ใช้ชื่อของ container อื่นหรือชื่อ service ใน Compose
A database without a volumeฐานข้อมูลที่ไม่มี volume
The data lives in the container's writable layer and is deleted with it. Recreating the container (to upgrade, or by docker compose down) loses everything.ข้อมูลอยู่ในเลเยอร์ที่เขียนได้ของ container และถูกลบไปพร้อมกัน การสร้าง container ใหม่ (เพื่ออัปเกรด หรือจาก docker compose down) ทำให้ข้อมูลหายหมด
COPY . . before installing packagesCOPY . . ก่อนติดตั้งแพ็กเกจ
Any code change invalidates the COPY . . layer, and every layer after it, including the slow install. I built this order, changed one line of server.js and built again: npm ci ran again instead of being cached.การแก้โค้ดใดๆ ทำให้เลเยอร์ COPY . . ใช้ไม่ได้ และเลเยอร์ทั้งหมดหลังจากนั้นด้วย รวมถึงการติดตั้งที่ช้า ผม build ด้วยลำดับนี้ แก้ server.js หนึ่งบรรทัด แล้ว build อีกครั้ง npm ci ก็รันใหม่แทนที่จะใช้ cache
Ignoring SIGTERMไม่สนใจ SIGTERM
Every docker stop, deploy and restart waits 10 seconds and then kills the app mid-request. Handle SIGTERM: finish the current requests, close connections, exit.ทุกครั้งที่ docker stop, deploy และ restart จะต้องรอ 10 วินาที แล้วฆ่าแอปทั้งที่ยังทำคำขอค้างอยู่ ให้จัดการ SIGTERM คือทำคำขอปัจจุบันให้เสร็จ ปิดการเชื่อมต่อ แล้วจบ
Lesson 14 · Referenceบทที่ 14 · อ้างอิง
Glossaryคำศัพท์
| English | ไทย | Meaningความหมาย |
|---|---|---|
| image | อิมเมจ | A read-only package of files plus a start commandแพ็กเกจไฟล์แบบอ่านอย่างเดียวพร้อมคำสั่งเริ่มทำงาน |
| container | คอนเทนเนอร์ | A running, isolated copy of an imageสำเนาของ image ที่กำลังทำงานแบบแยกตัว |
| layer | เลเยอร์ | One step of an image; shared and cachedหนึ่งขั้นของ image ที่ใช้ร่วมกันและ cache ได้ |
| tag / digest | แท็ก / ไดเจสต์ | A movable name / a fixed content fingerprintชื่อที่ย้ายได้ / ลายนิ้วมือของเนื้อหาที่ตายตัว |
| Dockerfile | Dockerfile | The recipe Docker builds an image fromสูตรที่ Docker ใช้ build image |
| build context | build context | The folder whose files a build can copyโฟลเดอร์ที่การ build คัดลอกไฟล์ได้ |
| build cache | build cache | Layers kept from earlier builds and reusedเลเยอร์ที่เก็บจากการ build ครั้งก่อนและนำมาใช้ซ้ำ |
| volume | วอลุ่ม | Storage managed by Docker that outlives containersพื้นที่เก็บข้อมูลที่ Docker จัดการและอยู่ได้นานกว่า container |
| bind mount | bind mount | A folder from your computer mounted into a containerโฟลเดอร์จากเครื่องของเราที่ mount เข้าไปใน container |
| publish (-p) | เปิดพอร์ต | Forward a host port to a container portส่งต่อพอร์ตของเครื่องไปยังพอร์ตของ container |
| registry | รีจิสทรี | A server that stores and serves imagesเซิร์ฟเวอร์ที่เก็บและแจกจ่าย image |
| multi-stage build | multi-stage build | Build in one stage, ship only the result from anotherbuild ใน stage หนึ่ง แล้วส่งเฉพาะผลลัพธ์จากอีก stage |
| Compose | Compose | A tool that runs a multi-container app from a YAML fileเครื่องมือที่รันแอปหลาย container จากไฟล์ YAML |
| exit code | exit code | How the main process ended: 0 ok, other values errorsprocess หลักจบอย่างไร 0 คือปกติ ค่าอื่นคือข้อผิดพลาด |
| SIGTERM | SIGTERM | The polite "please stop" signal sent by docker stopสัญญาณ "ช่วยหยุดด้วย" แบบสุภาพที่ docker stop ส่งไป |