Skip to content
CodeSook
CodeSook

Automate deploy using Watchtower in Docker

#Docker#Watchtower
CodeSookPublish: 5th November 2024
cover

มาลองทำความรู้จักกับ Watchtower กันในโพสต์นี้ครับ

What is Watchtower?

Watchtower เป็น open source ตัวนึง ที่จะคอยดู docker image ให้เราว่ามี image ตัวใหม่ pushed มาแล้วหรือยัง
ดูเพิ่มเติมที่ github

การทำงานของ Watchtower จะเป็นแบบ pull-based นะ

เราตั้งเวลาไว้ว่าจะให้ตรวจสอบเป็นช่วงๆห่างกันกี่วินาที ถ้าหาก Watchtower เจอว่ามี docker image ตัวใหม่ มันก็จะ pull มาให้ แล้วก็จะ stop การทำงานตัวเก่า แล้ว start container ด้วย image ตัวใหม่ให้เราเอง
หรือจะตั้งให้ Watchtower ไปตรวจสอบเป็นช่วงเวลาได้ด้วย ด้วยการใช้ Cron expression

นอกจากนี้เรายังสามารถ set ให้ Watchtower ทำ Zero downtime ให้เราได้ด้วย (ไม่ได้ zero downtime ซะทีเดียวนะ แต่ก็ถือว่า nearly zero downtime ได้แหละ) คือหลังจาก pull image มาแล้ว Watchtower จะไป start container ตัวใหม่ก่อน ถ้าสำเร็จทำงานได้มันจะไป stop container ตัวเก่าทีหลัง

เอาละเริ่มน่าสนใจแล้วใช่มะ

ยังก่อน

ก่อนจะไปเริ่ม

คำเตือน

อยากให้คำเตือนไว้ก่อน คือใน docs เขียนไว้ชัดเลยว่า ไม่ควรใช้ Watchtower กับ production server นะ ให้ใช้บน Dev server, UAT server เท่านั้น ถ้าอยากใช้บนเครื่อง prod แนะนำให้ไปใช้ Kubernetes จะดีกว่า ซึ่ง K8S distro ก็มีหลายตัว ซึ่งเค้าก็แนะนำ MicroK8s กับ K3S

แต่ๆ ผมก็เห็นว่า Dream of code ใช้บน production server เลยด้วย

ส่วนตัวผมนั้นก็ไม่ได้ใช้บนเครื่อง prod แต่จะใช้ที่ environment อื่นๆ แต่งานที่ผมทำเดี๋ยวนี้ก็ไม่ค่อยได้ deploy ด้วย docker แล้ว ส่วนใหญ่จะไป deploy บน K8S หมดแล้ว

แต่ก็มาลองเล่นกันดู

สร้าง API ง่ายๆกันก่อน

สร้าง api ด้วยอะไรก็ได้เอาง่ายๆเลย เพราะมันไม่ใช่ topic ของเรา ในที่นี้ผมจะใช้ Go Echo นะ เพราะมัน build ไว แล้ว image ก็เล็กตอน push จะได้ไม่รอนาน

Terminal window
go mod init api
Terminal window
go get github.com/labstack/echo/v4

สร้าง file

  1. main.go
  2. Dockerfile
  3. compose.yaml
main.go
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World")
})
e.Logger.Fatal(e.Start(":1323"))
}

สร้าง Dockerfile

Dockerfile
# Build stage
FROM golang:alpine AS builder
# Set working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download all dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the application with optimizations
# Statically linked binary with no external dependencies
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-a -installsuffix cgo \
-ldflags='-w -s -extldflags "-static"' \
-o main .
# Final stage
FROM scratch
# Copy the binary from builder
COPY --from=builder /app/main /main
EXPOSE 1323
# Command to run the application
ENTRYPOINT ["/main"]

ตัว Dockerfile ไม่ได้มีอะไรซับซ้อนเลย

ให้ build แล้ว push ขึ้น dockerhub ได้เลย ในที่นี้ผม push ขึ้นไปที่ repo ของผมเองชื่อว่า hadesgod/api:prod

Terminal window
docker build -t hadesgod/api:prod .
docker push hadesgod/api:prod

สร้าง compose file

compose.yaml
services:
watchtower:
image: containrrr/watchtower
environment:
- WATCHTOWER_LABEL_ENABLE=true # which service need to allow watchtower to watch new image that service should put the label
- WATCHTOWER_POLL_INTERVAL=30 # check new image every 30 seconds
- WATCHTOWER_ROLLING_RESTART=true # restart the one was changed image detected and got zero downtime deployment as well
volumes:
- /var/run/docker.sock:/var/run/docker.sock
api:
labels:
- "com.centurylinklabs.watchtower.enable=true"
image: hadesgod/api:prod
ports:
- 3333:1323

ส่วนของ docker compose file จะอธิบายให้ดีหน่อย แบบนี้

ที่ watchtower service เราจะใส่ Environment หลายตัว ค่อยๆดูไปทีละตัวตามนี้นะ

  • WATCHTOWER_LABEL_ENABLE=true

    อันนี้ตั้งให้ watchtower ไม่ต้องไปเช็คว่ามี image ใหม่หรือเปล่า ไม่ว่าจะ service ไหนก็แล้วแต่
    ถ้าเราไม่ได้ระบุแบบเจาะจงที่ image ตัวไหนก็ไม่ต้องไปดู image ตัวใหม่ ที่ต้องทำแบบนี้เพราะว่าโดยปกติ Watchtower จะไปเช็คทุก services เลย แต่เราอยากกำหนดเองว่าอยากให้ Watchtower คอยเช็ค image ของ service ที่เราต้องการเท่านั้น และถ้าเรามี sevice อื่นๆที่เราไม่ได้เขียนเองเช่น Redis เราก็ไม่จำเป็นต้องไปคอยเช็ค image ตัวใหม่ใช่มะ ไม่งั้นเจอ tag ที่เป็น version ใหม่เจอ breaking chaged ไป app เราก็แตกพอดี เอาแค่ app ที่เราเขียนก็พอนะ

    ซึ่งวิธีการบอกให้ watchtower รู้เนี่ย ก็คือต้องใส่ label เข้าไป ดูตัวอย่างที่ api service ก็ได้ จะเห็นว่าใส่ label แบบนี้

labels:
- "com.centurylinklabs.watchtower.enable=true"

ด้วย env ตัวนี้ทำให้ watchtower จะไปเช็ค image ของ service นี้ละว่ามี update มาบ้างหรือยัง ก็ทำตามกระบวนการต่อไป

  • WATCHTOWER_POLL_INTERVAL=30 อันนี้ก็สั่งให้ watchtower ไปดูว่ามี image ตัวใหม่มาแล้วหรือยังในทุกๆ 30 วินาที

  • WATCHTOWER_ROLLING_RESTART=true อันนี้ watchtower จะไปหยุดการทำงานอันเก่า start อันใหม่ เราจะได้ near zero downtime จากตรงนี้แหละ

จากนั้นก็สั่ง

Terminal window
docker compose up -d

ลองแก้ code ของเรา

เราจะแก้ code
แล้ว build image ใหม่
แล้ว push ขึ้นไปที่ docker hub ด้วย tag prod เหมือนเดิม

main.go
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "--> Hello, Watchtower <--") // แก้ตรงนี้
})
e.Logger.Fatal(e.Start(":1323"))
}

แล้วก็ build & push ได้เลย

Terminal window
docker build -t hadesgod/api:prod .
docker push hadesgod/api:prod

รอประมาณ 30 วินาที Watchtower น่าจะทำการสร้าง container ใหม่มาให้เราละ

ย้ำอีกทีว่ามีความเสี่ยง take your own risk นะครับ