ให้ AI เขียน Dockerfile และ Compose แบบปลอดภัย

ขอให้ AI เขียน Dockerfile สำหรับโปรเจกต์ PHP แทบทุกครั้งจะได้ไฟล์ที่ build ผ่านและรันได้ แต่ถ้าอ่านดี ๆ มักพบ FROM php:latest, คอนเทนเนอร์ที่รันเป็น root, COPY . . ที่พาไฟล์ .env และโฟลเดอร์ .git เข้าไปใน image หรือรหัสผ่านฐานข้อมูลที่เขียนไว้ใน ENV ตรง ๆ ของเหล่านี้ไม่ทำให้ระบบพังวันนี้ แต่เป็นช่องโหว่ที่รอวันถูกใช้ บทความนี้จะสรุปหลักความปลอดภัยที่ต้องบอก AI ตั้งแต่ใน prompt พร้อมตัวอย่าง Dockerfile สำหรับแอป Yii2 บน PHP-FPM, .dockerignore, Docker Compose ที่ใช้ secrets และเช็กลิสต์สำหรับ review
หลักความปลอดภัยที่ต้องใส่ใน prompt
AI จะเขียนตามรูปแบบที่พบบ่อยที่สุด ซึ่งมักเป็นตัวอย่างสั้น ๆ สำหรับเริ่มต้น ไม่ใช่สำหรับ production ถ้าเราต้องการมาตรฐานที่สูงกว่านั้นต้องบอกตรง ๆ
เขียน Dockerfile สำหรับแอป Yii2 basic template บน PHP-FPM และ PostgreSQL
ข้อกำหนด:
- ใช้ base image ทางการ ระบุ tag ที่ชัดเจน ห้ามใช้ latest
- multi-stage: stage ติดตั้ง composer แยกจาก runtime, ไม่มี composer และ dev dependency ใน image สุดท้าย
- runtime รันด้วย user ที่ไม่ใช่ root, ไฟล์โค้ดเป็นของ root และอ่านอย่างเดียว
เขียนได้เฉพาะ runtime/ และ web/assets/
- ห้ามมี secret ใน image (ENV, ARG, ไฟล์) ถ้าต้องใช้ token ตอน build ให้ใช้ BuildKit secret mount
- มี HEALTHCHECK ที่ตรวจว่า PHP-FPM ตอบสนองจริง
- เขียน .dockerignore ด้วย
อธิบายแต่ละบรรทัดที่เกี่ยวกับความปลอดภัยสั้น ๆ
คำขอให้ "อธิบายแต่ละบรรทัด" ช่วยให้เราจับได้ง่ายขึ้นว่าตรงไหนที่ AI ทำตามข้อกำหนดแค่ในคำอธิบาย แต่โค้ดจริงไม่ได้ทำ
ตัวอย่าง Dockerfile สำหรับ Yii2 บน PHP-FPM
นี่คือผลหลังจาก review และปรับแล้ว ใช้ Yii2 basic template (ถ้าเป็น advanced template ให้ปรับ path ของ runtime และ assets ตามแอปแต่ละตัว)
# syntax=docker/dockerfile:1
# ---------- base: PHP + extensions ที่ต้องใช้ ----------
FROM php:8.5-fpm-alpine AS base
RUN apk add --no-cache libpq fcgi \
&& apk add --no-cache --virtual .build-deps postgresql-dev \
&& docker-php-ext-install pdo_pgsql opcache \
&& apk del .build-deps \
&& cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# ---------- vendor: ติดตั้ง dependency ----------
FROM base AS vendor
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=secret,id=composer_auth \
COMPOSER_AUTH="$(cat /run/secrets/composer_auth 2>/dev/null)" \
composer install --no-dev --prefer-dist --no-interaction \
--no-progress --no-scripts --no-autoloader
COPY . .
RUN composer dump-autoload --no-dev --optimize
# ---------- runtime: image ที่ขึ้น production ----------
FROM base AS runtime
COPY docker/php-fpm/zz-app.conf /usr/local/etc/php-fpm.d/zz-app.conf
WORKDIR /var/www/app
COPY --from=vendor /app /var/www/app
RUN mkdir -p runtime web/assets \
&& chown -R www-data:www-data runtime web/assets
USER www-data
EXPOSE 9000
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000 | grep -q pong || exit 1
ไฟล์ docker/php-fpm/zz-app.conf เปิด endpoint สำหรับ health check ของ PHP-FPM
[www]
ping.path = /ping
ping.response = pong
อธิบายจุดสำคัญ:
- Pin version tag
8.3-fpm-alpineชัดกว่าlatestมาก แต่ยังขยับตาม patch release ถ้าต้องการให้ build ซ้ำได้ผลเดิมทุกครั้ง ให้ pin ด้วย digest เช่นphp:8.5-fpm-alpine@sha256:...แล้วอัปเดตอย่างตั้งใจเป็นรอบ ๆ เลือกเวอร์ชัน PHP ที่ยังได้รับ security support โดยตรวจที่ php.net หลักเดียวกันใช้กับcomposer:2 - Multi-stage composer และไฟล์ชั่วคราวจากการติดตั้งอยู่แค่ใน stage
vendorimage สุดท้ายจึงเล็กลงและมีเครื่องมือให้ผู้บุกรุกใช้น้อยลง - ไม่รันเป็น root
USER www-dataทำให้ถ้าแอปถูกเจาะ ผู้บุกรุกไม่ได้สิทธิ์ root ในคอนเทนเนอร์ ไฟล์โค้ดยังเป็นของ root ทำให้ www-data แก้โค้ดไม่ได้ เขียนได้เฉพาะโฟลเดอร์ที่จำเป็น เมื่อ PHP-FPM ไม่ได้รันเป็น root จะมี warning ว่า directiveuserถูกเพิกเฉย ซึ่งเป็นเรื่องปกติ - Secret ตอน build ถ้าต้องดึงแพ็กเกจจาก private repository ให้ส่ง token ผ่าน
--mount=type=secretซึ่งไม่ถูกบันทึกลงใน layer ต่างจากARGหรือENVที่ดูย้อนได้จากdocker history - HEALTHCHECK ใช้
cgi-fcgi(จากแพ็กเกจ fcgi) คุยกับ PHP-FPM ตรง ๆ แล้วตรวจคำตอบ pong ซึ่งบอกได้จริงว่า FPM รับ request ได้ ไม่ใช่แค่ว่า process ยังอยู่
สั่ง build พร้อมส่ง secret ได้แบบนี้ ไฟล์ auth อยู่บนเครื่องที่ build เท่านั้น
docker build \
--secret id=composer_auth,src="$HOME/.config/composer/auth.json" \
-t myapp:1.4.0 .
.dockerignore: กันของที่ไม่ควรเข้า image
COPY . . จะ copy ทุกอย่างใน build context ยกเว้นที่ระบุใน .dockerignore ไฟล์นี้จึงเป็นด่านสำคัญที่ AI มักลืมเขียน
.git
.gitignore
.env
.env.*
secrets/
vendor/
node_modules/
runtime/
web/assets/
tests/
docker-compose*.yml
Dockerfile
*.log
.idea/
.vscode/
หลังเขียนแล้วให้ตรวจจริง เช่น build แล้วรัน docker run --rm myapp:1.4.0 ls -la ดูว่าไม่มี .env หรือ .git หลุดเข้าไป อย่าเชื่อแค่ว่าเขียนไฟล์ไว้แล้ว
Docker Compose ที่ไม่ฝัง secret
ใน Compose ปัญหาที่พบบ่อยคือรหัสผ่านอยู่ใน environment ของไฟล์ yml ที่ถูก commit และพอร์ตฐานข้อมูลเปิดออกมาที่เครื่อง host ตัวอย่างที่ปรับแล้ว:
services:
app:
image: myapp:1.4.0
environment:
DB_DSN: "pgsql:host=db;dbname=app"
DB_USER: app
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
depends_on:
db:
condition: service_healthy
restart: unless-stopped
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
db:
image: postgres:18-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
pgdata:
ฝั่ง Yii2 อ่านรหัสผ่านจากไฟล์ใน config/db.php
<?php
$passwordFile = getenv('DB_PASSWORD_FILE') ?: '/run/secrets/db_password';
return [
'class' => 'yii\db\Connection',
'dsn' => getenv('DB_DSN'),
'username' => getenv('DB_USER'),
'password' => is_readable($passwordFile) ? trim((string) file_get_contents($passwordFile)) : '',
'charset' => 'utf8',
];
ข้อสังเกต:
- บริการ
dbไม่มีportsจึงเข้าถึงได้เฉพาะจากคอนเทนเนอร์ใน network เดียวกัน ถ้าต้องต่อจากเครื่อง dev ให้ผูกกับ127.0.0.1เท่านั้น - โฟลเดอร์
secrets/ต้องอยู่ใน.gitignoreและ.dockerignoreและสิทธิ์ไฟล์ต้องให้ user ในคอนเทนเนอร์อ่านได้ บน production ควรใช้ระบบจัดการ secret ของแพลตฟอร์มที่ใช้อยู่ - image ของ postgres ทางการรองรับตัวแปรแบบ
_FILEทำให้รหัสผ่านไม่ต้องอยู่ใน environment - nginx ต้องเข้าถึงไฟล์ static ใน
web/ด้วย งานจริงมักสร้าง image ของ nginx ที่ copy ไฟล์เหล่านี้เข้าไป ตัวอย่างนี้ละไว้เพื่อให้สั้น
เช็กลิสต์ review Dockerfile ที่ AI เขียน
- ไม่มี
latestทุกFROMและทุก image ใน compose ระบุ tag หรือ digest - มี
USERที่ไม่ใช่ root ใน stage สุดท้าย และไม่มีchmod 777 - ไม่มี secret ใน
ENV,ARG, ไฟล์ที่ COPY เข้าไป หรือในประวัติ layer ตรวจด้วยdocker history --no-trunc - มี
.dockerignoreและตรวจแล้วว่า.envกับ.gitไม่อยู่ใน image - ไม่มี dev dependency และเครื่องมือ build ใน image สุดท้าย
- HEALTHCHECK ตรวจการตอบสนองจริง ไม่ใช่แค่
exit 0 - พอร์ตที่เปิดออก host มีเฉพาะที่จำเป็น
- สแกน image ด้วยเครื่องมือตรวจช่องโหว่ที่ทีมใช้ และ rebuild เป็นรอบเพื่อรับ security patch ของ base image
รายละเอียดเพิ่มเติมของแต่ละคำสั่งดูได้ที่ docs.docker.com
สรุป
- บอกข้อกำหนดด้านความปลอดภัยใน prompt ตั้งแต่ต้น เพราะค่าเริ่มต้นของ AI มักเป็นตัวอย่างสำหรับเริ่มต้น
- pin version ของ base image, ใช้ multi-stage และรันด้วย user ที่ไม่ใช่ root
- secret ไม่อยู่ใน image: ใช้ BuildKit secret ตอน build และ Compose secrets หรือระบบจัดการ secret ตอนรัน
- เขียน .dockerignore และ HEALTHCHECK แล้วตรวจผลจริงจาก image ที่ build ได้
ลองทำ
นำ Dockerfile ของโปรเจกต์ที่คุณมีอยู่ (หรือให้ AI เขียนใหม่โดยไม่ใส่ข้อกำหนดใด ๆ) มาตรวจตามเช็กลิสต์ 8 ข้อ จดว่าผ่านกี่ข้อ จากนั้นเขียน prompt ตามตัวอย่างในบทความ ให้ AI ปรับไฟล์ แล้ว build จริง รัน docker history --no-trunc และ docker inspect ดูสถานะ health ของคอนเทนเนอร์ เทียบผลก่อนและหลัง
ความคิดเห็น (0)
ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ