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:
+8
-5
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user