Optimize Container image size

จาก 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
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

Container Image Optimization Tools

docker build . -t invapi_squash_v1.0.12 --squash

Summary

  • สำหรับวิธีการที่ผมรวบรวมมา แม้ว่าจะไม่ได้ Build Image โดยใช้ docker อาจจะใช้ตัว Buildah (ที่มาคู่กับ Podman) ก็สามารถใช้งานได้นะ ถ้าถูกพัฒนาตามมาตรฐานกลางของทาง OCI (Open Container Initiative) ครับ

Reference


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.