docker 우아하게 종료시키기 (내가 만든 docker는 왜 10초 뒤에 종료 되는가?)


📖 5줄 요약

  • entrypoint.sh 사용시 꼭 exec <executable> 로 실행 해야합니다.
  • docker container는 PID 1을 가진 프로세스에 SIGINT, SIGTERM을 전달 합니다.
  • exec는 현재 실행중인 process의 PID를 승계해서 <executable> 이 실행되도록 합니다.
  • entrypoint.sh에서 exec를 빼먹으면 entrypoint.shPID 1로 실행되고 <exectuable>은 child process로 별도의 PID를 부여받아 실행됩니다.
  • PID 1이 아닌 프로세스는 docker container 종료시 SIGINT, SIGTERM을 받지 못하고, default timeout인 10초 후 강제 종료 됩니다.

웹어플리케이션을 만들고 dockerize 하기.

아주 간단한 hello world 수준의 web-server를 만들고, SIGINT, SIGTERM 을 받으면 종료 되도록 코드를 작성합니다.

main.go code 보기
package main

import (
        "fmt"
        "net/http"
        "os"
        "os/signal"
        "syscall"
)

func main() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
        })

        handleSigterm()
        http.ListenAndServe(":80", nil)
}

func handleSigterm() {
        c := make(chan os.Signal, 1)
        signal.Notify(c, syscall.SIGINT)
        signal.Notify(c, syscall.SIGTERM)
        go func() {
                sig := <-c
                fmt.Printf("signal(%v) received. exit.", sig)
                os.Exit(0)
        }()
}

바로 코드 실행하고 Ctrl+C(SIGINT) 입력하면 종료도 잘 됩니다.

hello-go-run

Dockerfileentrypoint.sh파일을 만들고 docker image를 만듭니다.

Dockerfile code 보기
FROM golang:1.23-alpine AS builder

COPY main.go /app/main.go
WORKDIR /app

RUN go build -o ./server main.go

FROM alpine:latest AS base

RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/server /app/server
RUN chmod +x /app/server
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT [ "/entrypoint.sh" ]
entrypoint.sh code 보기
#!/bin/sh
/app/server
docker-compose.yml code 보기
services:
  hello-server:
    image: hello-go-server:v0.1.0

docker image를 만들고 실행하고 stop 해봅니다.

docker image를 build 합니다.

docker build -t hello-go-server:v0.1.0 .

실행하고 stop해 봅니다. 왜? stop하는데 10초가 걸릴까요?

docker-stop-no-delay

다시 docker compose up -d로 실행하고, 실행중인 container내의 process list를 확인해 봅니다.

➜  hello-go docker exec -it hello-go-hello-server-1 /bin/sh
/app # ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 {entrypoint.sh} /bin/sh /entrypoint.sh
    7 root      0:00 /app/server
   20 root      0:00 /bin/sh
   26 root      0:00 ps -ef
  • entrypoint.sh가 1번, /app/server가 7번 입니다.
  • docker container가 종료 될떄 os signal(SIGINT)는 PID 1 프로세스로 전달 됩니다.
  • main.go로 작성된 프로그램은 PID 7로 떠있으니 os signal을 못 받습니다.
  • 당연히 프로램은 종료되지 않고 docker compose stop의 default timeout인 10초 후에 강제종료 됩니다.
  • 강제종료이므로 복잡한 프로그램의 경우 문제가 발생 할 수도 있습니다.

우아하게(graceful) 종료시켜 봅시다.

entrypoint.sh 파일에 exec만 추가하면 됩니다.

#!/bin/sh
exec /app/server

다시 build 하고 실행 후 container내의 process list를 확인해 봅니다.

➜  hello-go docker exec -it hello-go-hello-server-1 /bin/sh
/app # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /app/server
   13 root      0:00 /bin/sh
   19 root      0:00 ps
  • entrypoint.sh가 process list에 사라지고, /app/server가 PID 1로 실행중인 것을 확인 할 수 있습니다.
  • 그리고 docker compose stop으로 종료 시켜봅니다.

🎉 종료시 delay가 사라졌습니다.

docker-stop-no-delay

참고 문서

comments powered by Disqus