# Спецификация серверной части Minecraft проекта (Backend & Infrastructure) ## 1. Стек технологий * **Бэкенд-платформа:** Go (чистый net/http со стандартным роутером, для максимальной прозрачности и контроля, без внешних зависимостей). * **База данных:** PostgreSQL (реляционная СУБД для надежного хранения транзакций токенов, пользователей и связей файлов). * **Фронтенд (Сайт и Админка):** SPA на Vue.js / React / Svelte, либо классический монолит на Go-шаблонах (html/template) для максимального упрощения деплоя (всё в одном исполняемом файле). * **Reverse Proxy и статика:** Caddy (автоматический HTTPS, отдача файлов, reverse proxy к Go-бэкенду — всё в одном бинарнике). * **Контейнеризация:** Docker + Docker Compose. * **Registry:** Gitea Container Registry. * **Авто-обновление контейнеров:** Watchtower (pull новых образов из registry). --- ## 2. Архитектура хранилища файлов (Content-Addressable Storage - CAS) Для экономии места на диске сервера и клиента, а также для сквозного кэширования, все файлы модов, библиотек и ассетов хранятся по их SHA-1 хэшам. * **Путь на сервере:** `/var/www/cdn/files/[первые два символа хэша]/[полный хэш]` * *Пример:* Файл с хэшем `a1b2c3d4e5...` будет лежать по пути `/var/www/cdn/files/a1/b2c3d4e5...` (это предотвращает замедление файловой системы при наличии десятков тысяч файлов в одной директории). * **URL для скачивания:** `https://cdn.myserver.com/files/a1b2c3d4e5...` --- ## 3. Схема Базы Данных (PostgreSQL) ```sql -- Таблица пользователей CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(32) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, uuid UUID UNIQUE NOT NULL, role VARCHAR(16) DEFAULT 'user', -- 'user', 'admin' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Таблица скинов и плащей (ссылки на файлы в CAS) CREATE TABLE player_textures ( user_id INT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, skin_hash VARCHAR(40), -- SHA-1 хэш файла скина в CAS cape_hash VARCHAR(40), -- SHA-1 хэш файла плаща в CAS is_slim BOOLEAN DEFAULT FALSE -- модель скина (Alex/Steve) ); -- Сессии Yggdrasil (для авторизации лаунчера и игры) CREATE TABLE yggdrasil_sessions ( client_token UUID NOT NULL, access_token UUID PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at TIMESTAMP NOT NULL ); -- Таблица модпаков (игровых серверов) CREATE TABLE modpacks ( id SERIAL PRIMARY KEY, slug VARCHAR(32) UNIQUE NOT NULL, -- например: 'hitech', 'magic' name VARCHAR(64) NOT NULL, minecraft_version VARCHAR(16) NOT NULL, java_version INT NOT NULL, -- 8, 17, 21 server_ip VARCHAR(255) NOT NULL, -- IP:Port для добавления в servers.dat is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Единый реестр уникальных файлов (CAS) CREATE TABLE global_files ( sha1 VARCHAR(40) PRIMARY KEY, size_bytes BIGINT NOT NULL, file_name VARCHAR(255) NOT NULL -- оригинальное имя файла (для истории) ); -- Релизы лаунчера (заполняются через CI/CD) CREATE TABLE launcher_releases ( id SERIAL PRIMARY KEY, version VARCHAR(32) NOT NULL, -- например: '1.2.0' os VARCHAR(16) NOT NULL, -- 'windows', 'linux', 'darwin' arch VARCHAR(16) NOT NULL, -- 'amd64', 'arm64' sha256 VARCHAR(64) NOT NULL, -- SHA-256 для проверки бинарника лаунчера file_path VARCHAR(255) NOT NULL, is_active BOOLEAN DEFAULT TRUE, is_mandatory BOOLEAN DEFAULT TRUE, -- принудительное обновление created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` --- ## 4. Спецификация API Эндпоинтов ### 4.1. Автоматический деплой из CI/CD (Protected Route) Эндпоинт, который вызывает твой пайплайн сборки после успешной компиляции лаунчера. * **Маршрут:** `POST /api/admin/launcher/release` * **Авторизация:** Заголовок `X-CI-Token: <секретный_токен_из_секретов_репозитория>` * **Тело запроса (Multipart Form Data):** * `version`: "1.2.0" * `os`: "windows" * `arch`: "amd64" * `sha256`: "hash_of_binary..." * `file`: (бинарный файл лаунчера) ### 4.2. Yggdrasil API (Mojang Emulation) * `POST /authserver/authenticate` — Аутентификация по логину/пароля. Возвращает UUID и токены. * `POST /authserver/refresh` — Обновление сессии лаунчера по `clientToken` и `accessToken`. * `POST /authserver/validate` — Быстрая проверка токена при запуске лаунчера. * `GET /sessionserver/session/minecraft/profile/{uuid}` — Эндпоинт, к которому обращается сам игровой клиент для загрузки скинов игроков на сервере. ### 4.3. Публичное API сайта и лаунчера * `POST /api/web/register` — Регистрация игрока. * `POST /api/web/login` — Логин в личный кабинет сайта. * `POST /api/web/profile/skin` — Загрузка скина (принимает PNG, считает его SHA-1, сохраняет в `/files/` и привязывает к юзеру). * `GET /api/launcher/latest` — Возвращает информацию о последней версии лаунчера и ссылки на скачивание для автообновления. * `GET /api/servers.json` — Список активных модпаков для меню выбора в лаунчере. --- ## 5. Алгоритмы и Пайплайны работы ### 5.1. Пайплайн загрузки и сборки модпака (в Админ-панели) Когда админ загружает ZIP-архив с обновлением модов/конфигов через панель управления: ```text [Архив с модами] -> [Распаковка во временную папку] │ ▼ [Обход каждого файла в цикле] │ ▼ [Расчет SHA-1 хэша] │ ┌─────────────┴─────────────┐ ▼ ▼ [Хэш ЕСТЬ в базе?] [Хэша НЕТ в базе] │ │ │ [Копирование в CAS] │ [/files/ab/abcdef12...] │ │ └─────────────┬─────────────┘ ▼ [Добавление в manifest.json] ``` **Итоговый формат генерируемого `manifest.json` для лаунчера:** ```json { "minecraft_version": "1.21", "java_version": 21, "server_info": { "name": "HiTech Server", "ip": "play.myserver.com:25565" }, "files": [ { "path": "mods/jei-1.21.jar", "hash": "a1b2c3d4e5f6...", "size": 1048576, "url": "https://cdn.myserver.com/files/a1b2c3d4e5f6..." }, { "path": "config/general.json", "hash": "f7g8h9i0j1k2...", "size": 1245, "url": "https://cdn.myserver.com/files/f7g8h9i0j1k2..." } ] } ``` ### 5.2. Автоматическое добавление сервера в игру (servers.dat) Перед запуском игры лаунчер читает блок `server_info` из манифеста. Если в `instances//servers.dat` отсутствует сервер с IP `server_info.ip`, лаунчер парсит этот NBT-файл и добавляет в него новую запись: * **Имя:** `server_info.name` * **IP:** `server_info.ip` Это избавляет игроков от ручного ввода IP в клиенте. ### 5.3. Пайплайн CI/CD Релиза Лаунчера ```text [Коммит/Тег в Git] -> [Сборка в CI (Windows/Linux/macOS)] │ ▼ [Расчет SHA-256 для каждого] │ ▼ [POST-запрос с секретным токеном] │ ▼ [Go-Бэкенд сохраняет файлы] [Обновляет версию лаунчера в БД] │ ▼ [Игроки видят обновление при запуске] ``` --- ## 6. Docker Compose Все сервисы проекта поднимаются на VPS через Docker Compose. Структура: ### 6.1. Сервисы #### `backend` — Go-приложение Собирается как Docker-образ и публикуется в Gitea Container Registry. ```yaml backend: image: git.myserver.com/yourname/mc-backend:latest container_name: mc-backend restart: unless-stopped environment: DATABASE_URL: postgres://mcuser:${DB_PASSWORD}@postgres:5432/mcserver?sslmode=disable CI_TOKEN: ${CI_TOKEN} CDN_BASE_URL: https://cdn.myserver.com volumes: - cdn_files:/var/www/cdn/files depends_on: postgres: condition: service_healthy expose: - "8080" labels: - "com.centurylinklabs.watchtower.enable=true" ``` *Переменные `.env` на сервере:* ``` DB_PASSWORD=... CI_TOKEN=... ``` #### `postgres` — База данных ```yaml postgres: image: postgres:16-alpine container_name: mc-postgres restart: unless-stopped environment: POSTGRES_DB: mcserver POSTGRES_USER: mcuser POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data - ./migrations:/docker-entrypoint-initdb.d healthcheck: test: ["CMD-SHELL", "pg_isready -U mcuser -d mcserver"] interval: 10s timeout: 5s retries: 5 expose: - "5432" ``` Миграции из папки `./migrations/` применяются автоматически при первом запуске (согласно поведению официального образа PostgreSQL). #### `caddy` — Reverse Proxy + статика ```yaml caddy: image: caddy:2-alpine container_name: mc-caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config - cdn_files:/var/www/cdn/files:ro ``` Caddy автоматически получает и обновляет TLS-сертификаты для всех доменов, указанных в Caddyfile. #### `watchtower` — Авто-обновление контейнеров ```yaml watchtower: image: containrrr/watchtower container_name: mc-watchtower restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock environment: WATCHTOWER_CLEANUP: "true" WATCHTOWER_POLL_INTERVAL: 300 WATCHTOWER_LABEL_ENABLE: "true" WATCHTOWER_NOTIFICATIONS: "" ``` Watchtower проверяет наличие новых образов каждые 300 секунд (5 минут). Обновляются только контейнеры с меткой `com.centurylinklabs.watchtower.enable=true`. Старые образы удаляются после успешного обновления (`CLEANUP=true`). --- ### 6.2. Volumes ```yaml volumes: pgdata: driver: local cdn_files: driver: local caddy_data: driver: local caddy_config: driver: local ``` | Volume | Назначение | Доступ | |---|---|---| | `pgdata` | Данные PostgreSQL | `postgres` (rw) | | `cdn_files` | CAS-файлы (моды, ассеты, скины) | `backend` (rw), `caddy` (ro) | | `caddy_data` | TLS-сертификаты и состояние Caddy | `caddy` (rw) | | `caddy_config` | Кэш конфигурации Caddy | `caddy` (rw) | Том `cdn_files` примонтирован read-only в Caddy, чтобы proxy-сервер не мог случайно модифицировать файлы. Только `backend` имеет права на запись. --- ### 6.3. Полный docker-compose.yml ```yaml services: caddy: image: caddy:2-alpine container_name: mc-caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config - cdn_files:/var/www/cdn/files:ro backend: image: git.myserver.com/yourname/mc-backend:latest container_name: mc-backend restart: unless-stopped environment: DATABASE_URL: postgres://mcuser:${DB_PASSWORD}@postgres:5432/mcserver?sslmode=disable CI_TOKEN: ${CI_TOKEN} CDN_BASE_URL: https://cdn.myserver.com volumes: - cdn_files:/var/www/cdn/files depends_on: postgres: condition: service_healthy expose: - "8080" labels: - "com.centurylinklabs.watchtower.enable=true" postgres: image: postgres:16-alpine container_name: mc-postgres restart: unless-stopped environment: POSTGRES_DB: mcserver POSTGRES_USER: mcuser POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data - ./migrations:/docker-entrypoint-initdb.d healthcheck: test: ["CMD-SHELL", "pg_isready -U mcuser -d mcserver"] interval: 10s timeout: 5s retries: 5 expose: - "5432" watchtower: image: containrrr/watchtower container_name: mc-watchtower restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock environment: WATCHTOWER_CLEANUP: "true" WATCHTOWER_POLL_INTERVAL: 300 WATCHTOWER_LABEL_ENABLE: "true" volumes: pgdata: cdn_files: caddy_data: caddy_config: ``` --- ## 7. Конфигурация Caddy Caddy выполняет три роли: reverse proxy для API, отдача CAS-файлов и автоматический HTTPS. ``` # Caddyfile # CDN — отдача CAS-файлов cdn.myserver.com { root * /var/www/cdn/files @hasPrefix path /files/* handle /files/* { file_server { hide .htaccess } header Cache-Control "public, max-age=31536000, immutable" } handle /skins/* { file_server { hide .htaccess } header Cache-Control "public, max-age=3600" } } # API и Yggdrasil api.myserver.com { reverse_proxy backend:8080 } ``` **Логика кэширования:** * **CAS-файлы (`/files/`):** `Cache-Control: max-age=31536000, immutable` — на 1 год, потому что содержимое файла никогда не меняется (меняется только хэш в манифесте). * **Скины (`/skins/`):** `Cache-Control: max-age=3600` — 1 час, чтобы игроки видели смену скина без жёсткого сброса кэша. --- ## 8. Docker-образы и CI/CD ### 8.1. Тегирование образов Все образы публикуются в Gitea Container Registry с двумя тегами: * `latest` — всегда последняя успешная сборка * `${GIT_COMMIT_SHA:0:8}` — первые 8 символов хэша коммита для отслеживаемости ### 8.2. Dockerfile для backend ```dockerfile FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /mc-backend ./cmd/server FROM alpine:3.19 RUN apk --no-cache add ca-certificates WORKDIR /app COPY --from=builder /mc-backend . RUN adduser -D -g '' appuser USER appuser EXPOSE 8080 ENTRYPOINT ["/app/mc-backend"] ``` Используется multi-stage сборка: финальный образ на базе alpine (~20 MB), без Go toolchains и исходного кода. Приложение запускается от непривилегированного пользователя. ### 8.3. Схема деплоя ```text [Push в main] → [CI: тесты] → [CI: сборка Docker-образа] │ ↓ [Push в Gitea Container Registry] (теги: latest + sha) │ ↓ [Watchtower на VPS замечает новый образ] (опрос каждые 5 мин) │ ↓ [Pull → stop old → start new container] │ ↓ [Healthcheck PostgreSQL: OK] [Backend отвечает на :8080] [Caddy проксирует запросы] ``` --- ## 9. Переменные окружения на VPS Файл `.env` в корне проекта на сервере: ``` DB_PASSWORD=your-secure-password-here CI_TOKEN=your-ci-secret-token ``` Файл `.env` **не коммитится** в репозиторий. CI_TOKEN также хранится в секретах Gitea (Settings → Secrets) для использования в CI-пайплайне. --- ## 10. Структура проекта на VPS ``` /opt/mc-server/ ├── .env # Секретные переменные (не в git) ├── docker-compose.yml ├── Caddyfile ├── migrations/ │ ├── 001_init.sql │ └── 002_launcher_releases.sql └── backups/ # Резервные копии PostgreSQL ``` --- ## 11. Резервное копирование Ежедневный dump PostgreSQL через cron на хосте: ```bash docker exec mc-postgres pg_dump -U mcuser mcserver | gzip > /opt/mc-server/backups/mcserver_$(date +%Y%m%d).sql.gz ``` CAS-файлы рекомендуется бэкапить отдельно (rsync) — при утере их можно восстановить из манифестов модпаков, но займёт время.