Coding Gun

วิธีการเขียน Dockerfile เบื้องต้น

Dockerfile คือ ไฟล์ที่บอกขั้นตอนการสร้าง Container Image ซึ่งเราจะต้องระบุ

  1. Base Image ที่จะนำมาใช้เป็น Image เริ่มต้น
  2. คำสั่งต่างๆที่คุณค้องการ RUN ใน Base Image เพื่อให้ได้ Service ต่างๆที่เราต้องการ
  3. นำ Code หรือ Content ของเราเข้าไปใน Base Image นั้น
  4. Start service ที่เราได้ install ลงไป และเปิด port เพื่ิอให้สามารถเข้าถึงได้

หลักการทำงานของ Dockerfile

การทำงานของ Dockerfile นั้นจะทำการ Run command ทีละบรรทัด หลังจากที่เราสั่ง

$ docker build -t hello .

docker จะวิ่งไปหาไฟล์ที่ชื่อว่า Dockerfile ถ้าเราใช้ชื่ออื่นเราต้องระบุไฟล์ที่ต้องการ build แบบนี้

$ docker build -f mysql.dockerfile -t hello .

เมื่อพบแล้ว docker จะทำการตีความทีละบรรทัดโดยจะนำ Base Image มาสร้างเป็น Container แล้วก็ Run คำสั่งหลังจากนั้นไปทีละบรรทัด หลังจากที่ run ผ่านแล้วก็จะ commit กลับมาเป็น image ตัวใหม่ และทำซ้ำอย่างนี้ไปเรื่อยๆ

ดังนั้นการวาง Script ไว้ในบรรทัดเดียวกันหรือคนละบรรทัดจะทำให้ได้ image ไม่เหมือนกัน

การ Build Dockerfile Docker จะไม่ Build ซ้ำที่เดิม นั่นคือถ้าขั้นตอนไหน run ผ่านแล้วก็จะ cache image เก็บไว้ ทำให้ไม่ต่้องกลับมา run ซ้ำตั้งแต่ขั้นตอนแรก

Docker Build แบบ No-Cache

ในการทำงานบางขั้นตอนจะเกิด Error ระหว่างทาง ซึ่งต้องกลับไป Build ใหม่ตั้งแต่ตั้น เราจะต้องใส่ –no-cache เข้าไปใน command เพื่อบอก docker ว่าไม่ต้องเอา image ที่อยู่ใน cached มาใช้

$ docker build --no-cache -t hello .

จะเขียน Dockerfile ต้องรู้จักกับ Keywords เหล่านี้

FROM

เราจะต้องระบุ Base Image ที่เป็น Image เริ่มต้น ซึ่งการ re-use image แบบนี้จะช่วยให้เราไม่ต้องไปสร้าง image จาก OS เปล่าๆ ตลอดเวลา

FROM node

ตาม Best practices แล้วเราต้องระบุ tag ลงไปด้วยเสมอ เพราะจะได้รู้ว่า version ที่ลงมา ณ ปัจจุบันนี้เป็น version ไหน

FROM node:lts-alpine3.18

Multi-Stage Builds

เป็นการ build ที่มี keyword FROM อย่างน้อย 2 จุด ส่วนใหญ่เราจะใช้ Base Image ตัวแรกสำหรับการ Build และ Base Image ตัวที่ 2 สำหรับการ run

การทำ multi-stage builds จะช่วยให้เราไม่ต้องเอา source code ไปบรรจุไว้ใน container เราจะเอาแค่ผลลัพธ์ที่ได้จากการ build ไปใส่ไว้เท่านั้น ยกตัวอย่าง เช่น

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# syntax=docker/dockerfile:1
FROM golang:1.21
WORKDIR /src
COPY <<EOF ./main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]

จากตัวอย่างนี้ docker จะทำตามขั้นตอนนี้

  1. build ไฟล์ main.go ด้วย Base Image ที่ชื่อว่า golang:1.21 ได้ผลลัพธ์ออกมาเป็น binary file ชื่อว่า hello ที่เก็บอยู่ใน folder /bin
  2. หลังจากนั้นก็จะนำ hello ไปวางไว้ใน Base Image ที่ชื่อว่า scratch ซึ่งเป็น image แบบว่างเปล่า เพราะหลังจากที่เรา build main.go ออกมาแล้วเราจะได้ binary ที่สามารถ run ได้ด้วยตัวเอง

เราสามารถเพิ่ม stage มากกว่า 2 stages ได้

ซึ่งข้อดีที่เราได้จากการทำ Multi-stage builds คือ

  1. ใน Image ที่เอาไปใช้งานมีขนาดเล็กลง
  2. Image ที่นำไป deploy มีความปลอดภัยมากขึ้น เพราะไม่มีใครเห็น source code

RUN

เป็นคำสั่งที่ใช้ run command ใน shell ของ container ดังนั้นเราต้องการ update หรือ install service ใดๆ เราก็จะใช้คำสั่ง run ตัวอย่างเช่น

RUN echo 'hello world'

command ที่เรา run จะต้องใช้ shell หรือ /bin/sh แต่ถ้า image ที่เรานำมาใช้เป็น Base Image ไม่สามารถใช้ shell ได้ เราต้องระบุ shell เข้าไปแบบนี้ (ในตัวอย่างนี้เป็นการใช้งาน Bash แทน Shell)

RUN /bin/bash -c 'source $HOME/.bashrc && echo $HOME'

เราสามารถขึ้นบรรทัดใหม่ได้เหมือนกับที่เราใช้ใน shell ได้เลย

RUN /bin/bash -c 'source $HOME/.bashrc && \
echo $HOME'
RUN ["/bin/bash", "-c", "echo hello"]

CMD

เป็นการ run command ตอนที่เรา run container ซึ่งจะต่างจาก keyword RUN ด้านบน ซึ่งจะทำงานตอน build image การใช้ CMD จะมีอยู่ 3 รูปแบบคือ

ENTRYPOINT

เป็นการระบุ default command ตอนสั่ง docker run เราจะสามารถส่ง parameters เข้ามาได้เลยทันที ซึ่งรูปแบบการเขียนจะเหมือนกับ RUN และ CMD คือ

เราสามารถเปลี่ยน entrypoint ใหม่ได้ตอน run ด้วยคำสั่ง

docker run --entrypoint [คำสั่งใหม่] [ชื่อ image]

COPY

หลังจากที่เราได้ Service ที่เราต้องการมาเรียบร้อยแล้วเราก็ต้อง copy source code หรือ script ต่างๆ เข้าไปใน container โดยจะเขียนแบบนี้

FROM node:lts-alpine3.18
COPY package.json /

ปัญหาของการ Copy บน Windows คือจะมี Error ถ้าเราใส่ \ เราต้องใส่ escape เข้าไปในบรรทัดแรก

# escape=`
FROM microsoft/nanoserver
COPY testfile.txt c:\

EXPOSE

เป็นการเปิด port เพื่อให้สามารถ bind port ออกไปใช้งานข้างนอกได้ โดยรูปแบบของการ EXPOSE จะเป็นแบบนี้

EXPOSE <port> [<port>/<protocol>...]

โดย default จะใช้ TCP protocol

EXPOSE 80
# จะเหมือนกับ
EXPOSE 80/tcp

ENV

เป็นการกำหนดต่า Environment Variables ให้กับ container(เมื่อเราสร้าง contaienr ขึ้นมาจะมี Environment Variables ที่เราได้กำหนดไว้) โดยมีรูปแบบการเขียนแบบนี้

ENV <key>=<value> ...

ยกตัวอย่าง เช่น

ENV ASPNET_ENVIRONMENT=Development
ENV APPLICATION_NAME="Demo Web Application"

แต่เราสามารถเปลี่ยนต่าตอน run container ได้ด้วย parameter -e หรือ –env

docker run --env ASPNET_ENVIRONMENT=Production [ชื่อ image]

จัดการ Volume ใน Dockerfile

ในการทำงานกับ container เราต้องพยายามไม่ให้ container เก็บ data เพื่อคงความเป็น Stateless ดังนั้นตรงไหนที่มี data ที่มีการเปลี่ยนแปลงไม่ว่าจะเป็น source code หรือ log file เราก็ต้องย้ายสิ่งเหล่านี้ออกไปไว้ใน volume ซะ

เราสามารถนิยาม Docker Volume ไว้ใน Dockerfile ได้เพื่อบอกว่าภายใน container ของเรานั้นต้องการ volume ใน path ไหนบ้าง เช่น

VOLUME ["/data"]

หรือจะเขียนเป็น string ต่อกันก็ได้

VOLUME /var/log /var/db

ตอนที่เราสั่ง docker run docker จะสร้าง volume ใหม่ขึ้นมาโดย volume ที่สร้างขึ้นจะเก็บ data ที่อยู่ใน folder นั้น เช่น ใน folder /data มี file หรือ folder ใดๆอยู่ ณ ตอนที่สั่ง docker run ก็จะถูกย้ายออกไปยัง volume ตัวใหม่นี้ อ่านวิธีการจัดการ Docker Volume ได้ที่บทความนี้ต่อ

อ่านต่อเพิ่มเติม

Phanupong Permpimol
Follow me