---
title: "Развертывание Connect-сервисов"
description: "Деплой Connect-сервиса: мультистейдж Docker на distroless, пробы и graceful shutdown, ingress с TLS и HTTP/2, выравнивание таймаутов, лимитов и сжатия."
course: "Connect: современный RPC от Buf"
url: "https://golangmaster.ru/courses/connect/lessons/22-deploy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Развертывание Connect-сервисов

*Курс «Connect: современный RPC от Buf», урок 22 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/connect/lessons/22-deploy/*

деплой connect — последний рубеж пути: контейнер, оркестратор, ingress с TLS и HTTP/2 на границе. Соберём мультистейдж-образ на distroless, настроим окружение и лимиты рантайма, подключим readiness и graceful shutdown к жизненному циклу пода, выставим сервис через ingress с HTTP/2 и выровняем таймауты, лимиты тела и сжатие по всей цепочке клиент, ingress, хендлер.

Сервис, который отлично работает на localhost, в проде встречает оркестратор с пробами, ingress с TLS, лимиты тела, таймауты прокси и SIGTERM среди ночи. Деплой connect собирает всё, что ты построил за курс: один mux с healthz и метриками из девятнадцатого урока, graceful shutdown, h2c для gRPC-жанров, лимиты из четырнадцатого, метрики из семнадцатого. Задача урока — выровнять всю цепочку: клиент, ingress, под, хендлер — так, чтобы ни одно звено не резало то, что нужно соседнему. Если собрать частые поисковые запросы — connect docker, grpc docker, connect деплой, — получится ровно чек-лист этого урока, по которому деплой connect проходит без сюрпризов: образ, окружение, пробы, остановка, TLS, HTTP/2, таймауты, лимиты. План: мультистейдж Dockerfile и почему CGO_ENABLED=0, окружение 12-factor и лимиты рантайма, пробы и жизненный цикл пода, ingress с TLS на границе и HTTP/2 внутрь, и финальное выравнивание таймаутов, лимитов тела и сжатия по метрикам p99.

### Мультистейдж-образ: build-слой против runtime-слоя

Docker-образ для Go-сервиса собирается двумя этажами. Первый — builder на полном golang-образе: go mod download выносится в отдельный слой после копирования только go.mod и go.sum — пока зависимости не менялись, слой берётся из кэша и никакие пакеты не перекачиваются; затем сборка с CGO_ENABLED=0: чисто Go-бинарник без линковки с libc работает на любом минимальном базовом образе, вплоть до scratch. Второй этаж — runtime: distroless/static с nonroot-пользователем или alpine, куда копируется только бинарник. Итог — десятки мегабайт вместо гигабайта тулчейна, меньше поверхность атаки, быстрее pull на нодах. connect-специфика тут минимальна, но одна привычка важна: кодогенерация buf generate выполняется в builder-слое или раньше в CI — в runtime-образе buf не нужен, сгенерированные файлы уже вкомпилированы. EXPOSE документирует порт 8080, ENTRYPOINT запускает бинарник; HEALTHCHECK в Dockerfile полезен для docker-compose и standalone Docker, но Kubernetes его игнорирует — там правят бал пробы манифеста, о них ниже.

### Dockerfile: два этажа

```go
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/notes ./cmd/notes

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/notes /notes
EXPOSE 8080
USER nonroot
ENTRYPOINT ["/notes"]
```

Кэш по слоям: пока go.mod не менялся, download берётся из кэша и сборка начинается с копирования кода. nonroot в distroless — процесс не root; порт 8080 больше 1024, привилегии не нужны. Флаг -trimpath убирает локальные пути из бинарника. В runtime-образе нет ни Go, ни buf, ни shell — только бинарник и минимум системных слоёв.

**Вопрос:** Зачем CGO_ENABLED=0 при сборке образа Connect-сервиса?

- Ускоряет компиляцию в два раза
- ✅ Даёт статический бинарник без libc: работает на distroless и scratch, без сюрпризов базового образа
- Включает HTTP/2 в рантайме
- Отключает сжатие gzip в хендлере

Чисто Go-линковка делает бинарник самодостаточным: ему не нужна ни glibc, ни musl в runtime-слое. Для чистого connect-стека — net/http, protobuf, connect — cgo не нужен вовсе; он понадобится только если в цепочке есть драйверы с C-зависимостями, и тогда образ собирают на alpine-базе с совместимой libc.

### Окружение: 12-factor и лимиты рантайма

Конфигурация живёт в окружении, не в образе: один и тот же артефакт катится через dev, staging и prod, различаясь переменными — PORT, адрес базы, уровень логов, базовый URL из пятого урока. Секреты приходят из хранилища оркестратора в переменные или смонтированные файлы, никогда — слоями образа. Два рантайм-лимитера, которые в контейнерах кусаются регулярно. GOMAXPROCS по умолчанию равен числу ядер ноды, а не CPU-лимиту пода: при лимите в полтора ядра планировщик Go гоняет десятки потоков, конкуренция и троттлинг пляшут, и p99 проседает без видимых причин — лечит uber-go/automaxprocs или явная установка переменной. GOMEMLIMIT ставь чуть ниже memory-лимита пода, чтобы GC начал убираться до OOMKill, а не после. Для connect-сервиса это не теория: метрики из семнадцатого урока — первый индикатор, что CPU-троттлинг или GC-давление начались, и связка го-профилей с платформенными лимитами экономит часы дебага. Логи пиши в stdout структурно — сборщик платформы заберёт их сам, файлы и ротации в контейнере не нужны. Флаги командной строки оставь для редких переопределений, env — для всего остального: так пайплайн деплоя остаётся одинаковым для всех сервисов платформы независимо от языка.

### Пробы и жизненный цикл: связка с graceful shutdown

Пробы из девятнадцатого урока становятся контрактом с оркестратором. Liveness на /healthz отвечает «процесс жив» без походов к зависимостям — ошибка здесь перезапускает под, и зависшая база не должна устраивать рестарт-шторм. Readiness на /readyz проверяет готовность принимать трафик — лёгкий пинг базы с таймаутом в полсекунды, потому что readiness дёргают часто. StartupProbe даёт долгую инициализацию, не убивая под до первого успеха. Жизненный цикл при выкатке: под исключается из эндпоинтов сервиса — readiness снимается, — затем получает SIGTERM; graceful shutdown из девятнадцатого урока перестаёт принимать новые вызовы и ждёт активные до grace-дедлайна. Правило выравнивания: terminationGracePeriodSeconds манифеста должен быть больше Shutdown-дедлайна с запасом на закрытие зависимостей, иначе оркестратор убьёт под посреди доезда unary-вызовов. Долгие bidi-стримы сами не закончатся — их гасит дедлайн Shutdown, а клиент обязан переживать обрыв ретраем или переподключением: деплой и клиентская вежливость из пятого урока — одна система, а не два отдельных дела. StartupProbe особенно полезен, если сервис на старте прогревает кэши или ждёт миграций: без него liveness убьёт под раньше, чем тот успеет встать.

### Пробы и grace в манифесте

```go
containers:
  - name: notes
    ports:
      - containerPort: 8080
    readinessProbe:
      httpGet: { path: /readyz, port: 8080 }
      periodSeconds: 5
    livenessProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
    lifecycle:
      preStop:
        exec: { command: ["sleep", "5"] }
terminationGracePeriodSeconds: 35
```

preStop с паузой 5 секунд — время, чтобы исключение пода из эндпоинтов разъехалось по балансировщикам до SIGTERM; 35 секунд grace — больше Shutdown-дедлайна в 25-30 из девятнадцатого урока и больше p99 unary из метрик. Согласование трёх чисел — дедлайн Shutdown, grace пода, таймаут ingress — главная арифметика этого урока.

### Что случится с вызовами при SIGTERM?

```go
<-ctx.Done() // SIGTERM от оркестратора
shutdownCtx, _ := context.WithTimeout(context.Background(), 30*time.Second)
srv.Shutdown(shutdownCtx)
// живы: пачка unary-вызовов и один bidi-стрим из восьмого урока
// terminationGracePeriodSeconds: 35
```

**Что выведет программа?**

- Всё рвётся немедленно: SIGTERM убивает соединения
- ✅ Unary доезжают в пределах 30 секунд, стрим гасится дедлайном; под убивается не раньше 35-й секунды
- Стрим переживает Shutdown и продолжается уже в новом поде
- Unary получают 503, а стрим живёт вечно

Shutdown не рвёт активные соединения: unary успевают доехать, новые не принимаются; долгий стрим не завершится сам — дедлайн Shutdown закрывает его, и клиент переподключается к новой версии. Оркестратор даёт grace 35 секунд — запас поверх 30, чтобы закрытие зависимостей уложилось без отправки SIGKILL.

### Ingress: TLS на границе, HTTP/2 внутрь

Топология TLS из тринадцатого урока в деплое принимает конкретные формы. Вариант по умолчанию: TLS терминируется на ingress — сертификаты выпускает cert-manager или платформенный секрет, — внутрь кластера идёт plaintext, и тогда для gRPC-жанров обязателен h2c из девятнадцатого урока плюс ingress, который проксирует HTTP/2, а не даунгрейдит до HTTP/1.1: в ingress-nginx это аннотация backend-protocol со значением GRPC, в рукописном nginx — grpc_pass вместо proxy_pass. Connect-жанру хватает HTTP/1.1, но держи h2c всегда: жанры не выключены, пока живы старые gRPC-клиенты из двадцатого урока. Второй вариант — end-to-end TLS до пода: http.Server с TLSConfig из тринадцатого урока, ingress в TCP-режиме или mesh; безопаснее для комплаенса, тяжелее в сертификатном хозяйстве. Трейлеры: чистый gRPC-жанр требует HTTP/2-трейлеров через всю цепочку — ещё одна причина не даунгрейдить; Connect-протокол обходится без трейлеров, и это его прод-плюс: промежуточные прокси ломают его реже. Проверка после выката: grpcurl через ingress на gRPC-жанр и curl на Connect-жанр — оба должны ответить на одном хосте. Адрес сервера внутри кластера — обычный DNS сервиса; баланс по подам делает платформа, но долгие gRPC-соединения требуют аккуратности: headless-сервис или клиентские ретраи, иначе все соединения залипнут на первом поде.

**Вопрос:** За ingress, который терминирует TLS и даунгрейдит до HTTP/1.1, живут оба жанра — Connect и gRPC. Что сломается?

- Оба жанра: без TLS HTTP-сервер не работает
- ✅ Только gRPC-жанр: он требует HTTP/2; Connect-протокол живёт на HTTP/1.1
- Ничего: HTTP/2 нужен только браузерам
- Только стримы Connect-жанра

gRPC строится на механике HTTP/2 и на 1.1 не существует; Connect-протокол из четвёртого урока совместим с HTTP/1.1 осознанно. Лечение — h2c на поде и HTTP/2-проксирование в ingress: backend-protocol GRPC в ingress-nginx или grpc_pass в nginx. Проверка после правки — grpcurl через ingress и curl на тот же хост: оба жанра отвечают.

### Таймауты, лимиты и сжатие: выравнивание цепочки

У каждой прослойки свои ручки, и их надо свести в одну арифметику. Таймаут ingress больше p99 unary из метрик семнадцатого урока, иначе прокси режет доезжающие вызовы раньше сервера. Shutdown-дедлайн меньше terminationGracePeriodSeconds — правило из блока о пробах. Лимит тела — два барьера: WithReadMaxBytes из четырнадцатого урока в хендлере и client_max_body_size в ingress; из двух сработает меньший, поэтому сделай их согласованными, чтобы клиент получал честный код resource_exhausted, а не загадочный 413 от прокси. Сжатие — классическая двойная работа: ingress с gzip на RPC-пути пережимает то, что хендлер уже сжал по порогу WithCompressMinBytes; выключи прокси-компрессию для RPC-путей и оставь решение хендлеру — порог считает размер сообщения, а не HTTP-тела. Метрики скрейпятся с /metrics того же mux — отдельный порт не нужен, но проверь, что ingress не публикует /metrics и /healthz наружу: служебные роуты — внутренние, публикуется только RPC-путь. Дедлайны per-call остаются в контексте из пятого урока — платформенные таймауты их не заменяют. Наконец, drainage: балансировщик должен переставать слать новые длинные соединения раньше, чем под умрёт, — иначе SIGTERM придёт соединению, которому ещё жить и жить.

### Чек-лист деплоя Connect-сервиса

| Слой | Настройка | Практика |
| --- | --- | --- |
| Образ | мультистейдж, distroless, nonroot, CGO_ENABLED=0 | десятки МБ, без тулчейна и shell |
| Окружение | PORT, DSN, GOMAXPROCS, GOMEMLIMIT | конфиг в env, секреты из хранилища оркестратора |
| Пробы | liveness /healthz, readiness /readyz, startupProbe | без походов к зависимостям в liveness |
| Остановка | Shutdown 25-30 с меньше terminationGracePeriodSeconds | preStop sleep на разъезд эндпоинтов |
| TLS | терминация на ingress, cert-manager | h2c внутрь, HTTP/2-проксирование для gRPC-жанров |
| Таймауты | timeout ingress больше p99 unary | дедлайны per-call в контексте из пятого урока |
| Лимиты тела | WithReadMaxBytes и client_max_body_size согласованы | коды resource_exhausted вместо 413 от прокси |
| Сжатие | gzip ingress выключен на RPC-пути | WithCompressMinBytes решает в хендлере |
| Наблюдаемость | /metrics на mux, скрейп платформы | наружу публикуется только RPC-путь |

### Собери пробы манифеста

Впиши пути проб: живость — без зависимостей, готовность — с лёгкой проверкой базы.

```go
livenessProbe:
  httpGet:
    path: /___
    port: 8080
readinessProbe:
  httpGet:
    path: /___
    port: 8080
periodSeconds: 5
```

Подсказка: Живость проверяет /healthz и не трогает зависимости, готовность проверяет /readyz с лёгким пингом базы; оба роута уже живут на общем mux из девятнадцатого урока.

Правильные ответы в пропусках: `healthz`, `readyz`

### Путь релиза

Расставь путь релиза от коммита до трафика.

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

1. `CI прогоняет buf lint, buf breaking и buf generate до сборки`
2. `Мультистейдж-сборка: бинарник CGO_ENABLED=0 в distroless`
3. `Образ уезжает в registry с неизменяемым тегом`
4. `Манифест меняет тег, под стартует со startupProbe`
5. `readiness зелёный — под входит в эндпоинты сервиса`
6. `Ingress терминирует TLS и проксирует HTTP/2 внутрь`
7. `Старый под исключается из эндпоинтов, потом получает SIGTERM`
8. `Shutdown доезжает unary, стримы гасятся дедлайном, grace 35 с`

> **Три способа уронить релиз.** Даунгрейд HTTP/1.1 в ingress при живых gRPC-клиентах — жанр падает мгновенно, проверяй backend-protocol и grpc_pass. terminationGracePeriodSeconds меньше Shutdown-дедлайна — оркестратор прибивает поды посреди доезда unary-вызовов. Двойная компрессия ingress плюс хендлер — потраченный CPU на обоих этажах и прибавка латентности; компрессию решает хендлер порогом из четырнадцатого урока.

> **Один порт, один mux — меньше инфраструктуры.** Сервис на одном порту с mux из девятнадцатого урока даёт один набор проб, одно ingress-правило, один скрейп-таргет: платформенная конфигурация линейна. Второй порт с админ-плоскостью оправдан только сетевой политикой, когда RPC-путь обязан быть физически недоступен с прод-балансировщика — иначе не усложняй: разделить роуты можно и на одном порту.

> **Как проходить этот курс.** Код урока — настоящий Dockerfile, манифест и конфиги: собери образ учебного notes-сервиса из третьего урока и прокатись локально docker run с пробами curl-ом. Браузерная песочница сетевых вызовов не исполняет, поэтому исполняемых блоков в курсе нет — интерактивы работают на странице и дают XP, а деплой-цепочку проверяй docker build, docker run и вызовами grpcurl и curl через ingress.

### Карточки: деплой

- **Мультистейдж-образ** — Builder на golang-образе: go mod download кэшируется слоем, CGO_ENABLED=0 даёт статический бинарник; runtime — distroless/static nonroot. buf generate живёт в CI, в runtime-образе тулчейна нет.
- **GOMAXPROCS в контейнере** — По умолчанию равен ядрам ноды, а не CPU-лимиту пода: троттлинг и просевший p99. Лечит automaxprocs или явная установка; GOMEMLIMIT держи чуть ниже memory-лимита пода.
- **Пробы** — liveness /healthz без зависимостей — иначе рестарт-шторм; readiness /readyz с лёгким пингом базы; startupProbe на долгий старт. Readiness связан с выводом пода из ротации перед SIGTERM.
- **Grace против оркестратора** — Shutdown-дедлайн меньше terminationGracePeriodSeconds — запас на закрытие зависимостей; preStop sleep 5 на разъезд эндпоинтов; стримы гасит дедлайн, клиент переживает обрыв ретраем.
- **TLS и HTTP/2 на границе** — TLS терминирует ingress, сертификаты — cert-manager; для gRPC-жанров — h2c на поде и HTTP/2-проксирование: backend-protocol GRPC или grpc_pass. Connect-жанр живёт и на HTTP/1.1.
- **Выравнивание лимитов** — Таймаут ingress больше p99; WithReadMaxBytes и client_max_body_size согласованы — сработает меньший; компрессию решает хендлер порогом, прокси-gzip на RPC-пути выключен.

### Что дальше по курсу

Деплой замкнул дугу от proto до прода: образ, пробы, остановка, граница TLS и выровненные лимиты. Впереди — структура проекта: реальный шаблон с каталогами proto, gen и service в двадцать третьем уроке, типичные ошибки и отладка curl-режимом в двадцать четвёртом, и итоговый проект, соберущий сервис задач целиком. Для повторения: первый сервис и локальный запуск — в [третьем уроке](/courses/connect/lessons/03-pervyj-servis/), TLS и сертификаты — в [тринадцатом](/courses/connect/lessons/13-bezopasnost/), лимиты и сжатие — в [четырнадцатом](/courses/connect/lessons/14-compression/), метрики и p99 — в [семнадцатом](/courses/connect/lessons/17-metriki/), mux, h2c и graceful shutdown — в [девятнадцатом](/courses/connect/lessons/19-net-http/), JSON-поверхность и GET-кэширование — в [двадцать первом](/courses/connect/lessons/21-rest/). Программа всех двадцати пяти уроков — на [странице курса](/courses/connect/).

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

- деплой connect начинается с образа: мультистейдж Docker, CGO_ENABLED=0, distroless nonroot — без тулчейна и shell; buf generate живёт в CI.
- Конфиг в окружении по 12-factor; GOMAXPROCS через automaxprocs, GOMEMLIMIT ниже лимита пода — иначе троттлинг и OOMKill.
- liveness /healthz без зависимостей, readiness /readyz лёгкий, startupProbe для долгого старта; readiness связан с ротацией трафика.
- Shutdown-дедлайн меньше terminationGracePeriodSeconds; preStop sleep разъезжает эндпоинты; стримы гасит дедлайн Shutdown.
- TLS на границе с cert-manager, h2c внутрь, HTTP/2-проксирование ingress для gRPC-жанров; Connect-жанр живёт на HTTP/1.1.
- Таймаут ingress больше p99; лимиты тела согласованы с WithReadMaxBytes; компрессию решает хендлер, прокси-gzip выключен.
- Один порт и mux — один набор проб, одно ingress-правило, один скрейп; админ-порт только по сетевой политике.
- Проверка деплоя: docker run локально с curl-пробами, затем grpcurl и curl через ingress — оба жанра отвечают на одном хосте.

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

Упакуй notes-сервис и прогони его через всю цепочку. Шаг первый: мультистейдж Dockerfile из урока, docker build, docker run с публикацией 8080 — curl на healthz и JSON-вызов на процедуру из двадцать первого урока. Шаг второй: docker-compose с HEALTHCHECK и остановкой — запусти долгий bidi-стрим из восьмого урока, сделай stop контейнера и зафиксируй, что unary доезжают, а стрим гасится дедлайном Shutdown. Шаг третий: манифест с пробами и terminationGracePeriodSeconds 35, больше Shutdown-дедлайна; в kind или minikube сделай выкатку новой версии и понаблюдай за обрывами через метрики — их быть не должно для unary. Шаг четвёртый: ingress с TLS через cert-manager или self-signed и backend-protocol GRPC; grpcurl через ingress на gRPC-жанр и curl на Connect-жанр — оба должны ответить на одном хосте. Шаг пятый: выключи HTTP/2-проксирование и наблюдай падение grpcurl-жанра, верни обратно; затем проверь двойную компрессию, поставив gzip на ingress, и выпиши в заметки чек-лист выравнивания таймаутов и лимитов по p99 из семнадцатого урока.

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

### HEALTHCHECK в Dockerfile или пробы в манифесте?

HEALTHCHECK работает в docker-compose и standalone Docker, Kubernetes его игнорирует — там правят бал liveness, readiness и startup пробы манифеста. Если деплоишь и туда, и туда, держи один /healthz-эндпоинт для обеих систем: разница только в декларации, а не в коде сервиса.

### Нужен ли service mesh для Connect?

Не обязателен: Connect живёт на обычном HTTP, и mesh-сайдкары проксируют его без специфики. Если нужен mTLS между подами, mesh даёт его без правок кода; без mesh — end-to-end TLS из тринадцатого урока на http.Server с TLSConfig.

### Сколько реплик и какой autoscaler?

Начни с двух реплик и HPA по CPU после того, как GOMAXPROCS выровнен, иначе метрика врёт. Соединения gRPC длинные — scale-out новых реплик работает нормально, а scale-in рвёт стримы: держи graceful shutdown из девятнадцатого урока и drainage на балансировщике.
