Ports และ Environment Variables
วัดบน Docker Desktop 4.81.0 / Engine 29.6.1 (macOS, linux/arm64), 2026-09-30 ที่อยู่ LAN ของ Mac ในการทดสอบคือ 192.168.1.86
Publish port ด้วย -p
หัวข้อที่มีชื่อว่า “Publish port ด้วย -p”โดยค่าเริ่มต้น container ถูกแยกตัวสมบูรณ์ — port ของมันเข้าถึงจาก host ไม่ได้ flag -p publish port โดยสร้าง mapping ระหว่าง port ของ host กับ port ของ container:
docker run -d -p 8080:80 nginx:1.31-alpineรูปแบบคือ -p <host-port>:<container-port> หลังรันคำสั่งนี้ เปิด http://localhost:8080 ในเบราว์เซอร์จะเข้าถึง port 80 ภายใน container docker port แสดง mapping; บน Engine 29 -p ธรรมดาถูก publish บน ทั้ง IPv4 และ IPv6 จึงมีสองบรรทัด:
$ docker port web80/tcp -> 0.0.0.0:1801080/tcp -> [::]:18010publish หลาย port ได้:
docker run -d -p 8080:80 -p 8443:443 nginx:1.31-alpineให้ Docker เลือก host port ด้วย -P
หัวข้อที่มีชื่อว่า “ให้ Docker เลือก host port ด้วย -P”-P (ตัวใหญ่) publish ทุก port ที่ image EXPOSE ไว้ไปยัง host port สูงแบบสุ่ม -p 80 (ไม่มีส่วน host) ทำแบบเดียวกันกับ port เดียว:
$ docker run -d -P nginx:1.31-alpine # image EXPOSEs 80$ docker port <container>80/tcp -> 0.0.0.0:55002$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:55002200port ชั่วคราวที่นี่คือ 55002 อยู่ในช่วง ephemeral ของ VM (cat /proc/sys/net/ipv4/ip_local_port_range พิมพ์ 32768 60999) ใช้กับการทดสอบที่ตัวเลขไม่สำคัญ แล้วค้นหาด้วย docker port EXPOSE ใน Dockerfile แค่ บันทึก port; หากไม่มี -p/-P จะไม่มีอะไรถูก publish (docker ps แสดง 80/tcp โดยไม่มีลูกศร)
Bind กับ host IP ที่ระบุ
หัวข้อที่มีชื่อว่า “Bind กับ host IP ที่ระบุ”# Only accessible from localhost, not the networkdocker run -d -p 127.0.0.1:8080:80 nginx:1.31-alpineพิสูจน์ด้วย container สองตัว ตัวหนึ่ง publish เป็น -p 18010:80 และอีกตัวเป็น -p 127.0.0.1:18011:80 ร้องขอจากตัว Mac เองและจากที่อยู่ LAN 192.168.1.86:
127.0.0.1:18010 -> 200 # all interfaces, loopback192.168.1.86:18010 -> 200 # all interfaces, LAN address: any machine on the LAN could do this127.0.0.1:18011 -> 200 # loopback-only, loopback192.168.1.86:18011 -> curl rc=7 # loopback-only, LAN address: connection refused-p 8080:80 เข้าถึงได้จากทุกเครื่องที่ route มาถึง host ของคุณ Docker Desktop สำหรับ Mac proxy port เอง ผลทดสอบนี้จึงตรงกับที่ Linux host ทำ; บน Linux เอกสารยังระบุเพิ่มเติมว่า port ที่ publish ข้ามกฎ ufw/firewalld (Docker with iptables)
ส่ง environment variable ด้วย -e
หัวข้อที่มีชื่อว่า “ส่ง environment variable ด้วย -e”container อ่านค่า configuration จาก environment variable ส่งด้วย -e:
docker run -d --name db \ -e POSTGRES_PASSWORD='<your-db-password>' \ -e POSTGRES_DB=myapp \ postgres:18-alpineแต่ละ -e ตั้งตัวแปรหนึ่งตัวภายใน container image postgres ต้องการตัวหนึ่ง: ถ้าไม่มี container จะ exit ทันที:
$ docker run --rm postgres:18-alpineError: Database is uninitialized and superuser password is not specified. You must specify POSTGRES_PASSWORD to a non-empty value for the superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run".เมื่อมีตัวแปร psql -U postgres -d myapp -tAc "select current_database()" พิมพ์ myapp placeholder ที่นี่เป็นค่าปลอมในวงเล็บมุม: ค่าจริงให้เก็บใน secret store ไม่ใช่ในบทเรียนหรือ repository
-e NAME โดยไม่มีค่าจะคัดลอก NAME จาก shell ของคุณ (FROMHOST=from-mac docker run -e FROMHOST … พิมพ์ from-mac); ถ้า shell ไม่มีตัวแปรนั้นจะไม่ตั้งอะไร (NOPE=[unset])
ใช้ไฟล์ env
หัวข้อที่มีชื่อว่า “ใช้ไฟล์ env”เมื่อมีตัวแปรมากกว่าไม่กี่ตัว ให้เก็บในไฟล์แล้วใช้ --env-file:
POSTGRES_PASSWORD=<your-db-password>POSTGRES_DB=myappPOSTGRES_USER=appdocker run -d --name db --env-file .env postgres:18-alpineไฟล์ถูกอ่านโดย Docker CLI: หนึ่ง NAME=value ต่อบรรทัด บรรทัด # ถูกข้าม เครื่องหมายคำพูดไม่ถูกตัดออก ต่างจาก shell หรือ .env ของ Compose:
# t.env: GREETING="hello world" PLAIN=abc SPACED=a b EMPTY=$ docker run --rm --env-file t.env alpine:3.24 sh -c 'env | grep -E "GREETING|PLAIN|SPACED|EMPTY"'GREETING="hello world" # the double quotes are part of the valuePLAIN=abcSPACED=a b # spaces are kept without quotingEMPTY=-e GREETING="hello world" บนบรรทัดคำสั่งได้ hello world เพราะ shell ของคุณตัดเครื่องหมายคำพูดออก ดังนั้นเจตนาเดียวกันทำงานต่างกันระหว่างสองกลไก: ในไฟล์ env ให้เขียน GREETING=hello world ไม่ใช่ GREETING="hello world"
environment variable ไม่ใช่ความลับ
หัวข้อที่มีชื่อว่า “environment variable ไม่ใช่ความลับ”$ docker run -d --name x -e DB_PASSWORD='<your-db-password>' alpine:3.24 sleep 60$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' x | grep DB_DB_PASSWORD=<your-db-password>$ docker exec x sh -c 'cat /proc/1/environ | tr "\0" "\n" | grep DB_'DB_PASSWORD=<your-db-password>ใครก็ตามที่รัน docker inspect ได้ (หรืออ่าน /proc/<pid>/environ) จะเห็นค่า และมันยังติดอยู่ใน shell history เมื่อพิมพ์ด้วย -e --env-file ทำให้ค่าไม่อยู่ใน history และรายการ process แต่ไม่พ้น docker inspect สำหรับความลับจริงให้ใช้กลไก secret (secrets: ของ Compose, secret ของ Swarm/Kubernetes, ดึงจาก vault ตอนรัน)
ตั้งชื่อ container ด้วย --name
หัวข้อที่มีชื่อว่า “ตั้งชื่อ container ด้วย --name”การตั้งชื่อที่คาดเดาได้ทำให้ทุกคำสั่งถัดไปง่ายขึ้น:
docker run -d \ --name mysite \ -p 8080:80 \ nginx:1.31-alpineตอนนี้คุณใช้ mysite แทน ID สุ่มในทุกคำสั่ง docker stop, docker logs และ docker exec ได้ ชื่อต้องไม่ซ้ำและถูกจองไว้ตราบที่ container ยังอยู่ แม้หยุดแล้ว (first-container แสดง error name conflict ไว้)
รวมทุกอย่างเข้าด้วยกัน
หัวข้อที่มีชื่อว่า “รวมทุกอย่างเข้าด้วยกัน”docker run -d \ --name webserver \ -p 8080:80 \ -e NGINX_HOST=localhost \ nginx:1.31-alpine
# Verifydocker pscurl http://localhost:8080# Run nginx with a port mapping and an env var
docker run -d \
--name webserver \
-p 8080:80 \
-e NGINX_HOST=localhost \
nginx:1.31-alpine
# Confirm the mapping
docker ps
docker port webserver
# Fetch the default page (should return HTML)
curl -s http://localhost:8080 | head -5
# Inspect the env var inside the container
docker exec webserver env | grep NGINX
# Clean up
docker stop webserver && docker rm webserverReading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”สคริปต์รันของ Node service ตัวเล็กที่ “ฟังที่ 3000” ซึ่ง AI เขียน:
🐛 หา bug ในโค้ดนี้
const http = require('http');// AI: "bind to localhost for safety"const host = process.env.HOST || '127.0.0.1';http.createServer((req, res) => res.end('hello\n')).listen(3000, host, () => console.log(`listening on ${host}:3000`));เฉลย
เมื่อรันด้วย -p 3000:3000 ตามปกติ (ที่นี่ -p 127.0.0.1:18020:3000) port ที่ publish ตอบกลับเป็น empty reply แอปฟังเฉพาะ loopback ของ container เอง ส่วน Docker ส่ง traffic ที่ publish ไปยัง network interface ของ container ไม่ใช่ loopback ของมัน:
$ docker logs apilistening on 127.0.0.1:3000$ curl -sS http://127.0.0.1:18020/curl: (52) Empty reply from server$ docker exec api wget -qO- http://127.0.0.1:3000/ # from inside the container it workshelloแอปไม่ผิดและ port mapping ก็ไม่ผิด; bind address คือ bug ภายใน container ให้ bind ที่ 0.0.0.0 แล้วให้ -p ตัดสินว่าใครเข้าจากภายนอกได้:
#!/bin/shset -eu# app/server.js reads HOST; 0.0.0.0 = all container interfacesdocker run -d --name api -e HOST=0.0.0.0 \ -p 127.0.0.1:3000:3000 \ -v "$PWD/app:/app:ro" \ node:24-alpine node /app/server.jsผล: listening on 0.0.0.0:3000 และ curl ได้ hello “bind ที่ localhost เพื่อความปลอดภัย” เป็นคำแนะนำที่ดีบน host ที่ใช้ร่วมกัน แต่ภายใน container ความปลอดภัยอยู่ที่คำนำหน้า 127.0.0.1: ของ -p ไม่ใช่ที่แอป
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”port ที่ publish คือ กฎ forward จากฝั่ง host ไปยัง network interface ของ container สามอย่างที่เป็นอิสระต่อกันต้องตรงกัน: แอปฟังที่ address ที่ไม่ใช่ loopback ภายใน container, port ของ container ตรงกับเลขทางขวาของ -p และฝั่ง host (127.0.0.1: หรือทุก interface) ตรงกับคนที่ควรเข้าได้ environment variable คือ input ของ container: ถูกคัดลอกเข้า process ตอน start, มองเห็นได้สำหรับทุกคนที่มีสิทธิ์ Docker และโปรแกรมส่วนใหญ่อ่านเพียงครั้งเดียว
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
Bind เฉพาะ 127.0.0.1 (-p 127.0.0.1:8080:80) | ปลอดภัยกว่า — เข้าถึงได้จาก host เดียวกันเท่านั้น (request จาก LAN ได้ connection refused) | เครื่องอื่นในเครือข่ายเข้าไม่ได้ ต้องใช้ reverse proxy เพื่อแชร์ |
Bind ทุก interface (-p 8080:80) | เข้าถึงจากเครือข่ายได้ทันที ง่าย | เปิดกว้าง — เสี่ยงถูกเข้าถึงโดยไม่ตั้งใจถ้าไม่มี firewall ขวาง |
-P / ephemeral | ไม่มี port ชนกัน | เลขเปลี่ยนทุกครั้ง ต้องค้นหา |
-e บนบรรทัดคำสั่ง | ชัดเจนและเห็นในสคริปต์ | อยู่ใน shell history และผลของ ps |
--env-file | ค่าไม่อยู่ใน history ใส่หลายตัวแปรพร้อมกันได้ | เครื่องหมายคำพูดคงอยู่ตามตัวอักษร และยังเห็นใน docker inspect |
Common mistakes
หัวข้อที่มีชื่อว่า “Common mistakes”- ฮาร์ดโค้ด config เช่นรหัสผ่านหรือ hostname ของ DB ลงใน Dockerfile ด้วย
ENVแทนที่จะส่งผ่าน-e/--env-fileตอนรัน ทำให้ทุกการเปลี่ยนต้อง build image ใหม่ (และค่าถูกฝังในประวัติ image) - commit ไฟล์
.envที่มีความลับจริงเข้า git แทนการใช้.env.exampleเป็นแม่แบบ - ลืม publish port ด้วย
-pแล้วสงสัยว่าทำไมcurlจาก host ได้ connection refused ทั้งที่ container ทำงานปกติ - publish ฐานข้อมูลด้วย
-p 5432:5432(ทุก interface) ทั้งที่มีแค่แอปบน host เดียวกันที่ต้องใช้ - เขียน
NAME="value with spaces"ใน--env-fileแล้วได้เครื่องหมายคำพูดจริงในค่า
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
EXPOSEเปิด port” มันแค่บันทึก หากไม่มี-p/-Pก็ไม่มีอะไรถูก publish - “
docker portพิมพ์หนึ่งบรรทัด” บน Engine 29 พิมพ์หนึ่งบรรทัดต่อ address family ดังนั้น-pธรรมดาได้ทั้ง0.0.0.0และ[::] - “
--env-fileเป็นวิธีส่งความลับที่ปลอดภัย” มันซ่อนค่าจาก history และpsไม่ใช่จากdocker inspect - “ถ้า container ขึ้นและ map port แล้ว แอปก็เข้าถึงได้” เฉพาะเมื่อแอปฟังที่
0.0.0.0ภายใน container (พิสูจน์ empty reply สำหรับ127.0.0.1)