Skip to main content

รู้จักกับ Docker และ Container

มาทำความรู้จัก Docker กันก่อน

สำหรับใครที่ยังไม่มีพื้นฐานการใช้ Docker เลยขอแนะนำหัวข้อนี้ก่อน Docker (for development)

จาก idea ของ Docker นั้น ทำให้เราสามารถที่จะสร้าง application โดยการทำ image ออกมา และนำ image นั้นไป build "ที่ใดก็ได้" ตราบเท่าที่ support Docker ได้ ซึ่งในปัจจุบัน นอกเหนือจากพวก cloud server ทั่วไป Service serverless หลายๆตัว support การ build ผ่าน docker image เช่นเดียวกัน (รวมถึงตัวที่เราหยิบมาด้วย)

ในหัวข้อนี้เราจะพามาลอง build Docker image golang เพื่อเตรียมสำหรับนำ image ไปใช้งานในการ Deploy กัน

Dockerfile คืออะไร ?

Dockerfile คือ text document ที่ประกอบไปด้วย set ของคำสั่ง (instructions) และ commands ที่ใช้สำหรับการ bulid Docker image โดยเป็นเหมือนพิมพ์เขียวของ การสร้างตัว image นั้นขึ้นมา เพื่อใช้สำหรับการนำไป build บน container ต่อไป โดย Dockerfile นั้นจะทำการ automates process การประกอบร่างตัว image จากคำสั่งที่กำหนดใน Dockerfile ขึ้นมาได้

นี่คือตัวอย่างของ Dockerfile

# Start from the latest golang base image
FROM golang:latest

# Set the Current Working Directory inside the container
WORKDIR /app

# Copy go.mod and go.sum files first; they are less frequently changed than source code, so Docker can cache this layer
COPY go.mod go.sum ./

# Download all dependencies
RUN go mod download

# Copy the source from the current directory to the Working Directory inside the container
COPY . .

# Build the Go app
RUN go build -o main .

# Expose port 8080 to the outside world
EXPOSE 8080

# Command to run the executable
CMD ["./main"]

ไอเดียหลักๆของ Dockerfile และคำสั่งพื้นฐานที่ใช้มีดังนี้

  1. Base Image ทุกๆ Dockerfile จะทำการเริ่มต้น จาก Base Image (Image พื้นฐานที่ใช้เป็นตัว runtime ของ Docker) โดยจะเริ่มจากการใช้คำสั่ง FROM โดยการระบุสิ่งนี้เป็นการระบุ image สำหรับการเริ่มต้น build ของ image นี้ โดยปกติก็จะเป็นพวก OS เช่น ubuntu, alpine หรือพวก pre-built image อย่าง nodejs หรือ golang (เป็นตัว runtime สำหรับการ run application)

  2. คำสั่ง run command คำสั่ง RUN ใช้สำหรับการรันตัว command ภายในตัว image ซึ่งโดยปกติก็จะมี process ตั้งแต่ การลง software ภายในเพิ่ม, การ build หรือ compile code หรือการเปลี่ยน configuration ต่างๆ โดยในการใช้คำสั่ง RUN ทุกรอบก็จะเป็นการสร้าง layer ใหม่ของตัว Docker image ขึ้นมา (ขึ้นเป็น step แต่ละขั้นในการ build docker image)

  3. **คำสั่ง เพิ่ม file ** คำสั่ง ADD และ COPY เป็นคำสั่งใช้สำหรับการเพิ่ม file หรือ directory จาก host file system (เช่นจากเครื่อง local หรือเครื่อง server ที่กำลัง build docker image ตัวนี้อยู่) เข้าสู่ตัว Docker image

  • คำสั่ง COPY จะตรงไปตรงมาคือใช้สำหรับการ copy local file เข้ามา
  • คำสั่ง ADD นั้นจะมี process เพิ่มมาคือการ automatically การบีบอัด file รวมถึงสามารถ fetch file จาก URL เข้ามาได้
  1. คำสั่ง set ENV คำสั่ง ENV ใช้สำหรับการ set ค่า environment variables ภายใน image (พวกค่า environment ที่ใช้ใน application) ซึ่งสามารถกำหนดได้ทั้งผ่าน path และ ใส่ค่าโดยตรงเข้าไป

  2. คำสั่งส่ง Port ออกมา คำสั่ง EXPOSE เป็นการบอกกับ Docker ว่า Container ตัวนี้กำลังมีการ listen port ภายใน network อยู่ โดยจะเป็นการระบุ Port ออกมาว่า กำลัง run Port อะไรออกมาตอนที่ application กำลัง run อยู่ที่ runtime เพื่อใช้สำหรับการ communication ภายใน container ด้วยกัน หรือภายใน host ของเครื่องที่กำลัง run อยู่ เพื่อให้สามารถเรียกใช้งาน Container นั้นผ่าน port ที่ส่งออกมาได้

  3. คำสั่ง default command คำสั่ง ENTRYPOINT และ CMD เป็นการกำหนด default command สำหรับการเริ่มต้น Image ซึ่งโดยปกติจะใช้หลังจากที่ install ทุกอย่างเรียบร้อยแล้ว (เปรียบเหมือนเราลง go package ทุกอย่างครบ ก็จะทำการเริ่ม run application ด้วยคำสั่ง go run เพื่อเริ่มต้นใช้งาน application golang) โดย

  • ENTRYPOINT เป็น set ของ command สำหรับการเริ่มต้น
  • CMD เป็นคำสั่งสำหรับการ default arguments ของ ENTRYPOINT
  • โดยปกติ ถ้า command มี argument จะนิยมใช้ CMD กัน แต่ถ้ามีการใช้งานร่วมกันทั้งคู่ CMD ก็จะนำ argument ไปต่อกับ ENTRYPOINT อีกที
  1. คำสั่ง default directory คำสั่ง WORKDIR เป็นการกำหนด working directory ที่ใช้ร่วมกับคำสั่ง RUN, CMD, ENTRYPOINT, COPY, ADD เพื่อเป็นการกำหนด directory สำหรับการทำงานคำสั่งนี้
  • โดยหากไม่มีการระบุ WORKDIR ภายใน Dockerfile จะถือว่าเป็นการใช้ default WORKDIR ของตัว base image นั้นๆแทน

และสุดท้ายเมื่อทำการเขียน Dockerfile ได้เรียบร้อย ตัว docker เองก็ได้เตรียมคำสั่งสำหรับการ build image ไว้คือ docker build เมื่อไหร่ก็ตามที่เราทำการ build เสร็จเรียบร้อย ตัว image นั้นจะถูกเก็บไว้กับ docker โดยสามารถตรวจสอบผ่านคำสั่ง docker images ออกมาได้

docker-03

สร้าง application ตั้งต้น

เริ่มต้น เราจะมี structure ของ file go application ตามนี้

.
├── go.mod
├── go.sum
├── docker-compose.yml
├── main.go
└── models.go

โดย โจทย์ของตัว application ของเราคือ

  • เราจะทำการสร้าง Web server ขึ้นมาสำหรับจัดการ user (CRUD user) โดย source code นี้จะเป็นการหยิบจากบทของ ตอนที่ 5: GORM และ Fiber มาใช้งาน

โดยเริ่มต้นถ้าเรา development application ทุกอย่างออกมาเรียบร้อยแล้ว

  • มีไฟล์ go.mod และ go.sum ที่เกิดขึ้นจาก go mod init <ชื่อ package>
  • ทำการลง package สำหรับ Fiber / GORM และ env (สำหรับการอ่านค่า config) เอาไว้ให้เรียบร้อย (มีการลง godotenv เนื่องจากเราจะต้องใช้ env สำหรับการเปลี่ยน environment ตอนขึ้นไปบน server)
go get github.com/gofiber/fiber/v2
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/joho/godotenv
  • ทำการเพิ่ม docker-compose.yml สำหรับ run postgres เพื่อทดสอบ database
docker-compose.yml
version: '3.8'

services:
postgres:
image: postgres:latest
container_name: postgres
environment:
POSTGRES_DB: mydatabase
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: unless-stopped

pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
depends_on:
- postgres
restart: unless-stopped

# เพิ่มสำหรับทดสอบการ run app บน network เดียวกัน
myapp:
build: .
ports:
- "8000:8000"
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=myuser
- DB_PASSWORD=mypassword
- DB_NAME=mydatabase
depends_on:
- postgres
restart: unless-stopped

volumes:
postgres_data:
  • และนี่คือตัว source code สำหรับการ build image ครั้งนี้โดย source code นั้นจะเหมือน ตอนที่ 5: GORM และ Fiber แต่จะมีการเพิ่มเรื่องการเรียกใช้งาน env เข้ามา
package main

import (
"fmt"
"github.com/gofiber/fiber/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/joho/godotenv"
"log"
"os"
"strconv"
"time"
)

func main() {
// Load .env file
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file")
}

// Read database configuration from .env file
host := os.Getenv("DB_HOST")
port, _ := strconv.Atoi(os.Getenv("DB_PORT")) // Convert port to int
user := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
dbname := os.Getenv("DB_NAME")

// Configure your PostgreSQL database details here
dsn := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)

// New logger for detailed SQL logging
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
Colorful: true, // Enable color
},
)

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
})

if err != nil {
panic("failed to connect to database")
}

// Migrate the schema
db.AutoMigrate(&Book{})

// Setup Fiber
app := fiber.New()

// CRUD routes
app.Get("/books", func(c *fiber.Ctx) error {
return getBooks(db, c)
})
app.Get("/books/:id", func(c *fiber.Ctx) error {
return getBook(db, c)
})
app.Post("/books", func(c *fiber.Ctx) error {
return createBook(db, c)
})
app.Put("/books/:id", func(c *fiber.Ctx) error {
return updateBook(db, c)
})
app.Delete("/books/:id", func(c *fiber.Ctx) error {
return deleteBook(db, c)
})

// Start server
log.Fatal(app.Listen(":8000"))
}
  • โดย code ในส่วนนี้นั้นจะมีการเรียกใช้งาน env ด้วย จาก config ของ GORM เราจะต้องเพิ่มไฟล์ .env เข้าไป เพื่อเป็นการกำหนด config เริ่มต้นของ database ให้กับ GORM โดยจะทำการเริ่มทดสอบจากการต่อเข้า docker-compose.yml เข้าไปก่อน
DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASSWORD=mypassword
DB_NAME=mydatabase

หลังจากนั้นทำการ run application ขึ้นมา

go run .

เมื่อทำการ run project ขึ้นมาเราก็จะได้ web application ที่ run Fiber และ GORM ออกมาที่สามารถ ยิง request CRUD ของเส้น /users ออกมาได้

ผลลัพธ์จะออกมาได้ประมาณนี้ ที่สามารถส่งข้อมูลเข้าไปได้ docker-01

เริ่มสร้าง Dockerfile

มาเริ่มต้นเขียน Dockerfile สำหรับห่อ Go application เป็น image กัน โดยคำสั่งที่ใช้ก็จะมีตามนี้

Dockerfile
# เริ่มต้นหยิบ golang image มาเป็น base image
FROM golang:latest

# ทำการกำหนด path /app เป็น directory เริ่มต้น
WORKDIR /app

# Copy go.mod และ go.sum ไฟล์เข้ามา
COPY go.mod go.sum ./

# Download dependencies ทั้งหมด
RUN go mod download

# Copy the source จาก directory ปัจจุบัน สู่ working directory ภายใน container
COPY . .

# Build the Go app
RUN go build -o main .

# Expose port 8000 ออกมาภายนอก
EXPOSE 8000

# ทำการ run command ผ่าน binary file ที่ build มาได้
CMD ["./main"]

เมื่อเขียน Dockerfile เสร็จ ทำการ build image ของ go application

docker build -t myapp .

Ref: https://stackoverflow.com/questions/31249112/allow-docker-container-to-connect-to-a-local-host-postgres-database

(สำหรับ Mac) ทำการเปลี่ยน .env เป็น

DB_HOST=docker.for.mac.host.internal
DB_PORT=5432
DB_USER=myuser
DB_PASSWORD=mypassword
DB_NAME=mydatabase

หลังจากนั้นลอง run ด้วยคำสั่ง

docker run -d -p 8000:8000 --env-file .env myapp

ก็จะสามารถ run application ตัวนี้ออกมาที่ docker ได้

docker-02

หรือ ลองนำ image มาต่อเข้ากับ docker-compose โดยการเพิ่มไปใน docker-compose.yml เพื่อทดสอบว่าเชื่อมต่อติดหรือไม่ (ถ้าเป็นวิธีนี้สามารถใช้ .env เหมือนเดิมได้)

version: '3.8'

services:
postgres:
image: postgres:latest
container_name: postgres
environment:
POSTGRES_DB: mydatabase
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: unless-stopped

pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
depends_on:
- postgres
restart: unless-stopped

# เพิ่มสำหรับทดสอบการ run app บน network เดียวกัน
myapp:
build: .
ports:
- "8000:8000"
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=myuser
- DB_PASSWORD=mypassword
- DB_NAME=mydatabase
depends_on:
- postgres
restart: unless-stopped

volumes:
postgres_data:

เมื่อทำการทดสอบ run ด้วย docker-compose up ออกมา ก็สามารถ run application แบบเดียวกันออกมาได้