Skip to main content

ลดขนาด Image size ของ Docker ด้วย Build stages

Kongvut Sangkla

Intro

สวัสดีครับ บทความนี้เขียนเกี่ยวกับ Docker โดยจะเป็นเทคนิคการลดขนาด Image size หลังจากที่ Build Dockerfile เพื่อสร้าง Docker images ไว้สำหรับใช้รัน Containers

ดังนั้นการ Optimizing เพื่อทำให้ Docker image เบา (slim) ที่จะช่วยเพิ่มความเร็วและลดขนาดบนดิสสำหรับ Build และ Deploy Containers ที่ควรเข้าใจและฝึกฝนการปรับแต่ง Docker image ให้เหมาะสม

docker multiple stages

ความสำคัญกับขนาด 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
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"
}
}
Dockerfile (single build)
# 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 image size (single stage)
$ 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 เท่านั้น
Dockerfile (multiple stage)
#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 --from=development /app/node_modules ./node_modules
RUN npm prune --production
CMD npm start

หลัง build แล้ว Image มีขนาด 184MB จะเห็นว่าขนาดนั้นลดลงเยอะพอสมควร

Build with target stage (command)
build with target stage
$ docker build --target development -t node_multiple_stages_dev .
Docker image size (multiple stage)
$ 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 stage with react

เพื่อให้เข้าใจมากขึ้นตัวอย่างนี้จะเป็น 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

Dockerfile
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 --from=dependencies /app/node_modules ./node_modules
RUN pnpm prune --prod
RUN pnpm build

FROM nginx:alpine AS deploy
WORKDIR /app
COPY --from=build /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
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

run react on nginx with docker

ประโยชน์ของ 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
dockerfile
FROM node:20-alpine
WORKDIR /app

COPY package.json ./
COPY package-lock.json ./

RUN npm install
COPY . .

EXPOSE 3000
CMD npm start
Docker image size
$ 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
dockerfile
FROM node:20-slim
WORKDIR /app

COPY package.json ./
COPY package-lock.json ./

RUN npm install
COPY . .

EXPOSE 3000
CMD npm start
Docker image size
$ 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 เดียว

before
COPY package.json ./
COPY package-lock.json ./
after
COPY package*.json ./

ตัวอย่างการลดขนาดของ Layers ด้วยการพยายามรวบคำสั่ง

before
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
after
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 Slim
>> 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

Loading...