ลดขนาด Image size ของ Docker ด้วย Build stages
Table of contents
Intro
สวัสดีครับ บทความนี้เขียนเกี่ยวกับ Docker โดยจะเป็นเทคนิคการลดขนาด Image size หลังจากที่ Build Dockerfile เพื่อสร้าง Docker images ไว้สำหรับใช้รัน Containers
ดังนั้นการ Optimizing เพื่อทำให้ Docker image เบา (slim) ที่จะช่วยเพิ่มความเร็วและลดขนาดบนดิสสำหรับ Build และ Deploy Containers ที่ควรเข้าใจและฝึกฝนการปรับแต่ง Docker image ให้เหมาะสม
ความสำคัญกับขนาด Docker images
ขนาดของ Docker images มีความสำคัญด้วยเหตุผลต่าง ๆ ดังนี้
- การ Deployment ที่เร็วเมื่อ Download, Transfer หรือ Load เพื่อใช้สร้างเป็น Container อีกทั้งช่วยปรับปรุงประสิทธิภาพของแอปพลิเคชัน
- การลดการใช้พื้นที่เก็บข้อมูลขึ้นในเครื่องที่ดีขึ้น
- ลดแบนด์วิธของเครือข่ายเมื่อถ่ายโอนระหว่าง Host และ Container
- การลบไฟล์ที่ไม่จำเป็น เพื่อการกำจัดส่วนประกอบที่มีช่องโหว่ภายใน Image เกิดปัญหาด้านความปลอดภัย
- เพื่อประสิทธิภาพสำหรับการ Build ที่เร็ว
Docker Build stage
Docker Build stage คือการกำหนด Stage environments ที่แตกต่างกันตามวัตถุประสงค์ และสามารถใช้งานเครื่องมือ (env) ระหว่าง Stage ร่วมกันได้ เช่น
- Stage: develop (สำหรับโหมด Development)
- Stage: testing (สำหรับโหมด UAT)
- Stage: prod (สำหรับโหมด Production)
Single stage
ตามปกติแล้วในขั้นตอนการ Build Docker image เราจะเขียนคำสั่งต่าง ๆ ไว้ใน Dockerfile
สมมุติว่ามีคำสั่งดังนี้
package.json
{
"name": "express-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "node index.js",
"dev": "nodemon index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.1"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
}
# Node.js base image
FROM node:20
# Docker working directory
WORKDIR /app
# Copy dependencies files
COPY package*.json ./
# Install dependencies on Docker
RUN npm install
# Copy application files
COPY . .
# Application start scripts
CMD npm start
จากนั้นสั่ง build Dockerfile
และใช้คำสั่ง docker image ls
เพื่อดูขนาดไฟล์ที่ build แล้วได้ขนาดไฟล์ดังนี้
$ docker build -t node_single_stage .
...
$ docker image ls -a
REPOSITORY TAG IMAGE ID CREATED SIZE
node_single_stage latest b0f6fb628da5 9 seconds ago 959MB
จะเห็นว่า Image มีขนาด 959MB
เป็นขนาดที่ใหญ่พอสมควร แม้ว่าจะเป็น App ตัวอย่างง่าย ๆ และอีกอย่างอย่าลืมว่าถ้า App โตขึ้นจะมีขนาด Image ใหญ่ขึ้นเช่นกัน
Multiple stages
Multiple stages คือการกำหนดหลาย stage ใน Dockerfile
โดยใช้คำสั่ง FROM... AS
ซึ่งแต่ละ stage สามารถใช้ Base image และคำสั่ง (commands) ที่แตกต่างกันได้
นี่คือตัวอย่างอย่างง่ายที่ประกอบด้วย 3 stages
- Base stage: ใช้สำหรับกำหนด base image (หรือใช้สำหรับเป็น base สำหรับ Package manager ตัวอื่นเช่น yarn หรือ pnpm)
- Development stage: ใช้สำหรับติดตั้ง dependencies และ devDependencies
- Production stage: ใช้สำหรับติดตั้ง dependencies เท่านั้น
#Stage 0: Base image
FROM node:20 AS base
#Stage 1: Development Application
FROM base AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD npm start
#Stage 2: Production Application
FROM node:20-alpine AS production
WORKDIR /app
COPY . .
COPY /app/node_modules ./node_modules
RUN npm prune --production
CMD npm start
หลัง build แล้ว Image มีขนาด 184MB
จะเห็นว่าขนาดนั้นลดลงเยอะพอสมควร
Build with target stage (command)
$ docker build --target development -t node_multiple_stages_dev .
$ docker image ls -a
REPOSITORY TAG IMAGE ID CREATED SIZE
node_multiple_stages_prod latest 2975042643d1 58 seconds ago 184MB
node_multiple_stages_dev latest 80223960900f About a minute ago 959MB
โดย production image จะทำการคัดลอก dependencies จาก development ข้อดีคือไม่จำเป็นต้องสั่ง npm install อีกรอบจากนั้นก็จะลบ devDependencies ออก เพื่อลดขนาด image
ตัวอย่าง Build stages กับ React Frontend
เพื่อให้เข้าใจมากขึ้นตัวอย่างนี้จะเป็น docker build stages กับ React โดยจะประกอบด้วย 4 stages ดังนี้
สร้าง React template ด้วย vite
pnpm create vite my-react-app --template react-ts
cd my-react-app
สร้าง Dockerfile
และกำหนดคำสั่งแต่ละ Stages ดังนี้
ตัวอย่างนี้จะใช้ร่วมกับ Package manager pnpm
FROM node:20-alpine AS base
RUN npm i -g pnpm
FROM base AS dependencies
WORKDIR /app
COPY package.json pnpm-*.yaml ./
RUN pnpm install
FROM base AS build
WORKDIR /app
COPY . .
COPY /app/node_modules ./node_modules
RUN pnpm prune --prod
RUN pnpm build
FROM nginx:alpine AS deploy
WORKDIR /app
COPY /app/dist ./dist
COPY nginx-default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
สร้างไฟล์ nginx-default.conf
nginx-default.conf
server {
listen 80;
location / {
root /app/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
}
ทำการ build และทดสอบ run container ด้วย image
docker build -t my-react-app .
docker run -p 8088:80 -d --name my-react-app -t my-react-app
ประโยชน์ของ multi-stage builds
- ได้ Images ขนาดที่เล็ก
- ไม่จำเป็นต้องสร้างไฟล์ Dockerfiles สำหรับ Development และ Production แยกกัน
- Maintain Dockerfile ไฟล์เดียวสำหรับ Multistage build ที่สามารถรวมหลาย environments ที่สามารถสร้าง production builds ได้
ปัจจัยที่ส่งผลต่อขนาดของ Docker image
Base images
Docker base image เป็น Layer พื้นฐานในการ Build Docker image โดยจะเป็น Pre-built images รวมกับเครื่องมือและ libraries หลายตัวที่จำเป็นเพื่อให้ Application ทำงาน ใน Containers
ดังนั้นการเลือกใช้งาน Docker base image สำหรับการ Optimized นั้นก็ตามแต่ละวัตถุประสงค์การใช้งานของแต่ละ use case
- node:[version]-alpine ใช้ Alpine linux distributions ที่มีขนาดของ base image แค่ 5MB และเมื่อรวมกันกับ NodeJS แล้วคือ node:alpine จะใช้พื้นที่ประมาณ 50MB
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD npm start
$ docker build -t node_alpine .
...
$ docker image ls -a
REPOSITORY TAG IMAGE ID CREATED SIZE
node_single_stage latest b0f6fb628da5 5 minutes ago 959MB
node_alpine latest cc410c9fd328 1 minutes ago 188MB
- node:[version]-slim ใช้ Debian linux ที่เป็น version slim ทำให้ image size มีขนาดเล็กแต่ก็ยังมีความสามารถบางอย่างของ Debian ที่ไม่ได้ตัดออกซึ่ง image แบบนี้เหมาะสำหรับนำมาใช้ใน Development mode
FROM node:20-slim
WORKDIR /app
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD npm start
$ docker build -t node_slim .
...
$ docker image ls -a
REPOSITORY TAG IMAGE ID CREATED SIZE
node_single_stage latest b0f6fb628da5 6 minutes ago 959MB
node_alpine latest cc410c9fd328 2 minutes ago 188MB
node_slim latest 12ba43cc45a6 1 minutes ago 276MB
แสดงว่าการเลือกใช้ Base image ตั้งแต่แรกนั้นพอที่จะคาดเดาได้ว่าสุดท้ายขนาดของ Image size หลัง build จะมีขนาดเท่าไหร่ ดังนั้นต้องตรวจสอบ image tags ที่เลือกใช้เสมอ
Docker image layers
Docker image ถูกแบ่งออกเป็นชั้น ๆ (Layers) โดยที่ Layers ถูกสร้างโดยคำสั่งใด ๆ ที่อยู่บน Dockerfile ซึ่งเป็น Dockerfile command หรือเป็น Image layer
นั่นก็หมายความว่าในทุก ๆ Dockerfile command เช่น FROM, WORKDIR, COPY, RUN, COPY, EXPOSE จะเป็นการสร้าง Layer ของคำสั่งนั้น ๆ จากนั้น Layers ทั้งหมดจะถูกรวมเป็น Image
โดยที่เมื่อเพิ่มคำสั่งต่าง ๆ ใน Dockerfile ก็จะทำให้ Image สะสมและเพิ่มจำนวน Layers ทำให้ขนาดของ Image size เพิ่ม ดังนั้นทุกครั้งที่ Build ควรที่จะลดขนาดของ Layers เพื่อให้ได้ Image ที่มีขนาดเล็ก
ตัวอย่างการเพิ่ม wildcard เพื่อบอกให้ Docker Copy ทั้ง package.json และ package-lock.json ใน layer เดียว
COPY package.json ./
COPY package-lock.json ./
COPY package*.json ./
ตัวอย่างการลดขนาดของ Layers ด้วยการพยายามรวบคำสั่ง
FROM node:20-alpine
RUN apk add --no-cache git
RUN mkdir -p /app
RUN chown node:node /app
WORKDIR /app
USER node
RUN git clone https://github.com/Azure-Samples/nodejs-docs-hello-world .
RUN npm install
EXPOSE 3000
CMD npm start
FROM node:20-alpine
RUN apk add --no-cache git && \
mkdir -p /app && \
chown node:node /app
WORKDIR /app
USER node
RUN git clone https://github.com/Azure-Samples/nodejs-docs-hello-world . && \
npm install
EXPOSE 3000
CMD npm start
Add .dockerignore
อีกวิธีการลดขนาดของ Docker image คือการสร้างไฟล์ .dockerignore
เพื่อระบุไฟล์ที่ไม่ต้องการไว้ เป็นเพราะว่าไม่ใช่ทุกไฟล์หรือทุกโฟล์เดอร์ที่จะต้องการ Copy ไปไว้ใน Image ดังนั้นการใช้คำสั่ง COPY . .
จะเป็นการคัดลอกไฟล์ทั้งหมดก็จริง แต่ถ้ามีการกำหนด .dockerignore
ไว้ ก็เป็นการกำหนดรายการที่ยกเว้นไว้
สำหรับตัวอย่างคือ เมื่อเราสร้าง Node.js image โดยที่จะมี Folders หรือ Files เช่นดังนี้ node_modules, .dist, build และ npm-debug.log ทำให้บางครั้งเมื่อ Build stage อื่น ๆ Image นั้น ๆ บางทีก็ไม่ได้ต้องการที่จะใช้ไฟล์เหล่านี้เสมอไปดังนั้นการทำ .dockerignore
ก็จะช่วยยกไว้ไฟล์อย่างที่ไม่ต้องการได้
Compression tools
วิธีการลดขนาด Docker image ด้วยการใช้เครื่องมือช่วยสำหรับทำ minify
เช่น
>> docker pull archlinux:latest
...
>> slim build --target archlinux:latest --tag archlinux:curl --http-probe=false --exec "curl checkip.amazonaws.com"
...
>> docker run archlinux:curl curl checkip.amazonaws.com
...
>> docker images
archlinux curl ... ... 17.4MB
archlinux latest ... ... 467MB
...
สรุป
การลดขนาดของ Docker image มีหลายวิธี และ Docker multi stages ก็เป็นหนึ่งในนั้น แต่สิ่งที่ทำให้ Docker multi stages พิเศษคือการสร้าง Stages หลายแบบเพื่อแยกส่วนการทำงานแล้วแต่การใช้งานตาม use case
ดังนั้นการพิจารณาการนำ Docker multi stages มาใช้กับงาน Development เป็นสิ่งที่ดี และถ้าเป็นไปได้ควรเขียน Dockerfile แบบ multi stages ไว้กับทุก Project เลยจะดีมาก
ซึ่ง Stages ใน Project ที่ควรจะมี มีดังนี้
- Base stage: ใช้สำหรับกำหนด base image
- Development stage: ใช้สำหรับติดตั้ง dependencies และ devDependencies
- Staging stage: คล้าย Development stage แต่เป็นการใช้
.env
ที่เสมอเป็น Production - Production stage: ใช้สำหรับติดตั้ง dependencies เท่านั้น
สุดท้ายกา รพิจารณาการในสร้าง Stages ว่าควรมีเท่าไหร่ก็อยู่ที่ Context ของงานว่าต้องการให้แต่ละ Stage มีบริบททำงานแต่อย่างอย่างไร เช่น การใช้ .env
ที่แตกต่างกัน หรือแสดงการ Logs เพื่อ Debug หรือใช้เป็น Stage ทดสอบ
References
- ABOULLAITE, M. (2018). Building thin Docker images using multi-stage build for your java apps!. https://aboullaite.me/multi-stage-docker-java/
- Docker Inc. (2023, May 5). Multi-stage builds | Docker Documentation. Docs. https://docs.docker.com/build/building/multi-stage/
- Docker Inc. (2023, April 4). docker build | Docker Documentation. Docs. https://docs.docker.com/engine/reference/commandline/build/
- Omojola, S. (2022, October 10). Using DockerSlim to minimize container image size - LogRocket Blog. Blog. https://blog.logrocket.com/using-dockerslim-minimize-container-image-size/