วิธีการเขียน Dockerfile เบื้องต้น
Dockerfile คือ ไฟล์ที่บอกขั้นตอนการสร้าง Container Image ซึ่งเราจะต้องระบุ
- Base Image ที่จะนำมาใช้เป็น Image เริ่มต้น
- คำสั่งต่างๆที่คุณค้องการ RUN ใน Base Image เพื่อให้ได้ Service ต่างๆที่เราต้องการ
- นำ Code หรือ Content ของเราเข้าไปใน Base Image นั้น
- 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 ไปใส่ไว้เท่านั้น ยกตัวอย่าง เช่น
|
|
จากตัวอย่างนี้ docker จะทำตามขั้นตอนนี้
- build ไฟล์ main.go ด้วย Base Image ที่ชื่อว่า golang:1.21 ได้ผลลัพธ์ออกมาเป็น binary file ชื่อว่า hello ที่เก็บอยู่ใน folder /bin
- หลังจากนั้นก็จะนำ hello ไปวางไว้ใน Base Image ที่ชื่อว่า scratch ซึ่งเป็น image แบบว่างเปล่า เพราะหลังจากที่เรา build main.go ออกมาแล้วเราจะได้ binary ที่สามารถ run ได้ด้วยตัวเอง
เราสามารถเพิ่ม stage มากกว่า 2 stages ได้
ซึ่งข้อดีที่เราได้จากการทำ Multi-stage builds คือ
- ใน Image ที่เอาไปใช้งานมีขนาดเล็กลง
- 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 รูปแบบคือ
-
CMD command param1 param2 (run command โดยใช้ shell)
FROM ubuntu CMD echo "This is a test command."
ในรูปแบบนี้จะเป็นการ run command echo โดยส่ง parameter เป็น “This is a test command.”
-
CMD [“executable”,“param1”,“param2”] (เป็นรูปแบบที่ใช้เยอะที่สุดเหมือนกับ RUN สามารถใช้ bash แทน shell ได้)
FROM ubuntu CMD ["sh", "-c", "echo 'This is a test command.'"]
จะเหมือนกับรูปแบบแรก แต่ในรูปแบบนี้เราสามารถเปลี่ยนจาก sh เป็น bash ได้
-
CMD [“param1”,“param2”] (เป็นการส่ง parameter เข้าสู่ ENTRYPOINT)
FROM ubuntu ENTRYPOINT ["top", "-b"] CMD ["-c"]
รูปแบบนี้เราจะใช้ CMD ในการกำหนด default parameters อย่างในตัวอย่างนี้เราจะกำหนด default parameter เป็น -c หมายความว่า ถ้าเราสั่ง docker run แบบไม่มีอะไรต่อท้าย
$ docker build -t custom-ubuntu . $ docker run --rm custom-ubuntu
คำสั่งที่ถูกเรียกใช้ใน container ตือ
# top -b มาจาก ENTRYPOINT $ top -b -c
แต่ถ้าเราใส่ parameters เข้าไป เช่น
$ docker build -t custom-ubuntu . $ docker run --rm custom-ubuntu -h
คำสั่งที่ใช้ run ใน shell คือ
$ top -b -h
การเขียน CMD รูปแบบนี้ต้องมาพร้อมกับ ENTRYPOINT
ENTRYPOINT
เป็นการระบุ default command ตอนสั่ง docker run เราจะสามารถส่ง parameters เข้ามาได้เลยทันที ซึ่งรูปแบบการเขียนจะเหมือนกับ RUN และ CMD คือ
- ENTRYPOINT command param1 param2
- ENTRYPOINT [“executable”, “param1”, “param2”]
เราสามารถเปลี่ยน 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 ได้ที่บทความนี้ต่อ