👤
Novice Handbook
  • Novice Handbook
  • Guide
  • Internet และ Web
    • HTML
  • Computer Language
    • Basic Computer Language (LV.1)
    • C Language (LV.1)
    • Python3 (LV.1)
  • Operating System
    • Linux
      • Basic Linux (LV.1)
  • TOOLS
    • Text Editor
      • Vim Editor
    • Source Control
      • GitLab
        • GitLab for small site (LV.1)
    • Container
      • Docker
        • Docker (LV.1)
        • Docker (LV.2)
      • Kubernetes
        • Kubernetes Intro (LV.0)
        • Kubernetes Basic (LV.1)
        • Kubernetes Intermediate (LV.2)
        • Helm (LV.2)
        • RKE2 (LV.3)
        • K3S (LV.3)
        • K3D (LV.3)
    • Repository
      • Harbor
        • Harbor for small site (LV.1)
        • Harbor for enterprise (LV.2)
    • Database
      • Redis
        • Redis on Docker Compose (LV.1)
        • Redis on Kubernetes (LV.2)
      • Elastic Stack
        • Elasticsearch & Kibana for small site (LV.1)
    • Observability
      • Prometheus
        • Prometheus for small site (LV.1)
        • Prometheus Operator (LV.2)
    • Security
      • Certbot (LV.1)
      • Falco
      • Hashicorp Vault
    • Collaboration
      • Nextcloud
Powered by GitBook
On this page
  • การวาง structure ของ file และ folder ร่วมกับ source code
  • การทำ Multi-stage build
  • Docker build cache
  • Docker buildx, BuildKit และ cache backend

Was this helpful?

  1. TOOLS
  2. Container
  3. Docker

Docker (LV.2)

การวาง structure ของ file และ folder ร่วมกับ source code

สมมติว่าเรามี source code NodeJS เราจะวาง Dockerfile, .dockerignore และ docker-compose ดังนี้

myapp
|- .git
|- .gitignore
|- app.js
|- package.json
|- package-lock.json
|- libs
|  |- math.js
|- test
|  |- math.test.js
|- Dockerfile
|- .dockerignore
|- docker-compose.yml

ตัวอย่างไฟล์ content ดังนี้

app.js

require('dotenv').config();
const express = require('express');
const app = express();
const port = process.env.PORT || '3000';

const Math = require('./libs/math');

app.get('/', (req, res) => {
  res.status(200).send('Hello World!');
});

app.get('/square', (req, res) => {
  let value = Math.square(2);
  console.log(`Value is ${value}`);
  res.status(200).send(value.toString());
});

app.get('/error', (req, res) => {
  res.status(500).send('Oops!');
});

app.listen(port, () => {
  console.log(`Example app listening at http://0.0.0.0:${port}`);
});

package.json

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.20.0",
    "jest": "^29.7.0"
  }
}

package-lock.json ปกติจะเอาขึ้น git ด้วย แต่ไม่ได้ใส่มาในตัวอย่างนี้ เนื่องจากมีขนาดใหญ่ และมีโอกาสเปลี่ยนแปลงในอนาคต ให้ gen มาด้วยการสั่ง npm install ในเครื่องของตนเอง

libs/math.js

const square = (num) => {
  return num * num;
};

module.exports = {
  square
};

test/math.test.js

const Math = require('../libs/math');

test('square 10 should equal 100', () => {
  expect(Math.square(10)).toBe(100);
});

Dockerfile

FROM node:21.7-alpine
RUN mkdir -p /app
WORKDIR /app
COPY package*.json /app/
RUN npm ci
COPY . /app/
EXPOSE 3000
CMD ["node", "app.js"]

จากตัวอย่าง Dockerfile เราจะสร้าง /app ไว้ แล้ว copy package.json และ package-lock.json เข้าไปก่อน คำสั่ง npm ci จะทำการติดตั้ง lib ทั้งหมดจาก config ใน package-lock.json ซึ่งจะการันตีว่า lib version ตรงกับที่ developer ใช้งานในเครื่องของตนเองแน่นอน หลังจากติดตั้ง dependency ทั้งหมด จะทำการ copy source code อื่นๆตามหลังไป ข้อดีของการแยก layer เช่นนี้ ทำให้เวลาเปลี่ยน source code แต่ไม่เปลี่ยน dependency จะไม่เกิดการ build dependency layer ทำให้ pipeline เสร็จไว

.dockerignore

**/*.md
**/test
**/*.test.js
**/node_modules
**/.git
**/.env*

.dockerignore เป็นไฟล์ที่ทำหน้าที่คล้าย .gitignore ใน git มีอยู่เพื่อป้องกันการ copy file ที่ไม่จำเป็นและอาจจะมี sensitive data ไม่ให้หลุดเข้าไปใน Docker image

ไฟล์หลักๆที่เราต้องป้องกันคือ README.md, .git และ .env ส่วนที่เหลืออื่นๆเป็นการลดเพื่อไม่ copy ส่วนไม่จำเป็นเข้าไปใน image

Step ข้างต้นนี้เพียงพอที่จะ build container image ได้แล้ว แต่หากเราต้องการ build และ test ในเครื่องตนเองก็สามารถทำได้โดยเขียน docker-compose.yml เพิ่ม

docker-compose.yml

services:
  myapp:
    image: myapp:localtest
    build:
      context: .
    ports:
      - 3000:3000

จากตัวอย่างจะเป็นการบอก docker-compose ให้สร้าง image ชื่อ myapp:localtest โดย build image จาก Dockerfile ที่อยู่ใน current context directory และ bind port 3000 ของ host เข้ากับ port 3000 ของ container

หลังจากเตรียมไฟล์เรียบร้อย สามารถสั่ง docker-compose เพื่อ start ขึ้นมาทำงานได้ ดังนี้

docker-compose up -d

หากต้องการ force rebuild image อีกครั้ง เวลามีการเปลี่ยน version ให้ใช้คำสั่งดังนี้

docker-compose up -d --build

การทำ Multi-stage build

เป็นเทคนิคใน Dockerfile ที่ช่วยลดขนาดของ Docker image และทำให้การ build มีประสิทธิภาพมากขึ้น โดยการแยกขั้นตอนการ build ออกเป็นหลาย ๆ ขั้น (stages) เพื่อที่จะนำเฉพาะผลลัพธ์ที่ต้องการมาใช้งานใน final image โดยไม่ต้องเก็บทุกอย่างที่ใช้ในการ build เอาไว้ใน image สุดท้าย

ประโยชน์อย่างนึงที่เห็นได้ชัดเจน เช่นการแยก stage แรกซึ่งจะติด build tools และ application code อยู่ใน layer ออกจากอีก stage ซึ่งจะ copy มาแค่ artifact ที่ compile เสร็จแล้วมาแทน ทำให้ container image light-weight และ secure ขึ้น

ตัวอย่าง Dockerfile

FROM golang:1.23
WORKDIR /src
COPY <<EOF ./main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]

code ในส่วน EOF คือการเขียน content และใส่ลงไฟล์ชื่อ main.go

<<EOF ./main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF

ปกติจะไม่ใส่ใน Dockerfile เพียงแต่ตัวอย่างนี้ทำเพื่อให้สร้างไฟล์อย่างง่าย โดยผู้เรียนไม่ต้องมาเตรียมไฟล์แยกไว้เท่านั้น

ทำการ build image ด้วยคำสั่งดังนี้

docker build -t hello .

จากตัวอย่าง จะเห็น FROM มากกว่า 1 บรรทัดในไฟล์ ซึ่งส่วนนี้เป็นตัวระบุจุดเริ่มของแต่ละ stage และบอกให้ทราบว่าใช้ base image อะไร

stage แรกจะทำการ compile main.go ออกมาเป็นไฟล์ /bin/hello stage ที่สองจะใช้ base จาก scratch image แล้ว copy file จาก stage แรก (stage id 0) มา ผลลัพธ์จากการ build จะเอา stage สุดท้ายมาเป็น container image และติด tag ชื่อ hello

เนื่องจากการใช้ stage id มีโอกาสผิดพลาดได้ง่าย และยากต่อการเข้าใจ ส่วนมากจึงนิยมใช้การกำหนดชื่อ build stage แทน ดังตัวอย่างต่อไปนี้

FROM golang:1.23 AS build
WORKDIR /src
COPY <<EOF /src/main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=build /bin/hello /bin/hello
CMD ["/bin/hello"]

Docker build cache

Docker จะเก็บ cache ของแต่ละขั้นตอนการ build ใน Dockerfile ไว้ และจะมีการตรวจสอบว่ามีการเปลี่ยนแปลงหรือไม่ ดังนั้นหากมีขั้นตอนใด ๆ ที่ไม่เปลี่ยนแปลง Docker จะใช้ผลลัพธ์จาก cache ที่เก็บไว้แทนการรันขั้นตอนนั้นใหม่ ส่งผลให้การ build ทำงานเร็วขึ้น

ดังนั้นการที่เราเข้าใจการทำงานของ Docker build cache ก็จะทำให้เราสามารถ optimize ระยะเวลาที่ต้องใช้ในการ build แต่ละครั้งได้

ตัวอย่าง Dockerfile แรก

FROM node:16
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "app.js"]

ตัวอย่าง Dockerfile สอง

FROM node:16
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "app.js"]

จากตัวอย่าง Dockerfile ที่สองจะ copy ไปแค่ package.json ก่อน และสั่ง npm install แล้วจึง copy code nodejs ที่เหลือ หาก content ใน package.json ไม่มีการเปลี่ยนแปลง ก็จะทำให้สามารถใช้ cache จาก layer COPY package.json . และ RUN npm install ได้ ซึ่งจะช่วยให้ไม่ต้องดาวน์โหลด npm package จาก internet มาในทุกครั้งที่ build และลดระยะเวลาการ build ได้อย่างมาก ในขณะที่ Dockerfile แบบแรก จะไม่สามารถ reuse layer RUN npm install ได้ เนื่องจาก code application เปลี่ยนทุกครั้ง ทำให้ layer COPY . . และ layer ถัดๆไป ไม่สามารถ reuse cache ได้

Docker buildx, BuildKit และ cache backend

BuildKit เป็น backend หรือ engine ที่พัฒนาโดย Docker สำหรับการ build image ซึ่งมีประสิทธิภาพและความยืดหยุ่นที่ดีกว่า engine เดิมของ Docker Build เป้าหมายหลักของ BuildKit คือเพิ่มความเร็วและประสิทธิภาพในการ build image รวมถึงการปรับปรุงการจัดการ caching, multi-stage builds, และการรองรับการ build ข้ามแพลตฟอร์ม (multi-platform builds) ได้ดีขึ้น

Docker Buildx เป็น CLI extension ของ Docker ที่ขยายความสามารถในการ build โดยใช้ BuildKit เป็น backend เพื่อให้ผู้ใช้เข้าถึงฟีเจอร์ขั้นสูงของ BuildKit ได้ง่ายขึ้น โดยเฉพาะการสร้าง multi-platform image, การจัดการ cache ที่ซับซ้อน, และการ build image ข้ามระบบได้ง่ายขึ้น

จากเนื้อหาก่อนหน้า เราทราบกันแล้วว่า Docker สามารถใช้ build cache เพื่อเพิ่มประสิทธิภาพการ build ได้ ซึ่งจากตัวอย่างจะเป็นการ build บน local machine และใช้ local backend cache

ปัญหาคือเมื่อเรานำ Dockerfile เดียวกันไป build บนเครื่องอื่นที่ไม่เคย build image นี้มาก่อน เราจะไม่สามารถ reuse cache ได้ เพราะโดย default จะใช้ local backend cache BuildKit และ Buildx จึงจะเข้ามาแก้ไขในส่วนนี้ โดยทำให้เราสามารถไปใช้ external backend cache ได้

Buildx รองรับ cache storage backend ดังต่อไปนี้

  1. inline เก็บ cache ไว้ที่เดียวกับ output image

  2. registry เก็บ cache ไว้เป็นอีก image หนึ่งและ push ขึ้น registry เป็นคนละที่กับ output image

  3. local เขียน cache ใน local directory บน filesystem ของเครื่องตัวเอง

  4. gha เก็บ cache ที่ github action cache

  5. s3 upload build cache ขึ้น AWS S3

  6. azblob upload build cache ขึ้น Azure Blob Storage

เราสามารถระบุ cache ได้โดยการใส่ --cache-to และ --cache-from

docker buildx build --push -t <registry>/<image> \
  --cache-to type=registry,ref=<registry>/<cache-image>[,parameters...] \
  --cache-from type=registry,ref=<registry>/<cache-image>[,parameters...] .

ตัวอย่างการใช้ใน CI/CD แบบง่ายๆ

docker buildx build --push \
  --cache-from type=registry.example.com/myimage:latest \
  -t registry.example.com/myimage:v2 \
  -t registry.example.com/myimage:latest .

จะทำการ build โดยใช้ cache จาก image tag latest บน remote registry ถ้ามี เพื่อ build เป็น image tag v2 และ tag latest แล้ว push ขึ้นไปที่ remote registry

PreviousDocker (LV.1)NextKubernetes

Last updated 7 months ago

Was this helpful?