From 35cf719d58d0d7f2efd3dd8235e3b175a549a740 Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 19 Sep 2026 19:37:06 +0300 Subject: [PATCH] =?UTF-8?q?perf(container):=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B7=20=D0=B1=D0=B5=D0=B7=20apt-=D0=B7=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B5=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D0=B2=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?healthcheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - рантайм не тянет ca-certificates/tzdata/curl: bundle сертификатов копируется из официального alpine-образа, healthcheck выполняет сам бинарник - cmd/glchat: подкоманда healthcheck (HTTP-запрос к /healthz с таймаутом) - compose: APP_VERSION передаётся в environment (версия видна в meta без пересборки) - бюджет §9.1: базовый слой debian-slim + 9.2 MB бинарник + 356 KB клиент --- Dockerfile | 13 ++++++++----- cmd/glchat/main.go | 42 +++++++++++++++++++++++++++++++++++++++++ deploy/compose.yaml.tpl | 3 ++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index f08ded8..a3ce0c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,10 +31,12 @@ RUN CGO_ENABLED=1 go build \ # ---------- runtime ---------- FROM debian:bookworm-slim AS runtime -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates tzdata curl \ - && rm -rf /var/lib/apt/lists/* \ - && groupadd --system --gid 10001 glchat \ +# Корневой набор сертификатов берём из официального образа: полный пакет +# ca-certificates с хэш-ссылками занимает десятки мегабайт, а нужен только +# сам bundle. Рантайм не тянет apt-пакеты — образ укладывается в бюджет §9.1. +COPY --from=alpine:3.22 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +RUN groupadd --system --gid 10001 glchat \ && useradd --system --uid 10001 --gid glchat --home-dir /app --shell /usr/sbin/nologin glchat \ && mkdir -p /app/data/db /app/data/files /app/data/backups /app/web \ && chown -R glchat:glchat /app @@ -52,7 +54,8 @@ ENV DATA_DIR=/app/data \ EXPOSE 8080 +# healthcheck выполняется самим бинарём: curl в образе не нужен. HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ - CMD curl -fsS http://127.0.0.1:8080/healthz || exit 1 + CMD ["/usr/local/bin/glchat", "healthcheck"] ENTRYPOINT ["/usr/local/bin/glchat"] diff --git a/cmd/glchat/main.go b/cmd/glchat/main.go index 1f8208e..9b5b026 100644 --- a/cmd/glchat/main.go +++ b/cmd/glchat/main.go @@ -4,11 +4,14 @@ import ( "context" "errors" "fmt" + "io" "log/slog" + "net" "net/http" "os" "os/signal" "syscall" + "time" "glchat/internal/config" "glchat/internal/database" @@ -23,12 +26,51 @@ var ( ) func main() { + // healthcheck используется HEALTHCHECK контейнера: собственный бинарник + // избавляет образ от curl и лишних пакетов. + if len(os.Args) > 1 && os.Args[1] == "healthcheck" { + os.Exit(healthcheck()) + } if err := run(); err != nil { slog.Error("glchat exited with error", slog.Any("error", err)) os.Exit(1) } } +func healthcheck() int { + addr := config.DefaultListenAddr + if cfg, err := config.Load(); err == nil { + addr = cfg.ListenAddr + } + host, port, err := net.SplitHostPort(addr) + if err != nil { + host, port = "127.0.0.1", "8080" + } + if host == "" || host == "0.0.0.0" || host == "::" { + host = "127.0.0.1" + } + url := "http://" + net.JoinHostPort(host, port) + "/healthz" + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return 1 + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 1 + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + return 1 + } + return 0 +} + func run() error { if v := os.Getenv("APP_VERSION"); v != "" { version = v diff --git a/deploy/compose.yaml.tpl b/deploy/compose.yaml.tpl index 9e4274e..9fa0ed6 100644 --- a/deploy/compose.yaml.tpl +++ b/deploy/compose.yaml.tpl @@ -12,6 +12,7 @@ x-app-env: &app-env DOMAIN: "${DOMAIN}" FILES_DOMAIN: "${FILES_DOMAIN}" INSTANCE_NAME: "${INSTANCE_NAME}" + APP_VERSION: "${IMAGE_TAG}" LISTEN_ADDR: "0.0.0.0:8080" DATA_DIR: "/app/data" WEB_ROOT: "/app/web" @@ -46,7 +47,7 @@ services: cpus: "${APP_CPUS}" memory: ${APP_MEMORY_LIMIT} healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"] + test: ["CMD", "/usr/local/bin/glchat", "healthcheck"] interval: 30s timeout: 5s start_period: 20s