ข้ามไปยังเนื้อหา

Ports และ Environment Variables

วัดบน Docker Desktop 4.81.0 / Engine 29.6.1 (macOS, linux/arm64), 2026-09-30 ที่อยู่ LAN ของ Mac ในการทดสอบคือ 192.168.1.86

โดยค่าเริ่มต้น container ถูกแยกตัวสมบูรณ์ — port ของมันเข้าถึงจาก host ไม่ได้ flag -p publish port โดยสร้าง mapping ระหว่าง port ของ host กับ port ของ container:

Terminal window
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 web
80/tcp -> 0.0.0.0:18010
80/tcp -> [::]:18010

publish หลาย port ได้:

Terminal window
docker run -d -p 8080:80 -p 8443:443 nginx:1.31-alpine

-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:55002
200

port ชั่วคราวที่นี่คือ 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 โดยไม่มีลูกศร)

Terminal window
# Only accessible from localhost, not the network
docker 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, loopback
192.168.1.86:18010 -> 200 # all interfaces, LAN address: any machine on the LAN could do this
127.0.0.1:18011 -> 200 # loopback-only, loopback
192.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)

container อ่านค่า configuration จาก environment variable ส่งด้วย -e:

Terminal window
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-alpine
Error: 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-file:

.env
POSTGRES_PASSWORD=<your-db-password>
POSTGRES_DB=myapp
POSTGRES_USER=app
Terminal window
docker 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 value
PLAIN=abc
SPACED=a b # spaces are kept without quoting
EMPTY=

-e GREETING="hello world" บนบรรทัดคำสั่งได้ hello world เพราะ shell ของคุณตัดเครื่องหมายคำพูดออก ดังนั้นเจตนาเดียวกันทำงานต่างกันระหว่างสองกลไก: ในไฟล์ env ให้เขียน GREETING=hello world ไม่ใช่ GREETING="hello world"

$ 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 ตอนรัน)

การตั้งชื่อที่คาดเดาได้ทำให้ทุกคำสั่งถัดไปง่ายขึ้น:

Terminal window
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 ไว้)

Terminal window
docker run -d \
--name webserver \
-p 8080:80 \
-e NGINX_HOST=localhost \
nginx:1.31-alpine
# Verify
docker ps
curl 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 webserver

สคริปต์รันของ Node service ตัวเล็กที่ “ฟังที่ 3000” ซึ่ง AI เขียน:

🐛 หา bug ในโค้ดนี้

app/server.js
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 api
listening 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 works
hello

แอปไม่ผิดและ port mapping ก็ไม่ผิด; bind address คือ bug ภายใน container ให้ bind ที่ 0.0.0.0 แล้วให้ -p ตัดสินว่าใครเข้าจากภายนอกได้:

scripts/run-api.sh
#!/bin/sh
set -eu
# app/server.js reads HOST; 0.0.0.0 = all container interfaces
docker 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 ไม่ใช่ที่แอป

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 และโปรแกรมส่วนใหญ่อ่านเพียงครั้งเดียว

ตัวเลือกข้อดีข้อเสีย
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
  • ฮาร์ดโค้ด 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 แล้วได้เครื่องหมายคำพูดจริงในค่า
  • “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)
-p 8080:80 ทำอะไร?
คุณ start container ด้วย -p 127.0.0.1:8080:80 ใครเข้าถึงได้?
-e NAME=value ต่างจาก --env-file .env อย่างไร?
environment variable ที่ส่งด้วย -e ซ่อนจากผู้ใช้คนอื่นของ Docker host หรือไม่?
service ฟังที่ 127.0.0.1:3000 ภายใน container และคุณรันด้วย -p 3000:3000 curl บน host เห็นอะไร?
-P (ตัวใหญ่) ทำอะไร?