Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +50 XP

Развертывание: бинарник, Docker, graceful shutdown и reverse proxy

деплой echo — урок, где процесс, наученный в [двадцать третьем](/courses/echo/lessons/23-monitoring/) отвечать метриками, пробами и slog-логами, наконец покидает ваш ноутбук и приезжает на сервер. Приложение из [двадцать первого урока](/courses/echo/lessons/21-struktura/) собрано в слои, прод-режим из [двадцать второго](/courses/echo/lessons/22-performance/) включается переменной APP_ENV, наблюдаемость из двадцать третьего отвечает на вопросы «жив ли, готов ли, как себя чувствует» — сегодня собираем артефакт: статичный бинарник через go build, Docker-образ в две сцены, окружение с секретом JWT, systemd-юнит для хоста и reverse proxy перед каноном на :1323. Деплой echo — это не «как поставить», а три контракта: артефакт, который собирается один раз и везде одинаково; остановка, которая не рвёт in-flight запросы; конфигурация, которая меняется без пересборки. Ключевая новая механика — graceful shutdown: сигнал SIGTERM, e.Shutdown с таймаутом и честная работа с hijacked-соединениями из [восемнадцатого урока](/courses/echo/lessons/18-websocket/). Приступаем — echo docker-история начинается с одного go build.

Содержание урока · 10 разделов

деплой echo: артефакт вместо исходников#

Тема «деплой echo» начинается с признания: пока приложение запускается командой go run на машине разработчика, у него нет ни версии, ни владельца, ни способа откатиться. Деплой — это переход от исходников к артефакту: куску, который собран один раз, помечен версией и запускается одинаково в любой среде. Для Go-приложения артефакт обязан радовать компилятор языка: статичный бинарник, в котором уже ничего не нужно ни доустанавливать, ни интерпретировать. Этим деплой echo отличается от многих экосистем: «сборка» — одна команда, «рантайм» — сам бинарник и ядро, а всё остальное — Docker-образ, systemd-юнит, окружение — упаковка одного и того же файла.

План урока следует за жизненным циклом выката: собрать (go build и флаги, которые делают бинарник пригодным для минимального образа), упаковать (multi-stage Dockerfile с HEALTHCHECK по /healthz из двадцать третьего урока), научить останавливаться (graceful shutdown — самая богатая механика урока), прокормить окружением (секрет и порт без пересборки) и поставить на границу сети (systemd на голом хосте или reverse proxy перед контейнером). Каждая ступень проверяется приборами, которые вы уже подключили: пробы, метрики и JSON-логи — это те глаза, без которых выкат не отличается от надежды.

go build: статичный бинарник и версия внутри#

Команда сборки канона выглядит так: CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(git describe --tags)" -o bin/server ./cmd/server. Разберём по флагам, потому что каждый решает конкретную деплой-задачу. CGO_ENABLED=0 запрещает компилятору связывать C-код: бинарник получается полностью статичным и работает в пустейшем образе (scratch, distroless/static), где нет ни glibc, ни bash; побочный бонус — кросс-компиляция в GOOS=linux GOARCH=amd64 с любой машины. Для канона на :1323, где все зависимости — чистый Go (echo, echo-jwt, echoprometheus, slog), ноль CGO ничего не отнимает. -trimpath вырезает локальные пути из бинарника: в стектрейсах падают чистые пути модуля вместо /home/z/... — меньше шума в логах и меньше информации наружу. -ldflags "-s -w" отбрасывает таблицу символов и DWARF-отладку: минус четверть веса файла; отладчик для прода не нужен — точную сборку всегда можно пересобрать.

Самое важное — -X main.version=..., которое вшивает значение в переменную пакета main при линковке. В cmd/server/main.go объявляется var version = "dev", логгер печатает её на старте, и вместе с echo-баннером, спрятанным HideBanner из двадцать второго урока, в журнале появляется честный «version=v1.4.0». Теперь на вопрос «а что вообще крутится на сервере» отвечает curl или slog-строка, а не опрос коллег. Канон собран; переезжаем в контейнер.

Канон: сборка с версией и проверка

GO
// cmd/server/main.go — точка входа из двадцать первого урока
var version = "dev" // перезаписывается на линковке

func main() {
        // ... сборка приложения из app.New
        slog.Info("starting", "version", version, "addr", addr)
        // ...
}

// сборка и проверка артефакта
CGO_ENABLED=0 go build -trimpath \
  -ldflags "-s -w -X main.version=$(git describe --tags)" \
  -o bin/server ./cmd/server
./bin/server & curl -fsS localhost:1323/healthz

Переменная version объявлена с дефолтом "dev": локальная сборка без флагов не ломается, а пайплайн на линковке подставляет git-тег. Проверка артефакта перед упаковкой — /healthz из двадцать третьего урока: бинарник отвечает 200, значит слои собраны и порт слушается. file bin/server покажет «statically linked» — это сертификат, что CGO_ENABLED=0 сработал и образ может быть сколь угодно пустым.

Docker: две сцены, нонрут-пользователь и HEALTHCHECK#

Docker-образ канона — multi-stage: в первой сцене компилятор, во второй — только артефакт. Builder стартует с golang:1.25-alpine, копирует go.mod/go.sum и качает модули (этот слой кэшируется и не переживает правки кода), копирует исходники и выполняет ту же команду сборки, что и выше. Финальная сцена — alpine или distroless/static: копирует bin/server, объявляет USER nonroot и EXPOSE 1323. Почему не один stage: компилятор весит гигабайты, рантайм — десятки мегабайт; образ-финал без компилятора быстрее качается, имеет меньше CVE и, главное, честно соответствует артефактной модели — в прод уезжает бинарник, а не исходники. USER nonroot — прямое требование безопасности: процесс, слушающий :1323 (порт выше 1024, привилегий не нужно), не обязан быть root в контейнере.

Специфично echo docker-дополнение — HEALTHCHECK, и здесь заходит наблюдаемость из двадцать третьего урока: докер сам опрашивает /healthz и помечает контейнер unhealthy, если проба перестала отвечать. В alpine есть busybox wget, в distroless нет ничего — выбор базового образа определяет синтаксис проверки. Для выходящего HTTPS-трафика (HTTP-клиент из двадцать второго урока зовёт внешние API) финальному образу нужен пакет ca-certificates: без корневых сертификатов TLS-рукопожатие падает, и это одна из самых частых ошибок первого выката — процесс жив, health-пробы зелёные, а исходящие запросы валятся по x509.

Канон: Dockerfile в две сцены

GO
# стадия сборки: компилятор и модули
FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download          # слой кэшируется до правки зависимостей
COPY . .
RUN CGO_ENABLED=0 go build -trimpath \
    -ldflags "-s -w -X main.version=${VERSION}" \
    -o /out/server ./cmd/server

# финальная сцена: только артефакт
FROM alpine:3.20
RUN adduser -D -u 65532 nonroot && apk add --no-cache ca-certificates
USER nonroot
COPY --from=build /out/server /server
EXPOSE 1323
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s \
  CMD wget -qO- http://127.0.0.1:1323/healthz || exit 1
ENTRYPOINT ["/server"]

Слои идут от реже меняющегося к чаще: зависимости → код → сборка; правка исходника не переигрывает go mod download. USER nonroot с uid 65532 — UID из образа distroless, согласован и с файловыми правками. HEALTHCHECK опрашивает /healthz каждые 15 секунд с таймаутом 3с и start-period на прогрев — параметры зеркалят пробы из двадцать третьего урока, только решает их Docker, а не Kubernetes. ENTRYPOINT в exec-форме — сигнал SIGTERM от docker stop попадает прямо процессу-серверу, без прослойки-shell, которая сигнал не пересылает.

Проверьте себя: что произойдёт при docker stop

Контейнер с каноном запущен, вы выполняете docker stop tasksapp. Что делает Docker?

Graceful shutdown: контракт net/http и e.Shutdown#

Внезапная смерть процесса рвёт все in-flight запросы: пользователь увидел ошибку на честном POST — потому, что вы переезжали с v1.3.0 на v1.4.0. Graceful shutdown — договорённость: при получении сигнала сервер перестаёт принимать новые соединения, дожидается завершения начатых и только потом выходит. Док-комментарий http.Server.Shutdown описывает протокол по шагам: «Shutdown works by first closing all open listeners, then closing all idle connections, and then waiting indefinitely for connections to return to idle and then shut down. If the provided context expires before the shutdown is complete, Shutdown returns the context's error». Словарь: листенеры закрываются — новые запросы не принимаются; idle-соединения закрываются; активные — дожидают своего завершения: платформа ждёт столько, сколько даёт контекст.

Echo оборачивает это одной строкой: e.Shutdown(ctx) — по исходнику echo.go v4.15.0 это «It internally calls http.Server#Shutdown()» на внутреннем Server, а e.Shutdown без аргумента-контекста не существует — контекст задаёт бюджет ожидания. И здесь же ловушка, из-за которой половина graceful-туториалов в интернете не работает: e.Start(":1323") возвращает значение http.ErrServerClosed, когда сервер остановлен через Shutdown — это штатный возврат, а не авария. Если main написан как e.Logger.Fatal(e.Start(":1323")), то штатная остановка упадёт фатальной ошибкой и процесс выйдет с кодом 1 — системы оркестрации считают это сбоем и на ровном месте поднимают тревогу. Правильный main отделяет запуск в горутину и сравнивает ошибку через errors.Is(err, http.ErrServerClosed).

Канон: main.go с graceful shutdown

GO
// cmd/server/main.go — полный производственный main
func main() {
        secret := os.Getenv("JWT_SECRET")
        if secret == "" {
                slog.Error("JWT_SECRET is empty") // падаем до старта листенера
                os.Exit(1)
        }
        prod := os.Getenv("APP_ENV") == "production"
        e := app.New(repository.NewMemory(), []byte(secret), prod)

        go func() {
                if err := e.Start(":1323"); err != nil && !errors.Is(err, http.ErrServerClosed) {
                        e.Logger.Fatal(err) // это НЕ штатный возврат
                }
            }()
        ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
        defer stop()
        <-ctx.Done() // SIGTERM от docker stop или systemd

        shutdownCtx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
        defer cancel()
        if err := e.Shutdown(shutdownCtx); err != nil {
                e.Logger.Error("shutdown timed out", "err", err)
        }
        hub.Close() // закрыть hijacked-соединения WebSocket — ниже в уроке
        slog.Info("stopped", "version", version)
}

Три оси этой сборки. Сигнал: signal.NotifyContext превращает SIGTERM/SIGINT в закрытие канала ctx.Done() — код ждёт не в обработчике сигнала, а в обычном select-е. Бюджет: 8 секунд внутри 10-секундного окна docker stop — запас на закрытие горутин и выгрузку буферов; контекст истёк — Shutdown вернёт ctx.Err(), но соединения не порвёт принудительно, их добьёт выход процесса. Ошибка запуска: errors.Is(err, http.ErrServerClosed) отличает штатную остановку от настоящей аварии — без этого main падает фатально каждый деплой. JWT_SECRET читается в composition root из двадцать первого урока: пустой секрет — отказ до старта, а не 500 на первом запросе.

Проверьте себя: ErrServerClosed

После e.Shutdown(shutdownCtx) что вернёт горутина, ждущая e.Start(":1323"), и почему в каноне стоит проверка errors.Is(err, http.ErrServerClosed)?

Что произойдёт с активным WebSocket при остановке

Прочитайте код и выберите, что он выведет:

// канон: клиент держит WS-соединение из восемнадцатого урока
// (hijacked, в hub-е с register/unregister)
ctx, stop := signal.NotifyContext(...)
<-ctx.Done()
shutdownCtx, _ := context.WithTimeout(context.Background(), 8*time.Second)
err := e.Shutdown(shutdownCtx) // клиент ещё в комнате WS
// что вернёт Shutdown и что станет с WS-соединением?

Упростим main и посмотрим на код выхода

Прочитайте код и выберите, что он выведет:

// вариант main из блога «короче — лучше»:
go func() {
        e.Logger.Fatal(e.Start(":1323"))
}()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer stop()
<-ctx.Done()
_ = e.Shutdown(context.Background())
// docker stop сработал штатно. Каков код выхода процесса?

Окружение: секрет и порт без пересборки#

Один образ обязан работать во всех средах — иначе артефактная модель ломается: собирать образ на каждую конфигурацию — дорога к дрейфу «на тесте работало». Двадцать второй урок уже задал канон: prod := os.Getenv("APP_ENV") == "production". Деплой добавляет два обязательных параметра и правило. Параметры: JWT_SECRET (секрет подписи токенов из пятнадцатого урока — его время жить равно времени жить пода, а не образа) и PORT, если :1323 занят или прокси требует другого. Правило: конфигурация читается только в composition root — cmd/server/main.go; слои из двадцать первого урока os.Getenv не зовут: тест передаёт всё параметрами, прод — теми же параметрами, но значения из окружения.

Способы передать: docker run --env-file prod.env (файл рядом с контейнером, в git не попадает), -e JWT_SECRET=... (виден в ps — только для локалки), в systemd — EnvironmentFile=/etc/tasksapp.env (ниже), в оркестраторах — секреты платформы. Запрет один: секреты не должны попадать в образ ни через RUN, ни через --build-arg — слои образа читаются любым, кто скачал его из реестра. Знакомая ситуация: секрет уехал в docker build --build-arg JWT_SECRET=..., слой с ARG остался в истории — образ в реестре теперь «сделан с секретом» навсегда, откати — не откатишь.

Канон: prod.env рядом с контейнером

GO
# prod.env (в деплой-репозитории, не в git приложения)
APP_ENV=production
JWT_SECRET=<64 байта из генератора, менять при компрометации>
PORT=1323

# запуск
docker run -d --name tasksapp \
  -p 1323:1323 --env-file prod.env \
  registry.example/tasksapp:v1.4.0

# main.go: порт из окружения, дефолт — канон
addr := ":1323"
if p := os.Getenv("PORT"); p != "" {
        addr = ":" + p
}

Образ не содержит ни среды, ни секрета: он один и тот же для теста и прода — меняется только env-файл. PORT остаётся параметром композиции: app.New и маршруты из двадцать первого урока не знают, на каком порту слушают, — это вопрос main. Смена секрета = рестарт контейнера с новым env: активные access-токены подписаны старым ключом и отвалятся — потому секреты ротируют вместе с refresh-токенами, а не между ними.

Проверьте себя: где жить конфигурации

Ревьюер предлагает читать JWT_SECRET внутри service.TaskService через os.Getenv при каждом Create. Почему это ошибка слоёв?

systemd и reverse proxy: два способа жить без оркестратора#

Далеко не каждый сервер имеет Kubernetes, и деплой echo обязан уметь жить по-простому: бинарник плюс systemd. Юнит описывает контракт службы: ExecStart=/usr/local/bin/tasksapp-server (тот самый артефакт), EnvironmentFile=/etc/tasksapp.env (тот же prod.env, что у Docker), User=www-data (nonroot не только в контейнере), Restart=on-failure (падение — поднять, остановку — уважить), After=network-online.target. systemd шлёт службе тот же SIGTERM, что и docker stop: graceful shutdown из середины урока работает один в один — это и есть выгода сигнал-контракта: механизм остановки не зависит от упаковки. systemctl enable --now tasksapp включает автозапуск, journalctl -u tasksapp читает stdout со slog-JSON — тот же поток, что собирает Docker.

Перед сервером почти всегда стоит reverse proxy: nginx терминирует TLS, раздаёт статику и держит внешний порт 443. Конфигурация коротка: location / { proxy_pass http://127.0.0.1:1323; } плюс заголовки X-Forwarded-For/Host и правила для длинных соединений — WebSocket из восемнадцатого урока требует Upgrade/Connection, SSE из девятнадцатого — отключённой буферизации. Сетевые тонкости nginx — отдельная вселенная, и повторять их здесь не будем: полный разбор proxy_pass, X-Forwarded-For и hop-by-hop заголовков сделан в уроке fiber о развертывании — механика proxy одинакова, а вашему echo-серверу важно одно: видеть прокси дружелюбно (таймауты http.Server из двадцать второго урока согласованы с proxy_read_timeout) и не доверять вслепую заголовкам клиента.

Канон: systemd-юнит для задачника

GO
# /etc/systemd/system/tasksapp.service
[Unit]
Description=Tasksapp (Echo)
After=network-online.target
Wants=network-online.target

[Service]
User=www-data
ExecStart=/usr/local/bin/tasksapp-server
EnvironmentFile=/etc/tasksapp.env
Restart=on-failure
RestartSec=2
# шлёт SIGTERM, ждёт TimeoutStopSec, потом SIGKILL
TimeoutStopSec=10

[Install]
WantedBy=multi-user.target

Симметрия с Docker нарочно видимая: тот же артефакт, тот же env-файл, тот же SIGTERM и тот же бюджет (TimeoutStopSec=10 против 10 секунд docker stop). Restart=on-failure поднимает процесс при падении, но не мешает systemctl stop — в отличие от Restart=always, который конфликтует с ручной остановкой. TimeoutStopSec согласован с 8-секундным контекстом Shutdown канона: main успевает закончить ожидание раньше, чем systemd перейдёт к SIGKILL.

Соберите сигнальную связку main.go

Восстановите костяк graceful shutdown: конвертация сигналов, ожидание и мягкая остановка.

ctx, stop := signal.Context(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() <-ctx.Done() shutdownCtx, cancel := context.Timeout(context.Background(), 8*time.Second) defer cancel() if err := e.(shutdownCtx); err != nil { e.Logger.Error("shutdown timed out", "err", err) }

Порядок: пайплайн выката канона

Расставьте шаги выката от тестов до проверки остановки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: язык развертывания

Термин

Graceful shutdown

нажмите, чтобы перевернуть
Определение

Протокол net/http.Shutdown: закрыть листенеры → закрыть idle-соединения → дождаться активных до истечения контекста. Echo: e.Shutdown(ctx) = http.Server#Shutdown().

1 / 6

Канон выкачен#

Канон на :1323 теперь артефакт, а не исходники. Сборка: CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(git describe)" — статичный бинарник с версией внутри; file подтверждает «statically linked». Упаковка: multi-stage Dockerfile с кэшем модулей, nonroot, ca-certificates для исходящих HTTPS и HEALTHCHECK по /healthz из двадцать третьего урока. Остановка: signal.NotifyContext по SIGTERM, 8-секундный контекст внутри 10-секундного бюджета, e.Shutdown дожидается завершения HTTP-запросов, hub.Close() закрывает hijacked WebSocket, а errors.Is(err, http.ErrServerClosed) отличает штатный выход от аварии. Окружение: JWT_SECRET и PORT через --env-file или EnvironmentFile, os.Getenv — только в main. Хост без Docker: systemd-юнит с Restart=on-failure и journalctl; перед контейнером — reverse proxy с правилами для Upgrade и SSE.

Про песочницу честно, как во всём курсе: у курса Echo её нет — код исполняется локально, поэтому runner-ов здесь не будет; соберите бинарник, поднимите образ, остановите контейнер под нагрузкой и почитайте логи: Shutdown должен дождаться in-flight запросов, а метрики — показать ноль обрывов на 5-хх.

Следующий, двадцать пятый урок курса echo — Итоговый проект: API на Echo с JWT, валидацией и тестами. Всё, что вы собрали за курс — маршруты, слои, ошибки, тесты, наблюдаемость, деплой, — склеится в один задачник, который можно отдать в портфолио. Навигация по курсу — на странице курса Echo; полный разбор nginx-прокси и заголовков X-Forwarded-For — в уроке fiber о развертывании; пробы и метрики, проверяющие выкат, — в уроке про мониторинг; таймауты http.Server, согласованные с proxy_read_timeout, — в уроке про производительность; composition root, где читается окружение, — в уроке про структуру проекта; hub WebSocket, закрывающий hijacked-соединения, — в уроке про WebSocket.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Деплой echo — артефактная модель: CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(git describe)" -o bin/server ./cmd/server даёт статичный бинарник с версией внутри (var version = "dev", перезаписывается на линковке, печатается slog-ом на старте).
  • Multi-stage Dockerfile: builder на golang:1.25-alpine с кэшем go mod download, финал — alpine/distroless: USER nonroot, EXPOSE 1323, ca-certificates для исходящих HTTPS, HEALTHCHECK по /healthz из двадцать третьего урока, ENTRYPOINT в exec-форме — SIGTERM доходит процессу.
  • Graceful shutdown: signal.NotifyContext(os.Interrupt, syscall.SIGTERM) → ctx.Done() → e.Shutdown(8s-контекст внутри 10-секундного docker stop). Протокол net/http: закрыть листенеры → idle → дождаться активных; истёк контекст — возвращается ctx.Err(), соединения не рвутся принудительно.
  • e.Start после Shutdown возвращает http.ErrServerClosed — штатный исход: горутина проверяет errors.Is и не падает Fatal-ом, иначе каждый деплой — код выхода 1 и ложная тревога оркестратору.
  • Shutdown не закрывает и не ждёт hijacked-соединения (WebSocket из восемнадцатого урока) — их закрывает hub.Close() после Shutdown; SSE-циклы не hijacked и ждутся — их выход обязан быть привязан к Context.Done()/closeNotify.
  • Окружение: JWT_SECRET и PORT читает только composition root (cmd/server/main.go) — пустой секрет = отказ до старта листенера; docker run --env-file / systemd EnvironmentFile; секреты не попадают в образ (--build-arg остаётся в истории слоёв).
  • Без оркестратора: systemd-юнит — ExecStart артефакта, EnvironmentFile, User=www-data, Restart=on-failure, TimeoutStopSec=10 (согласован с Shutdown), journalctl читает slog-JSON; перед контейнером reverse proxy: proxy_pass на :1323, X-Forwarded-For, отдельные правила для WS-Upgrade и SSE-буферизации — подробный разбор в fiber-курсе.

Домашнее задание

Выкачайте канон и докажите каждый контракт. Первое: соберите артефакт CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(git describe)" -o bin/server ./cmd/server, проверьте file bin/server («statically linked») и строку version в slog-старте; объясните, что изменится в стектрейсе из-за -trimpath. Второе: напишите двухсценный Dockerfile с USER nonroot и HEALTHCHECK по /healthz; приостановите процесс SIGSTOP-ом и посмотрите, как Docker помечает контейнер unhealthy. Третье: доведите main.go до канона урока: signal.NotifyContext, горутина с errors.Is(err, http.ErrServerClosed), 8-секундный Shutdown; сначала заведите версию с e.Logger.Fatal(e.Start(...)) и задокументируйте код выхода 1 при docker stop — затем почините. Четвёртое: под нагрузкой (500 RPS локальным генератором) выполните docker stop и посчитайте в /metrics из двадцать третьего урока, сколько in-flight запросов получили честные ответы — объясните роль 8-секундного бюджета и что изменится при 11-секундном. Пятое: подключите WebSocket-клиент из восемнадцатого урока, держите комнату и выполните остановку; убедитесь по логам, что Shutdown вернулся, не дожидаясь WS, и что hub.Close() закрыл коннект; объясните, почему SSE-цикл обязан выходить по Context.Done(). Шестое: вынесите JWT_SECRET и PORT в prod.env, запустите docker run --env-file, затем docker history и найдите там --build-arg-вариант с секретом — зафиксируйте, почему так нельзя. Седьмое: напишите systemd-юнит по канону, запустите бинарник без Docker, выполните systemctl restart и прочитайте slog-JSON в journalctl -u tasksapp; сравните коды выхода при systemctl stop и при kill -9 — почему Restart=on-failure не поднимает первое.

Личные заметкиПусто
Хранятся только в вашем браузере (Echo · Развертывание: бинарник, Docker, graceful shutdown и reverse proxy)

Вопросы и ответы по уроку

Какой таймаут выбрать для e.Shutdown и как он связан с docker stop?

Правило вложенности: бюджет Shutdown строго меньше времени, которое даёт платформа. Docker stop по умолчанию 10 секунд до SIGKILL, systemd TimeoutStopSec настраивается, в Kubernetes terminationGracePeriodSeconds задаётся явно. Канон берёт 8 секунд: два-три запаса на закрытие горутин, логов и буферов. Истёк контекст — Shutdown вернёт ctx.Err() и выйдет: соединения он не рвёт принудительно, их добьёт выход процесса, но важно, что вы не зависли дольше бюджета. Отдельная ловушка — длинные SSE/WS: либо их циклы выходят по Context.Done(), либо бюджет таймаута будет съедаться каждый рестарт.

Нужен ли Docker вообще, если есть systemd?

Нет: это два честных способа жить на одном хосте, и канон поддерживает оба одним артефактом. systemd проще операционно: файл юнита, journalctl, Restart=on-failure — и никакого реестра образов; изоляции нет, поэтому версии языка и библиотек — вопрос хоста. Docker даёт изоляцию, неизменяемый образ с версией и одинаковый рантайм на любой машине — ценой реестра, слоёв и чуть большей площади атаки. Выбор обычно диктует инфраструктура команды: есть платформа-контейнеры — деплойте образ; есть «сервер под столом» — юнит. Сигнальный контракт SIGTERM и env-файл у обоих совпадают нарочно — перенос между ними меняет три строки.

Как деплой echo отличается от fiber-версии того же урока?

Артефактная часть совпадает почти дословно: go build, multi-stage, nonroot, HEALTHCHECK — Go-компилятор один и тот же. Различия начинаются в деталях фреймворка: fiber построен на fasthttp и имеет свои ListenConfig и TrustProxy-механику, а echo стоит на net/http — поэтому здесь канонный graceful shutdown через e.Shutdown и signal.NotifyContext с честным ErrServerClosed, документированное поведение для hijacked-соединений и стандартные http.Server-таймауты из двадцать второго урока. Reverse proxy и nginx-заголовки — общие для обоих: транспорт один и тот же, и подробный разбор мы оставили в fiber-уроке о развертывании, чтобы не пересказывать его дважды.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

GORM25 уроков~14 чпрактика25 уроков уже доступны

GORM: ORM-библиотека для Go

Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.

  • Подключать БД и настраивать пул соединений
  • Описывать модели и связи: 1:1, 1:N, M:N
  • Писать CRUD и сложные выборки chainable-стилем
  • Контролировать N+1 через Preload