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

AI & Vibe Coding

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

ให้ 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 vendor image สุดท้ายจึงเล็กลงและมีเครื่องมือให้ผู้บุกรุกใช้น้อยลง
  • ไม่รันเป็น root USER www-data ทำให้ถ้าแอปถูกเจาะ ผู้บุกรุกไม่ได้สิทธิ์ root ในคอนเทนเนอร์ ไฟล์โค้ดยังเป็นของ root ทำให้ www-data แก้โค้ดไม่ได้ เขียนได้เฉพาะโฟลเดอร์ที่จำเป็น เมื่อ PHP-FPM ไม่ได้รันเป็น root จะมี warning ว่า directive user ถูกเพิกเฉย ซึ่งเป็นเรื่องปกติ
  • 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 เขียน

  1. ไม่มี latest ทุก FROM และทุก image ใน compose ระบุ tag หรือ digest
  2. มี USER ที่ไม่ใช่ root ใน stage สุดท้าย และไม่มี chmod 777
  3. ไม่มี secret ใน ENV, ARG, ไฟล์ที่ COPY เข้าไป หรือในประวัติ layer ตรวจด้วย docker history --no-trunc
  4. มี .dockerignore และตรวจแล้วว่า .env กับ .git ไม่อยู่ใน image
  5. ไม่มี dev dependency และเครื่องมือ build ใน image สุดท้าย
  6. HEALTHCHECK ตรวจการตอบสนองจริง ไม่ใช่แค่ exit 0
  7. พอร์ตที่เปิดออก host มีเฉพาะที่จำเป็น
  8. สแกน 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)

ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ