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

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

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

Урок 23 из 25 ~20 мин +50 XP

Развертывание Fiber-приложения

Прошлый урок закрылся обещанием: структура, построенная из слоёв, деплоится как есть — корень собирает слои, слои не знают про окружение, а весь хаос env читается в одном FromEnv. Исполняем. Тема «деплой fiber» — это путь от команды go build до процесса, к которому клиенты достукиваются через доменное имя: сначала статический бинарник с CGO_ENABLED=0, который не требует на сервере ни Go, ни исходников; затем fiber docker — образ, собранный по multi-stage схеме, где тулчейн остаётся в сцене сборщика, а в доставку уезжает один файл; затем reverse proxy за nginx — с заголовками X-Forwarded-For и WebSocket-апгрейдом из [шестнадцатого урока](/courses/fiber/lessons/16-websocket/); наконец systemd-юнит и переменные окружения, которые [конфиг-урок двадцатый](/courses/fiber/lessons/20-konfiguraciya/) и [структура двадцать второй](/courses/fiber/lessons/22-struktura-proekta/) подготовили заранее. Все факты сверены с исходниками fiber v3.5.0, документацией nginx и базовыми паттернами Dockerfile для Go из официальных руководств Docker.

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

Мост из прошлого урока: структура деплоится как есть#

Начнём почти дословно с того, чем закончили: «двадцать третий урок — развертывание Fiber-приложения: сборка бинарника, Docker, nginx и переменные окружения. Структура, построенная сегодня, деплоится как есть: корень собирает слои, слои не знают про окружение, а весь хаос env читается в одном FromEnv — двадцатый урок подготовил это заранее». Это обещание не риторика, а прямая техническая смета. Композиционный корень cmd/api/main.go уже собирает repo, svc и h тремя конструкторами — значит, точка входа для прод-запуска существует. Конфиг из internal/config уже читает окружение одним вызовом FromEnv — значит, порт и таймауты приезжают из переменных окружения без правок кода. Регистрация маршрутов уже сидит на fiber.Router — значит, та же сборка работает и под systemd, и в контейнере, и за прокси. Отдельного «деплой-режима» в коде не будет: будут команды вокруг того же самого main. За урок мы поднимемся по четырём ступеням: артефакт — go build; контейнер — Dockerfile; фронт — reverse proxy nginx; служба — systemd и env. На каждой ступени канон курса — инстанс на :8080, маршрут /api/tasks, buildApp и страж requireAPIKey — остаётся неизменным, меняется только упаковка.

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

Слово деплой fiber означает превращение репозитория в работающий процесс на чужой машине, и у Go в этом пути есть преимущество, которое стоит проговорить до всяких инструментов: компиляция происходит заранее. Node или Python везут на сервер интерпретатор и исходники, Java — рантайм и байткод, а Go везёт один исполнимый файл, в котором уже лежит и HTTP-сервер, и весь ваш сервисный слой, и компилятор, который всё это склеил. На боевом сервере не нужен ни тулчейн Go, ни каталог с исходниками, ни менеджер пакетов: нужен файл, право его запускать и свободный порт. Отсюда весь урок — это управление двумя сущностями: артефактом (что мы собираем) и окружением (где и как он живёт). Запрос «деплой fiber» вбивают в поиск обычно тогда, когда localhost кончился: приложение держит состояние в памяти, слушает :8080, и нужно, чтобы оно пережило выход из сессии ssh, поднялось после перезагрузки и отвечало на доменное имя по TLS. Каждое из этих «нужно» закрывается отдельной ступенью урока, и важно, что порядок ступеней неслучаен: контейнер упаковывает бинарник, nginx прикрывает контейнер, systemd следит за процессом — на каждом уровне тот же принцип слоёв, что и в двадцать втором уроке, только не внутри программы, а вокруг неё.

Ступень первая: go build и статический бинарник

GO
# Сборка статического бинарника под Linux — с любой машины, где есть Go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o bin/api ./cmd/api

# Проверка, что бинарник действительно статический
file bin/api     # ELF 64-bit ... statically linked
ldd bin/api      # not a dynamic executable

Одна команда — и весь проект из прошлого урока превращается в файл bin/api. Разберём флаги подробнее, потому что вокруг них половина ночных инцидентов первого деплоя. CGO_ENABLED=0 выключает cgo — мост из Go в мир библиотек C, которым сборщик по умолчанию пользуется, если какой-то пакет его тянет; без флага бинарник получается динамическим и требует ровно ту libc, с которой собран: классический сценарий поломки — собираете на своей Ubuntu (glibc) и кладёте бинарник в образ alpine (musl) или в scratch (нет вообще ничего), и запуск падает до первой строки main с сообщением вида no such file or directory, хотя файл лежит и права стоят, — это не найден динамический загрузчик. CGO_ENABLED=0 закрывает вопрос целиком: чисто Go-программа не ищет загрузчик, потому что ей нечего загружать, она не линкуется ни с какой системной библиотекой C и самодостаточна вплоть до запуска в пустом образе scratch. Сеть не страдает: пакет net без cgo переключается на собственный чисто-Go резолвер DNS, и для HTTP-сервера это несущественно. Оговорка честности: если проект требует cgo-пакета — например, некоторые драйверы SQLite, — флаг трогать нельзя, и тогда либо собирают в образе с той же libc, что на бою, либо ищут pure-Go замену; канон нашего курса — память под мьютексом, так что CGO_ENABLED=0 ему ничего не ломает. GOOS=linux и GOARCH=amd64 — кросс-компиляция: собирать можно на macOS или Windows, а артефакт выйдет под целевую систему; для ARM-серверов меняется только GOARCH=arm64. Флаг -trimpath вычищает локальные пути из исполняемого файла, а -ldflags "-s -w" отрезает отладочные таблицы — бинарник худеет на десяток процентов, что для образа приятно. Проверка честная: file говорит statically linked, ldd отвечает not a dynamic executable — значит, зависимостей от окружения нет и уехать такой файл может куда угодно, от Raspberry Pi до distroless; именно статичность делает финальную сцену Dockerfile такой тонкой, и это следующий блок.

Зачем Docker и что такое fiber docker: multi-stage#

Бинарник — артефакт, но «не работает» чаще всего означает «не работает среда»: другая версия glibc, отсутствующие сертификаты, чужие права на каталог, занятый порт. Контейнер лечит это радикально: образ упаковывает процесс вместе со всем, что ему нужно, и запускается одинаково на ноутбуке и в бою. По запросу fiber docker вы найдёте сотни Dockerfile, и почти все они собираются по одной схеме — multi-stage, многоступенчатая сборка, официальный паттерн руководства Docker для Go. Идея зеркалит канон двадцать второго урока: слои, которые нужны только для сборки, живут отдельно от слоёв, которые уезжают в прод. Первая сцена — builder: полноценный образ golang с тулчейном, где скачиваются зависимости и выполняется go build. Вторая сцена — доставка: пустой или почти пустой образ, куда COPY --from=build переносит единственный файл — готовый бинарник. В финальном образе нет ни Go, ни исходников, ни менеджера пакетов, ни шелла — только ваш API-сервер размером в мегабайты против гигабайтного builder-образа. Это одновременно и безопасность: в прод-контейнере просто нечем ковыряться, потому что инструментария там нет.

Канон: multi-stage Dockerfile для Fiber

GO
# Dockerfile — две сцены: сборка и доставка
# Сцена 1: builder — здесь живёт весь тулчейн
FROM golang:1.25-alpine AS build
WORKDIR /src

# Сначала только манифесты зависимостей: этот слой кэшируется,
# пока go.mod и go.sum не изменились
COPY go.mod go.sum ./
RUN go mod download

# Теперь исходники: правки кода не дёргают скачивание модулей
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/api ./cmd/api

# Сцена 2: доставка — ни Go, ни исходников, ни шелла
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /bin/api /api
EXPOSE 8080
ENTRYPOINT ["/api"]

Ровно тот рисунок, который подтверждает базовый паттерн Docker для Go из официальных руководств: builder-сцена с FROM golang, скачивание зависимостей, сборка, а финальная сцена — FROM scratch или distroless, куда переносится только бинарник. Версия golang:1.25 не случайна: go.mod ядра fiber v3.5.0 требует go 1.25.0. COPY go.mod go.sum перед COPY . . — не педантизм, а экономика: Docker кэширует слои, и пока манифесты зависимостей не менялись, слой go mod download берётся из кэша, даже если вы правили весь код. EXPOSE — документация порта для читателя и инструментов, ENTRYPOINT в exec-форме — квадратные скобки — запускает бинарник напрямую, без обёртки-шелла, и это критично для сигналов: подробности в quiz ниже. Тег nonroot у distroless означает, что процесс стартует под безымянным пользователем с UID 65532 — не под root, как и положено сервису.

Финальная сцена: scratch, alpine или distroless#

Выбор финального образа — три честных варианта с разными компромиссами. scratch — это вообще ничего: в образе лежит один ваш файл, и запуск возможен только потому, что бинарник статический; ни шелла для отладки, ни библиотек, ни сертификатов. Последнее — главная ловушка: если приложение само ходит по HTTPS наружу (клиент к чужому API, отправка почты), ему нужны корневые сертификаты — в scratch их нет, и первый же исходящий запрос по TLS упадёт с ошибкой проверки сертификата; лечится COPY /etc/ssl/certs/ca-certificates.crt из builder-сцены. distroless/static — та же минималистичность плюс уже уложенные сертификаты, часовые пояса и заготовленный непривилегированный пользователь nonroot; это дефолт, который мы взяли в каноническом Dockerfile. alpine — полноценный мини-дистрибутив с busybox-шеллом: удобен для отладки docker exec, но тащит musl вместо glibc — со статическим CGO_ENABLED=0-бинарником это безопасно, с динамическим — ловушка из прошлого блока. Отдельная строка экономики: образ на базе scratch или distroless весит порядка пятнадцати мегабайт и почти не несёт поверхности атаки — нечего обновлять, кроме вашего кода; сборка и доставка образа — это docker build -t api:1.0 . и docker run -p 8080:8080 api:1.0, и оба шага можно прогнать локально до всякого сервера.

Базовые образы для финальной сцены#

ОбразЧто внутриПорядок размераКогда брать
scratchпусто: только ваш бинарникединицы МБмаксимальная минимализм, сами кладёте CA-сертификаты при исходящем TLS
distroless/static-debian12:nonrootбинарник, CA-сертификаты, tzdata, пользователь 655322–3 МБ + бинарникдефолт для прод-сервисов без шелла
alpine:3.xbusybox-шелл, musl, apkоколо 5 МБ + бинарникнужен шелл для отладки docker exec
golang:1.25-alpineтулчейн Go, модули, исходникисотни МБ–ГБтолько сцена builder: в доставку не уезжает

Проверьте себя: exec-форма ENTRYPOINT

Почему в каноническом Dockerfile ENTRYPOINT записан в exec-форме ["/api"], а не в shell-форме ENTRYPOINT /api?

Соберите Dockerfile по памяти

Вставьте пропуски так, чтобы multi-stage сборка собрала статический бинарник и доставила его в пустой образ.

FROM golang:1.25-alpine build WORKDIR /src COPY go.mod go.sum ./ RUN go mod COPY . . RUN =0 GOOS=linux go build -o /bin/api ./cmd/api FROM scratch --from=build /bin/api /api EXPOSE 8080 ENTRYPOINT ["/api"]

Переменные окружения в бою: FromEnv встречает прод#

Приложение собрано и упаковано, но дев и прод различаются не кодом, а окружением — и здесь долги прошлого возвращаются с процентами. Канон двадцатого урока: встроенной поддержки env в fiber.Config нет, конфигурация — ваш код между os.Getenv и литералом структуры; канон двадцать второго — этот код живёт в одном месте, internal/config.FromEnv, читает переменные один раз на старте и раздаёт значения вниз параметрами. Прод-запуск ложится на этот канон без единой правки: контейнер получает переменные флагами docker run -e APP_NAME=api -e FIBER_READ_TIMEOUT=5s, а из файла целиком — docker run --env-file api.env; docker compose делает то же директивами environment и env_file; systemd — директивами Environment и EnvironmentFile в юните. Заметим главное: ни один слой не изменился, потому что ни один слой не зовет os.Getenv сам. Добавим в FromEnv чтение PORT из двадцатого урока с дефолтом 8080 — и точка прослушивания станет конфигурируемой: buildApp получает Config, а main вызывает app.Listen(":"+cfg.Port). Переменные канона (APP_NAME, FIBER_READ_TIMEOUT, PORT) едут в прод в том же виде, в котором их проверяли t.Setenv-тестами из двадцать первого урока: окружение и код уже договорились.

Что пересоберёт docker build после правки кода?

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

# Первая сборка: go mod download качал зависимости 40 секунд
$ docker build -t api:1.0 .
... (полный прогон всех слоёв)

# Правим ТОЛЬКО internal/handlers/task.go, не трогая go.mod и go.sum
$ docker build -t api:1.1 .

Ступень третья: зачем reverse proxy за nginx#

Контейнер слушает :8080, но пользователи стучатся на 80 и 443 вашего домена, и между ними принято ставить reverse proxy — прокси, который принимает запросы от клиентов и от их имени ходит к бэкенду. Nginx в этой роли решает сразу пачку задач, каждая из которых — отдельная глава, если делать её внутри Go-процесса: терминация TLS (сертификат живёт в одном конфиге, а не в каждом приложении), много сервисов на одном IP по имени хоста (example.com → fiber, grafana.example.com → monitoring), отдача статики и сжатие, общие журналы доступа и базовая оборона от мусорных запросов. Есть и техническая причина: порты ниже 1024 — привилегированные, их слушают процессы с соответствующими правами; fiber-сервис работает под непривилегированным пользователем (nonroot 65532 в контейнере, свой пользователь в systemd), и пусть порт 8080 остаётся приватным, а общение с внешним миром берёт на себя прокси. Связка отзеркаливает канон урока про группы: клиент видит один домен, за ним дерево маршрутов делится между бэкендами. Настройка — один конфигурационный файл, который мы сейчас соберём; в него же войдут заголовки, о которых у fiber есть строгое мнение.

Канон: nginx reverse proxy перед Fiber

GO
# /etc/nginx/conf.d/api.conf — reverse proxy перед Fiber
upstream fiber_api {
    # приложение слушает только локальный интерфейс
    server 127.0.0.1:8080;
}

server {
    listen 80;
    server_name example.com;

    # обычные HTTP-маршруты уезжают приложению
    location / {
        proxy_pass http://fiber_api;
        # кто пришёл на самом деле: дописываем адрес клиента в цепочку
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # схема, которую видел клиент (после включения TLS — https)
        proxy_set_header X-Forwarded-Proto $scheme;
        # оригинальный Host вместо 127.0.0.1:8080
        proxy_set_header Host $host;
    }

    # WebSocket-ветка шестнадцатого урока требует особых заголовков
    location /ws/ {
        proxy_pass http://fiber_api;
        # по умолчанию nginx говорит с бэкендом по HTTP/1.0 — апгрейд там не живёт
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

upstream — именованный бэкенд: когда приложений станет два, список серверов расширится, а location останется прежним. proxy_pass передаёт запрос приложению; proxy_set_header переписывает заголовки на пути. X-Forwarded-For собирается директивой $proxy_add_x_forwarded_for: nginx берёт пришедшую цепочку и дописывает в конец адрес клиента ($remote_addr), так что к fiber заголовок доезжает честной историей маршрута. X-Forwarded-Proto доносит схему — приложение узнает, что клиент шёл по https, хотя до прокси дошёл http; Host сохраняет домен для генерации абсолютных ссылок. Конфиг проверяется nginx -t и применяется systemctl reload nginx — без разрыва обслуживаемых соединений.

X-Forwarded-For глазами fiber: TrustProxy и c.IP()#

Заголовок X-Forwarded-For — штучка двусторонняя: его пишет прокси и его же может подделать любой клиент, дописав себе чужой IP. Исходники fiber v3.5.0 предупреждают в док-комментарии поля ProxyHeader дословно: headers are easily spoofed and the detected IP addresses are unreliable. Поэтому в v3 машина доверия работает в два такта. Первый такт — fiber.Config.TrustProxy: без него fiber вообще не смотрит в пересылаемые заголовки. Второй — TrustProxyConfig: список адресов, с которых можно верить, — Proxies (IP-адреса или CIDR-диапазоны) и флаги Loopback, LinkLocal, Private, UnixSocket для целых классов адресов. Если запрос пришёл с доверенного адреса и задан ProxyHeader, c.IP() идёт по цепочке X-Forwarded-For справа налево, пропуская доверенные прокси, и возвращает первый недоверенный адрес — так подделка слева цепочки игнорируется автоматически. Если запрос пришёл не с доверенного адреса, c.IP() возвращает RemoteIP — адрес TCP-соединения, то есть сам прокси. Заодно доверенные прокси открывают c.Scheme() из X-Forwarded-Proto и c.Host() из X-Forwarded-Host. Логика разложена в req.go: метод IP() проверяет IsProxyTrusted() и достаёт адрес из заголовка, только когда доверие подтверждено; EnableIPValidation добавляет валидацию строк и пусть стоит там, где цепочки длинные и странные.

Канон: Fiber за nginx — верим только своему прокси

GO
// buildApp: прод-сборка Fiber за reverse proxy на той же машине
cfg := config.FromEnv()

app := fiber.New(fiber.Config{
    ReadTimeout: cfg.ReadTimeout,
    // без TrustProxy заголовки пересылки не читаются вовсе
    TrustProxy: true,
    TrustProxyConfig: fiber.TrustProxyConfig{
        // nginx на той же машине: доверяем loopback, 127.0.0.0/8
        Loopback: true,
    },
    // у доверенных прокси c.IP() читает этот заголовок
    ProxyHeader: fiber.HeaderXForwardedFor, // "X-Forwarded-For"
})

app.Get("/whoami", func(c fiber.Ctx) error {
    // теперь это адрес клиента, а не 127.0.0.1 — адрес nginx
    return c.JSON(fiber.Map{"ip": c.IP(), "scheme": c.Scheme()})
})
log.Fatal(app.Listen(":8080"))

Три поля, и ни одного лишнего. TrustProxy: true включает сам механизм; Loopback: true описывает доверие классом адресов — для прокси на другой машине здесь был бы Proxies: []string{"10.0.0.5"} или CIDR; ProxyHeader: fiber.HeaderXForwardedFor называет заголовок, из которого c.IP() будет читать цепочку. Константа HeaderXForwardedFor объявлена в constants.go ядра равной строке X-Forwarded-For. Отныне лимитер семнадцатого урока и журналы девятнадцатого, если они пользуются c.IP(), видят реальных клиентов, а не один и тот же loopback. Проверка в одну строку: curl через nginx должен вернуть ваш внешний адрес, а curl напрямую на :8080 — 127.0.0.1. Отсюда же правило гигиены: приложение за nginx лучше слушать только на локальном интерфейсе, чтобы прямых запросов извне не существовало, — тогда доверие loopback остаётся честной сделкой: с того же хоста стучатся только свои.

Что вернёт c.IP() за nginx?

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

app := fiber.New(fiber.Config{
    TrustProxy: true,
    TrustProxyConfig: fiber.TrustProxyConfig{Loopback: true},
    ProxyHeader: fiber.HeaderXForwardedFor,
})

app.Get("/whoami", func(c fiber.Ctx) error {
    return c.JSON(fiber.Map{"ip": c.IP()})
})

// Клиент 203.0.113.7 заходит через nginx на той же машине.
// nginx шлёт fiber: X-Forwarded-For: 203.0.113.7

WebSocket за прокси: Upgrade, Connection и HTTP/1.1#

WebSocket-ветка из шестнадцатого урока — эхо на /ws/chat/:room, комнаты и broadcast — упрётся в прокси первой. Рукопожатие начинается как обычный HTTP GET с заголовками Upgrade: websocket и Connection: Upgrade, но nginx по умолчанию их не пропускает, и документация nginx объясняет почему без обинутков: Upgrade — hop-by-hop заголовок, он относится к конкретному соединению, а не путешествует по цепочке прокси, поэтому от клиента к бэкенду его нужно передать явно. Вторая деталь ещё коварнее: по умолчанию nginx говорит с бэкендом по HTTP/1.0, где механизма смены протокола нет вовсе, — директива proxy_http_version 1.1 обязательна. После обеих правок nginx с версии 1.3.13 умеет туннель: получив от бэкенда 101 Switching Protocols, он перестаёт быть HTTP-прокси и прозрачно гоняет кадры в обе стороны, так что contrib-пакет с его hijack из шестнадцатого урока не замечает подмены. Практические мелочи: длинноживущие соединения обрывает proxy_read_timeout (дефолт — 60 секунд) — для пуш-каналов его поднимают или держат heartbeat ping/pong, о котором урок шестнадцатый уже рассказывал; изящный вариант Connection через map $http_upgrade $connection_upgrade апгрейдит только те запросы, что просят апгрейд, и не портит обычные.

Проверьте себя: почему nginx рвёт WebSocket

Fiber-приложение с /ws-маршрутом стоит за nginx с одним proxy_pass. Почему браузер не получает 101 Switching Protocols?

Ступень четвёртая: systemd-юнит — когда Docker не нужен#

Docker — не единственный честный путь: на классическом VPS-сервере со свежесобранным бинарником смотрит systemd, и это не «бедный родственник» контейнеров, а штатный способ держать долгоживущий процесс. Юнит — текстовый файл в /etc/systemd/system/api.service — декларирует то, что раньше говорил руками: что запускать, от кого, с каким окружением и что делать при смерти. Раздел [Unit] описывает зависимости — After=network.target заставляет ждать готовности сети; раздел [Service] несёт сам процесс: User=api — сервис не работает от root, WorkingDirectory и ExecStart — где и что запускать, EnvironmentFile — тот же файл окружения, что и docker run --env-file; Restart=always — политика выживания, о которой дальше отдельный блок. Раздел [Install] с WantedBy=multi-user.target включает юнит в обычную загрузку системы. Команды управления трёхшаговые: systemctl daemon-reload перечитывает юниты после правки файла, systemctl enable --now api включает автозапуск и стартует сразу, journalctl -u api -f показывает журнал — stdout и stderr приложения journald собирает в единый журнал с поиском по юниту, что закрывает потребность «посмотреть логи» без всякого logger-мидлвара, хотя читаемость строк — по-прежнему заслуга девятнадцатого урока.

Канон: юнит /etc/systemd/system/api.service

GO
# /etc/systemd/system/api.service — юнит Fiber-приложения
[Unit]
Description=Fiber API (задачи)
# ждём готовности сети, прежде чем слушать порт
After=network.target

[Service]
# сервис работает не от root: свой пользователь и рабочая папка
User=api
WorkingDirectory=/opt/api
ExecStart=/opt/api/bin/api

# окружение из файла с правами 0600 — то же, что и в контейнере
EnvironmentFile=/opt/api/api.env

# упал или был убит — поднять заново; пауза между попытками
Restart=always
RestartSec=5

# stdout и stderr собирает journald: journalctl -u api
StandardOutput=journal
StandardError=journal

[Install]
# старт вместе с системой в многопользовательском режиме
WantedBy=multi-user.target

Файл читается сверху вниз как сборка из конструкторов прошлого урока: объявления, процесс, выживание, журнал, включение в загрузку. User=api и WorkingDirectory=/opt/api — минимальная гигиена: бинарнику не нужны права root, чтобы слушать :8080 (привилегированные порты начинаются ниже), а свой пользователь означает свой владелец файлов и обрубленные права при взломе. EnvironmentFile=/opt/api/api.env подкладывает переменные канона — PORT, APP_NAME, FIBER_READ_TIMEOUT — ровно те, что FromEnv читает на старте. Restart=always с RestartSec=5 превращает «приложение упало» из инцидента с пробуждением админа в строку журнала. После правки файла — systemctl daemon-reload, затем enable --now; статус и хвост журнала — systemctl status api и journalctl -u api.

Restart=always и сигналы: как служба умирает и оживает#

Политика Restart — договор с systemd о том, что считать поводом для перезапуска. Restart=always поднимает процесс при любом завершении — и с кодом ошибки, и с кодом 0; Restart=on-failure — только при неуспехе: не-нулевой код выхода или смерть от сигнала; Restart=on-success — наоборот, только после чистого завершения; дефолт no — не перезапускать вовсе. Исключение для всех политик одно: systemctl stop — вы явно попросили остановить, и systemd послушно не лезет обратно. Пауза между попытками задаётся RestartSec, чтобы падающий по кругу сервис не рожал шторм перезапусков. Теперь сигнал, который связывает этот блок с двадцатым уроком: при остановке и systemd, и docker stop шлют процессу SIGTERM и дают время на graceful shutdown — ритуал, который мы выстроили в двадцатом уроке, повторять не будем; здесь важно следствие: таймауты остановки в окружении должны быть щедрее вашего shutdown-таймаута, иначе дренаж оборвут принудительно. Замыкает картинку иерархия: systemd следит за процессом и его выходом, Docker — за контейнером (docker run --restart=on-failure или политика restart в compose), а fiber внутри просто корректно обрабатывает SIGTERM — три уровня, один и тот же контракт.

Проверьте себя: семантика Restart

Сервис в юните объявлен с Restart=always, админ выполняет systemctl stop api. Что произойдёт, а что — если процесс завершится сам с кодом 2?

Соберите порядок деплоя от тестов до боевого curl

Расставьте шаги пайплайна развертывания в правильном порядке: от зелёных тестов до проверки через reverse proxy.

Строки

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

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

Итоги: четыре ступени одного канона#

Соберём маршрут, который прошли: go build с CGO_ENABLED=0 превращает проект двадцать второго урока в статический бинарник; multi-stage Dockerfile пакует его в образ, где тулчейн остался в builder-сцене, а в доставку уехал один файл; nginx принимает 80/443, переводит стрелки на 127.0.0.1:8080 и подписывает запросы заголовками X-Forwarded-For и X-Forwarded-Proto; systemd или docker --restart следят, чтобы процесс жил, а FromEnv из internal/config читала окружение ровно как в деве. Канон курса не изменился ни на строчку: инстанс на :8080, маршрут /api/tasks, buildApp, страж requireAPIKey — деплой оказался не отдельным режимом, а упаковкой того же кода. Для сравнения дисциплин: в плане курса gin деплой-урок стоит соседний по повестке — сборка, Docker, health-чеки и окружение, — у Fiber-версии в центре nginx-связка с WebSocket-туннелем, и это честное различие маршрутов, а не претензия на превосходство: health-чек вы и сами добавите маршрутом /healthz за пять минут. Честное слово про инструменты: песочница сайта ни docker, ни nginx, ни systemd не исполняет — runner-ов в уроках Fiber нет с первого урока, а интерактивы здесь quiz, predict, fill, order и флешкарты; весь материал урока требует локального тулчейна и тренировочного сервера, и это единственный урок курса, где без виртуалки не обойтись. Полный план — на странице курса Fiber.

Следующий, двадцать четвёртый урок — производительность Fasthttp: почему Fiber быстр, zero allocation и границы применимости. Логика прямая: сегодня мы поставили приложение на боевой сервер, завтра заглянем под капот движка, который этот сервер кормит — и поймём, за счёт чего fiber и выигрывает те самые бенчмарки, и на каких нагрузках быстрые пути fasthttp перестают помогать.

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

Термин

CGO_ENABLED=0 GOOS=linux go build

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

Статический бинарник: без cgo не линкуется с libc — работает в scratch и alpine без glibc. GOOS/GOARCH — кросс-компиляция; -ldflags "-s -w" худит файл.

1 / 6

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

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

Вывод

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

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

  • Деплой fiber — это упаковка того же канона: go build собирает статический бинарник (CGO_ENABLED=0, GOOS=linux), Docker пакует его multi-stage схемой, nginx прикрывает reverse proxy, systemd следит за жизнью.
  • CGO_ENABLED=0 делает бинарник независимым от libc: без него сборка под glibc не заведётся в alpine/scratch; при cgo-пакетах собирают в образе той же libc.
  • Multi-stage Dockerfile: builder-сцена с тулчейном, финальная — scratch или distroless:nonroot; go mod download до COPY . . кэширует зависимости; ENTRYPOINT в exec-форме — чтобы SIGTERM дошёл до процесса.
  • За nginx fiber получает TrustProxy: true, TrustProxyConfig (Loopback/Proxies) и ProxyHeader: fiber.HeaderXForwardedFor — c.IP() вернёт клиента из X-Forwarded-For, а не адрес прокси; WebSocket требует proxy_http_version 1.1 и явных Upgrade/Connection.
  • Окружение читает один FromEnv из internal/config; секреты — не в git и не в слоях образа; systemd-юнит с Restart=always и EnvironmentFile повторяет для процесса то, что делает docker --restart для контейнера.

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

Разверните канон курса на тренировочной машине по ступеням урока. Шаг первый: соберите статический бинарник CGO_ENABLED=0 GOOS=linux go build и проверьте file/ldd. Шаг второй: напишите multi-stage Dockerfile по каноническому образцу, соберите образ и прогоните docker run -p 8080:8080 с переменными -e APP_NAME=api -e PORT=8080 — убедитесь, что /api/tasks отвечает. Шаг третий: добавьте в FromEnv чтение PORT с дефолтом 8080 и тест t.Setenv на него из канона двадцать первого урока. Шаг четвёртый: поставьте nginx с двумя location — обычным и WebSocket — и проверьте curl'ом, что /whoami возвращает ваш адрес, а не 127.0.0.1, а прямой запрос к :8080 возвращает 127.0.0.1. Шаг пятый: напишите systemd-юнит с User, EnvironmentFile, Restart=always и RestartSec=5, включите enable --now и уроните процесс kill'ом — убедитесь, что systemd поднял его через RestartSec. Бонус: соберите вариант финальной сцены на scratch с ручным COPY ca-certificates и сравните размеры образов.

Личные заметкиПусто
Хранятся только в вашем браузере (Fiber · Развертывание Fiber-приложения)

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

nginx отдаёт 502 Bad Gateway — что сломалось?

502 означает, что прокси не смог достучаться до бэкенда. Проверяйте по цепочке: жив ли процесс (systemctl status api / docker ps), слушает ли он именно 127.0.0.1:8080 (ss -tlnp), тот ли порт в upstream. Классика деплоя — приложение слушает 127.0.0.1 внутри контейнера, а маппинг портов -p не задан: для контейнера Fiber должен слушать 0.0.0.0 внутри, а снаружи наружу маппится -p.

Может ли Fiber слушать порт 80 напрямую, без nginx?

Технически да, но порты ниже 1024 — привилегированные: процессу нужны права (root, CAP_NET_BIND_SERVICE или sysctl-настройка), а TLS, сжатие, доступ к нескольким сервисам и журналы доступа придётся городить в приложении. Reverse proxy берёт всё это на себя и позволяет перезапускать приложение, не меняя внешний фронт.

scratch, alpine или distroless — что выбрать?

Дефолт — distroless/static-debian12:nonroot: сертификаты и непривилегированный пользователь уже внутри, шелла нет — и не нужно. scratch — когда хотите полный контроль и знаете, что исходящих TLS-вызовов не будет или сами положите CA-сертификаты. alpine — когда нужен шелл для отладки docker exec. В любом варианте бинарник должен быть статическим: CGO_ENABLED=0.

Зачем TrustProxy, если nginx и так шлёт X-Forwarded-For?

Потому что заголовок шлёт не только nginx: любой клиент может дописать X-Forwarded-For руками и подменить своё имя в ваших лимитерах и журналах. TrustProxy с TrustProxyConfig ограничивает круг источников, которым fiber верит: без него c.IP() честно возвращает адрес TCP-соединения — прокси, а не клиента; с ним — идёт по цепочке справа налево и игнорирует подделку слева.

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

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

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

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

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

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

Gin: самый популярный веб-фреймворк Go

Курс Gin для Go: маршрутизация, binding, валидация, middleware, JWT, WebSocket, тесты и деплой. 25 уроков с кодом и итоговым REST API.

  • Уверенно владеть gin.Engine: маршруты, группы, версии
  • Привязывать запросы к структурам (ShouldBindJSON и др.)
  • Писать middleware и использовать встроенные
  • Строить аутентификацию на JWT