535 lines
20 KiB
Markdown
535 lines
20 KiB
Markdown
# Спецификация серверной части 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/<slug>/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) — при утере их можно восстановить из манифестов модпаков, но займёт время.
|