Docker Beginner Guide

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 และเริ่มทำงานได้ด้วยคำสั่งเดียว

Every command on this page was run for real with Docker Desktop on a Mac, and the output under it is what it printed. The project was saved after each step, so each file shown is the file that produced the output. Some basic terminal use helps; the networking ideas come from the Networking Beginner Guide. ทุกคำสั่งในหน้านี้รันจริงด้วย Docker Desktop บน Mac และผลลัพธ์ด้านล่างคือสิ่งที่มันพิมพ์ออกมา โปรเจกต์ถูกบันทึกหลังแต่ละขั้น ไฟล์ที่เห็นจึงเป็นไฟล์ที่ให้ผลลัพธ์นั้น ถ้าใช้เทอร์มินัลเป็นบ้างจะช่วยได้ ส่วนแนวคิดเรื่องเครือข่ายมาจาก Networking Beginner Guide

  • Run withรันด้วย Docker 29.6
  • Compose 5.2
  • Node.js 22
  • Postgres 17

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 ขนาดเล็กไว้ให้

ImageContainer
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 นั้น
Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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 ที่กำลังทำงาน

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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, --rmbackground, a name, delete when it exitsทำงานเบื้องหลัง ตั้งชื่อ ลบเมื่อจบ
-p HOST:CONTAINERpublish a portเปิดพอร์ตออกมา
-e NAME=valueset an environment variableกำหนดตัวแปรสภาพแวดล้อม
-itinteractive with a terminal: docker run -it alpine shโต้ตอบได้พร้อมเทอร์มินัล เช่น docker run -it alpine sh
docker ps / ps -arunning / all containerscontainer ที่ทำงาน / ทั้งหมด
docker logs -f NAMEoutput, -f keeps following itผลลัพธ์ และ -f ติดตามต่อเรื่อยๆ
docker exec -it NAME sha shell inside a running containershell ภายใน container ที่กำลังทำงาน
docker stop / start / rmstop, 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 ซึ่งเปลี่ยนไปตามเวลา ในโปรเจกต์จริงจึงควรระบุเวอร์ชัน

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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-alpinestart from the official Node.js 22 image on Alpineเริ่มจาก image ทางการของ Node.js 22 บน Alpine
WORKDIR /appmake /app and work in it (like cd)สร้าง /app แล้วทำงานในนั้น (เหมือน cd)
COPY src destcopy files from your folder into the imageคัดลอกไฟล์จากโฟลเดอร์ของเราเข้าไปใน image
RUN cmdrun a command while building (install packages)รันคำสั่งระหว่าง build (เช่น ติดตั้งแพ็กเกจ)
EXPOSE 3000documents the port; it does not publish it (-p does)บอกให้รู้ว่าใช้พอร์ตนี้ แต่ไม่ได้เปิดพอร์ต (ต้องใช้ -p)
CMD ["node", "server.js"]the command a container runs when it startsคำสั่งที่ container รันเมื่อเริ่มทำงาน
Terminal, in ~/hello-apiเทอร์มินัล ใน ~/hello-apiDocker 29.6 · Compose 5.2
$ 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 ครั้งแรกของคุณ

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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)

Terminal, in ~/hello-apiเทอร์มินัล ใน ~/hello-apiDocker 29.6 · Compose 5.2
$ 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

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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 เก็บไฟล์ของมัน

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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 แทน สิ่งที่เราแก้ฝั่งเราจะปรากฏทันที ซึ่งสะดวกตอนพัฒนา

Terminal, in ~/docker-guideเทอร์มินัล ใน ~/docker-guideDocker 29.6 · Compose 5.2
$ 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

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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 จัดการให้

Terminal, in ~/hello-apiเทอร์มินัล ใน ~/hello-apiDocker 29.6 · Compose 5.2
$ 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 แล้ว

Terminal, in ~/hello-apiเทอร์มินัล ใน ~/hello-apiDocker 29.6 · Compose 5.2
$ 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 -dbuild if needed, create and start everythingbuild ถ้าจำเป็น แล้วสร้างและเริ่มทุกอย่าง
docker compose up -d --buildrebuild images first (after you change code)build image ใหม่ก่อน (หลังแก้โค้ด)
docker compose ps / logs -fstatus / follow the outputสถานะ / ติดตามผลลัพธ์
docker compose exec SERVICE cmdrun a command in a serviceรันคำสั่งใน service
docker compose down / down -vremove containers and network / and volumesลบ container และเครือข่าย / รวมถึง volume
Secretsข้อมูลลับ

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 ที่ว่างเปล่าโดยสิ้นเชิง

Terminal, in ~/hello-goเทอร์มินัล ใน ~/hello-goDocker 29.6 · Compose 5.2
$ 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 และไม่มีตัวจัดการแพ็กเกจ ซึ่งหมายความว่าผู้โจมตีมีของให้ใช้น้อยลงด้วย

Other languagesภาษาอื่น

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

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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 Hubdocker login, docker push me/hello-api:2
GitHubdocker login ghcr.io, docker push ghcr.io/me/hello-api:2
In CI/CDใน CI/CDthe 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 รอจนหมดความอดทน)

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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 ครั้ง

Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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

Terminal, in ~/hello-apiเทอร์มินัล ใน ~/hello-apiDocker 29.6 · Compose 5.2
$ 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ดูเฉลย
Terminalเทอร์มินัลDocker 29.6 · Compose 5.2
$ 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ดูเฉลย
Terminal, in ~/docker-guideเทอร์มินัล ใน ~/docker-guideDocker 29.6 · Compose 5.2
$ 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ดูเฉลย
Terminal, in ~/hello-apiเทอร์มินัล ใน ~/hello-apiDocker 29.6 · Compose 5.2
$ 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 ตัวที่สองก่อนจะล้มเหลวด้วย จึงต้องลบตัวนั้นด้วย

✗ Wrong✗ ผิด
✓ Right✓ ถูก

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)

✗ Wrong✗ ผิด
✓ Right✓ ถูก

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

✗ Wrong✗ ผิด
✓ Right✓ ถูก

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) ทำให้ข้อมูลหายหมด

✗ Wrong✗ ผิด
✓ Right✓ ถูก

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

✗ Wrong✗ ผิด
✓ Right✓ ถูก

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 คือทำคำขอปัจจุบันให้เสร็จ ปิดการเชื่อมต่อ แล้วจบ

✗ Wrong✗ ผิด
✓ Right✓ ถูก

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ชื่อที่ย้ายได้ / ลายนิ้วมือของเนื้อหาที่ตายตัว
DockerfileDockerfileThe recipe Docker builds an image fromสูตรที่ Docker ใช้ build image
build contextbuild contextThe folder whose files a build can copyโฟลเดอร์ที่การ build คัดลอกไฟล์ได้
build cachebuild cacheLayers kept from earlier builds and reusedเลเยอร์ที่เก็บจากการ build ครั้งก่อนและนำมาใช้ซ้ำ
volumeวอลุ่มStorage managed by Docker that outlives containersพื้นที่เก็บข้อมูลที่ Docker จัดการและอยู่ได้นานกว่า container
bind mountbind mountA 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 buildmulti-stage buildBuild in one stage, ship only the result from anotherbuild ใน stage หนึ่ง แล้วส่งเฉพาะผลลัพธ์จากอีก stage
ComposeComposeA tool that runs a multi-container app from a YAML fileเครื่องมือที่รันแอปหลาย container จากไฟล์ YAML
exit codeexit codeHow the main process ended: 0 ok, other values errorsprocess หลักจบอย่างไร 0 คือปกติ ค่าอื่นคือข้อผิดพลาด
SIGTERMSIGTERMThe polite "please stop" signal sent by docker stopสัญญาณ "ช่วยหยุดด้วย" แบบสุภาพที่ docker stop ส่งไป