จาก Blog ตอนก่อนที่ไปพบว่า docker image มีขนาดใหญ่มาก และถ้าปล่อยไปนานๆ ไม่น่าจะดีแน่นๆ หลังจากแก้ปัญหาเรียบร้อยไป ผมขอสรุป Step การ Optimize Docker image ครับ //ดองไปหลายเดือนกว่าจะเขียนจนจบได้
Best Practice
- Use Minimal Base Images
พยายามเลือกใช้ Base Image ที่เล็กที่สุด และน่าเชื่อถือด้วยครับ ส่วนใหญ่จะเป็นพวกตระกูล
- runtime specific image - ใช้ภาษาไหน เอา image ที่มีคนทำไว้ เช่น dotnet / java
- slim/minimal tag - Linux บาง distro เค้ามี optimize image นะ เช่น ubuntu / rockylinux
- alpine - เล็กลง แต่ยังมีความเป็น distro ของ Linux นั้นๆอยู่
- distroless Image (Image ที่ตัดพวก Tools ที่มากับ distro บางส่วนออกไป เช่น พวก Shell / Package ช่วยต่างๆ ทำให้ขนาดเล็กลง debug ยาก และเพิ่ม Security (Attacker ไม่มี Tools ใช้)
ข้อสังเกตุ
- Base Image เล็กเกิน อาจจะไม่ดีเสมอไปนะครับ สำหรับผมจะเป็นเคส .NET6 + DB2 เอา alpine มาลงเอง size ใหญ่กว่า แถม Run ไม่ได้ ทดสอบก่อนใช้ครับ
- พวก runtime specific image หลายๆค่ายมีทำ image แบบ slim tag / alpine หรือ แบบ distroless image อยู่แล้วด้วย
- พวก runtime อย่าง java มันมีวิธีลดขนาด image อยู่นะ ใช้ Jdeps หา dependency ที่ app ใช้ และ JLink มา Custom Java Runtime อื่น //แต่ส่วนตัวไม่ค่อยได้ใช้จริง มันดูจุกจิไป
- Minimize the Number of Layers
การ Run Command แต่ละรอบ
- สิ่งที่เกิดขึ้น มันจะไปเพิ่ม Layer ให้เพิ่ม เหมือนกัน Commit Code ยิ่งมีหลายชั้น ขนาดยิ่งเพิ่ม อารมณ์แบบเราทำพวกไฟล์ final / final1 / final2 / finalแล้วโว้ยยย บวมตามนั้นแหละ
- Command ที่มีผลกับการเพิ่ม Layer มีอะไรบ้าง
- FROM
- RUN
- COPY - หน้าที่ COPY จาก Local Source > Docker
NOTE: ถ้าต้อง Copy หลายๆ Folder มาใส่ Path เดียวกันใน Container จัด Structure ให้เรียบร้อย ก่อน Copy
- ADD - หน้าที่ เหมือนกับ COPY แต่รองรับ remote source (URL) / แตก TAR
ส่วนตัวผมใช้ COPY มากกว่า ไปเตรียมจากข้างนอกให้เรียบร้อยก่อน
COPY กับ RUN chowns นี้เพิ่ม Layer ได้เหมือนกันนะ เป็นไปได้ ยุบรวมกัน ยิ่งถ้า Copy Folder มาแล้ว มันมี Sub Directory เยอะ พอแยก Command กัน ได้เป็นหลายชั้นเลย แบบอันนี้ [Container] แก้ปัญหา docker image โต
- แบบที่ไม่ควร
COPY config . RUN chown -R youruser config
- แบบที่ควร
COPY --chown=youruser config .
ADD กับ RUN สามารถ Optimize ได้นะ เคสนี้จะเป็นเคสที่เรา download package จาก remote มาทำอะไรสั่งอย่าง และ execute command ต่อ
- แบบที่ไม่ควร - เกิด 2 Layer
ADD http://yoursource.file/package.file.tar.gz /tmpf RUN tar -xjf /tmpf/package.file.tar.gz \ && make -C /tmpf/package.file \ && rm /tmpf/ package.file.tar.gz
- แบบที่ควร - ยุบเหลือ 1 Layer หรือ จะไปปรับจากข้างนอก แลัว Copy มา หรือ ทำ multi-Stage แทนก็ได้ครับ
RUN curl http://yoursource.file/package.file.tar.gz \ | tar -xjC /tmpf/ package.file.tar.gz \ && make -C /tmpf/ package.file.tar.gz NOTE: แบบนี้บางแหล่งจะเรียกว่า Copy on Write ครับ
ส่วนคำสั่ง RUN เป็นไปได้ยุบให้เหลือน้อยที่สุดครับ โดยตัวอย่างจะอยู่ในเรื่องถัดไปพอดีครับ
- Installing dependencies & Cleaning in same layer
เป็นไปได้ ไม่จำเป็น ไม่ต้องไปลงอะไรเพิ่มนะครับ แต่ถ้าเสี่ยงไม่ได้ ลงเท่าที่จำเป็นเท่านั้น เพราะการ Run Command แต่ละรอบ มันจะไปเพิ่ม Layer
พวก Cache ของ package manager ใช้เฉพาะตอน Restore ใข้เสร็จอย่าลืมไปลบออกด้วย ไม่งั้นมันจะไปงอกใน image แทน
- Debian/Ubuntu (--no-install-recommends) ลงเท่าที่ระบุ
apt-get install -y --no-install-recommends <<list of package names to install>> && <optional: do something with packages> && apt-get clean && rm -rf /var/lib/apt/lists/*
- RPM Based (yum / dnf) พวกกลุ่ม CentOS / RHEL + พวก distro อื่นๆ SUSE
dnf -y install --setopt=install_weak_deps=False <<list of package names to install> && <<optional: do something with packages>> && dnf clean all
- Node.js (npm) npm ci = clean install ทำเสร็จ Clear Cache
npm ci && npm cache clean --force
- Python (pip)
pip install --no-cache-dir <<list of package names to install>>
- dotnet ตัว --no-cache บอกว่าไม่ต้องทำ http cache
dotnet restore --no-cache
ต้วอย่าง รวมเรื่องการใช้ RUN และ dependencies + cleaning ไปด้วยเลย
- แบบที่ไม่ควร - Run yum ไป 8 รอบ มี Layer 8 ชั้นเพิ่มเข้ามา
FROM rockylinux:8 ... ### - SHELL RUN yum install -y ksh-20120801 ### - Package for db2 RUN yum install -y binutils-2.30 RUN yum install -y kernel-devel-4.18.0 RUN yum install -y patch-2.7.6 libaio-0.3.112 RUN yum install -y numactl-2.0.12 ### - JAVA RUN yum install -y java-17-openjdk RUN yum install -y java-17-openjdk-devel
- แบบที่ควร - รวมคำสั่งให้ install + clean ให้ทำรอบเดียว เกิด 1 Layer เท่านั้น
FROM rockylinux:8 ... RUN yum install -y ksh-20120801 ncurses-6.1 binutils-2.30 kernel-devel-4.18.0 patch-2.7.6 libaio-0.3.112 numactl-2.0.12 openssh-clients-8.0p1 java-17-openjdk java-17-openjdk-devel; yum clean all
- Using .dockerignore file
.dockerignore เหมือนกับตัวไฟล์ .gitignore มันจะช่วยให้ป้องกัน เราเผลอใส่อะไรที่ไม่จำเป็นลงไป เช่น Application Logs / Application Data หรือ พวก Secret เป็นต้น ยกตัวอย่าง เช่น
.git .vscode .dockerignore node_modules npm-debug.log API_KEY.txt appdata
ถ้าคิดอะไรไม่ออก ลองไปดูจาก .gitignore มาเป็น guideline ก็ได้ครับ gitignore.io - Create Useful .gitignore Files For Your Project (toptal.com)
- Docker Multi-Stage Build
docker Multi-Stage คือ การแบ่งช่วงการบิ้ว Container ออกมาเป็น Step ย่อยๆ เพื่อลดการเกิด Layer ที่ไม่จำเป็น จากโจทย์ที่ว่า Image ที่เอาไปขึ้น Production ไม่ต้องมีของช่วง Test / Build / Initial Container ลง Package ต่างๆ ปนเข้าไปด้วยครับ
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS restore WORKDIR /app COPY *.csproj ./ RUN dotnet restore FROM restore AS build COPY ../engine/examples ./ RUN dotnet build -c Release --no-restore FROM build AS test RUN dotnet test -c Release --no-build --logger trx FROM scratch as export-test-results COPY --from=test /app/TestResults/*.trx . FROM build AS publish RUN dotnet publish -c Release --no-build -o out FROM mcr.microsoft.com/dotnet/aspnet:7.0 WORKDIR /app COPY --from=publish /app/out . ENTRYPOINT ["dotnet", "aspnetapp.dll"]
อันนี้เหมือนเรา dockerfile ข้างบนมา Visualize ดูจะพบว่าตัว Final Image จะได้ WebApp ที่ผ่านการ dotnet publish มาเรียบร้อย ไม่ต้องมีของในช่วง Step Restore / Build / Test (Report / Coverage) เข้ามาปนเลย แต่ควรเปิด Space นิดนึง มันจะใช้ระหว่างทางเยอะ แต่พอทำเสร็จก็ Clear หมด
How to Investigate Size
- มีหลาย Tool อย่างตัว docker หรือ podman เองมี command เพื่อให้ตรวจสอบแต่ละ Layer ใช้ Disk ไปเท่าไหร่ อย่าง
- docker history
- podman history > มี Blog ผมที่ลองด้วยนะ [PODMAN] มาดูกันว่า Image มันมีอดีตอะไร ทำไมถึงบวม
- ใช้ Tools อย่าง wagoodman/dive: A tool for exploring each layer in a docker image (github.com) เข้ามาช่วยตรวจสอบได้รองรับทั้ง docker / podman เลย
Container Image Optimization Tools
- SlimToolkit มัน คือ Docker Slim แต่เปลี่ยนชื่อ
- docker --squash เป็น experimental มานานแล้ว ถ้าลองใช้ต้องไปเปิด Experimental ก่อนครับ
docker build . -t invapi_squash_v1.0.12 --squash
- wagoodman/dive: A tool for exploring each layer in a docker image (github.com)
- goldmann/docker-squash: Docker image squashing tool (github.com)
Summary
- สำหรับวิธีการที่ผมรวบรวมมา แม้ว่าจะไม่ได้ Build Image โดยใช้ docker อาจจะใช้ตัว Buildah (ที่มาคู่กับ Podman) ก็สามารถใช้งานได้นะ ถ้าถูกพัฒนาตามมาตรฐานกลางของทาง OCI (Open Container Initiative) ครับ
Reference
- Finding the layers and layer sizes for each Docker image - Stack Overflow
- How To Reduce Docker Image Size: 5 Optimization Methods (devopscube.com)
- BEST PRACTICES TO REDUCE DOCKER IMAGES SIZE - CLOUDCONTROL (ecloudcontrol.com)
- Advanced Dockerfiles: Faster Builds and Smaller Images Using BuildKit and Multistage Builds - Docker
- yum install package without updating other packages or fail...? - Unix & Linux Stack Exchange
- Docker image and yum clean all (github.com)
- Dockerfile reference | Docker Documentation
- Difference between the COPY and ADD commands in a Dockerfile - GeeksforGeeks
- Using JLink to create smaller Docker images for your Spring Boot Java application - DEV Community
- Keep it small: a closer look at Docker image sizing | Red Hat Developer
Discover more from naiwaen@DebuggingSoft
Subscribe to get the latest posts sent to your email.