Контекст
Месяц тишины. Новый год, каникулы, другие дела. 26 января я наконец вернулся к fxTunnel с одной целью: поднять на боевом сервере и начать пользоваться по-настоящему. У меня VPS, домен и готовый бинарник. Что может пойти не так?
Деплой: push и забыл
Ручной деплой — это несерьёзно. Хочу нормально: запушил в master → тесты прошли → продакшен обновился. Автоматически.
Написал GitHub Actions workflow, который:
1. Ждёт прохождения CI (тесты + линтер)
2. Собирает серверный бинарник
3. Заливает на VPS по SSH
4. Атомарно заменяет старый бинарник (загружает как .new, потом mv)
5. Рестартует сервис
6. Проверяет, что поднялся. Если нет — автоматический откат
Последний пункт важен. Если новая версия не стартует, сервер сам откатывается на предыдущую. Без моего участия. Можно деплоить хоть в 3 ночи и не переживать.
localhost — это не то, что вы думаете
Первый настоящий баг в продакшене. Туннель поднят, всё зелёное, но клиент не может подключиться к локальному сервису. Порт правильный, сервис запущен — connection refused.
Разбираюсь. Все думают, что localhost — это одна штука. На самом деле — это два адреса:
- 127.0.0.1 — IPv4 (старый, классический)
- ::1 — IPv6 (новый, современный)
Это два РАЗНЫХ адреса. Если ваш dev-сервер слушает на ::1:3000 (IPv6), а клиент пытается подключиться к 127.0.0.1:3000 (IPv4) — connection refused. Хотя оба «localhost».
На разных системах localhost резолвится по-разному. Windows чаще в IPv6, Linux — зависит от настроек. Не баг операционной системы, просто реальность двух протоколов.
Решение — fallback: пробуем IPv4, не получилось — пробуем IPv6.
// Пробуем IPv4 первым
conn, err := net.DialTimeout("tcp", "127.0.0.1:3000", timeout)
if err == nil {
return conn, nil
}
// Fallback на IPv6
return net.DialTimeout("tcp", "[::1]:3000", timeout)
Простое решение для неочевидной проблемы.
CGO: когда Go — не совсем Go
Ещё одна ловушка. fxTunnel использует SQLite — файловую базу данных, никакого отдельного сервера. Для self-hosted — отлично. Но есть нюанс.
SQLite-библиотека для Go (go-sqlite3) — это обёртка над C-кодом. А значит, при сборке нужен C-компилятор. Go обычно компилируется без внешних зависимостей (один из его главных плюсов), но SQLite ломает это правило.
В моём deploy workflow стояло CGO_ENABLED=0 (отключить C-код). Бинарник собирался без ошибок. Но при запуске:
Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work
Бабах. Сервер не стартует. Фикс — одна строка: CGO_ENABLED=1. Но момент показательный: Go умеет собираться без C-компилятора, и многие этим пользуются. А потом добавляют SQLite — и всё ломается.
Грабли
workflow_run ненадёжен. Первая версия использовала его для запуска деплоя после CI. Иногда просто не срабатывал. Переключился на wait-on-check-action — ждёт завершения конкретного job и только потом деплоит.
Забыл про фронтенд. Сервер встраивает веб-панель через go:embed. На CI нужно сначала собрать Vue-приложение (npm run build), и только потом компилировать Go. Порядок имеет значение.
Sync бинарников. Хотел на деплое скачивать клиентские бинарники из GitHub Releases. Но релиз может ещё не иметь файлов, если CI не завершился. Сделал синхронизацию «мягкой» — нет файлов, ну и ладно, деплой продолжается.
Итог
26 января — fxTunnel работает на боевом сервере: - Auto-deploy с автоматическим откатом - IPv4/IPv6 fallback — работает на любой системе - Push в master = обновление продакшена
766ba87 fix(client): add IPv4/IPv6 fallback
ce333d8 ci(deploy): add auto-deployment workflow
88fce88 fix(ci): enable CGO for server build
Я пользуюсь этим каждый день. И это лучшее чувство — когда собственный инструмент реально работает и помогает.
В следующей части: JWT-токены, которые протухают в самый неподходящий момент, и кастомная страница 404 — первые боевые фичи.