perf(container): образ без apt-зависимостей и встроенный healthcheck

- рантайм не тянет 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 клиент
This commit is contained in:
2026-09-19 19:37:06 +03:00
parent b13f9a7479
commit 35cf719d58
3 changed files with 52 additions and 6 deletions
+8 -5
View File
@@ -31,10 +31,12 @@ RUN CGO_ENABLED=1 go build \
# ---------- runtime ---------- # ---------- runtime ----------
FROM debian:bookworm-slim AS runtime FROM debian:bookworm-slim AS runtime
RUN apt-get update \ # Корневой набор сертификатов берём из официального образа: полный пакет
&& apt-get install -y --no-install-recommends ca-certificates tzdata curl \ # ca-certificates с хэш-ссылками занимает десятки мегабайт, а нужен только
&& rm -rf /var/lib/apt/lists/* \ # сам bundle. Рантайм не тянет apt-пакеты — образ укладывается в бюджет §9.1.
&& groupadd --system --gid 10001 glchat \ 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 \ && 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 \ && mkdir -p /app/data/db /app/data/files /app/data/backups /app/web \
&& chown -R glchat:glchat /app && chown -R glchat:glchat /app
@@ -52,7 +54,8 @@ ENV DATA_DIR=/app/data \
EXPOSE 8080 EXPOSE 8080
# healthcheck выполняется самим бинарём: curl в образе не нужен.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ 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"] ENTRYPOINT ["/usr/local/bin/glchat"]
+42
View File
@@ -4,11 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
"glchat/internal/config" "glchat/internal/config"
"glchat/internal/database" "glchat/internal/database"
@@ -23,12 +26,51 @@ var (
) )
func main() { func main() {
// healthcheck используется HEALTHCHECK контейнера: собственный бинарник
// избавляет образ от curl и лишних пакетов.
if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
os.Exit(healthcheck())
}
if err := run(); err != nil { if err := run(); err != nil {
slog.Error("glchat exited with error", slog.Any("error", err)) slog.Error("glchat exited with error", slog.Any("error", err))
os.Exit(1) 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 { func run() error {
if v := os.Getenv("APP_VERSION"); v != "" { if v := os.Getenv("APP_VERSION"); v != "" {
version = v version = v
+2 -1
View File
@@ -12,6 +12,7 @@ x-app-env: &app-env
DOMAIN: "${DOMAIN}" DOMAIN: "${DOMAIN}"
FILES_DOMAIN: "${FILES_DOMAIN}" FILES_DOMAIN: "${FILES_DOMAIN}"
INSTANCE_NAME: "${INSTANCE_NAME}" INSTANCE_NAME: "${INSTANCE_NAME}"
APP_VERSION: "${IMAGE_TAG}"
LISTEN_ADDR: "0.0.0.0:8080" LISTEN_ADDR: "0.0.0.0:8080"
DATA_DIR: "/app/data" DATA_DIR: "/app/data"
WEB_ROOT: "/app/web" WEB_ROOT: "/app/web"
@@ -46,7 +47,7 @@ services:
cpus: "${APP_CPUS}" cpus: "${APP_CPUS}"
memory: ${APP_MEMORY_LIMIT} memory: ${APP_MEMORY_LIMIT}
healthcheck: healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"] test: ["CMD", "/usr/local/bin/glchat", "healthcheck"]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
start_period: 20s start_period: 20s