feat: add production deployment for rf4spotter.ru
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
# DNS: A/AAAA records for both names must point to the deployment server.
|
||||||
|
SITE_DOMAIN=rf4spotter.ru
|
||||||
|
FILES_DOMAIN=files.rf4spotter.ru
|
||||||
|
ACME_EMAIL=admin@rf4spotter.ru
|
||||||
|
|
||||||
|
POSTGRES_DB=rf4_spotter
|
||||||
|
POSTGRES_USER=rf4
|
||||||
|
POSTGRES_PASSWORD=replace-with-long-random-value
|
||||||
|
# URL-encode special characters from POSTGRES_PASSWORD in this URL.
|
||||||
|
DATABASE_URL=postgresql+psycopg://rf4:replace-with-url-encoded-password@db:5432/rf4_spotter
|
||||||
|
|
||||||
|
ADMIN_TOKEN=replace-with-at-least-32-random-characters
|
||||||
|
RATE_LIMIT_SECRET=replace-with-at-least-32-random-characters
|
||||||
|
ADMIN_BASIC_USER=rf4admin
|
||||||
|
# Generate with: docker run --rm caddy:2.10.2-alpine caddy hash-password --plaintext 'YOUR PASSWORD'
|
||||||
|
ADMIN_BASIC_PASSWORD_HASH=replace-with-caddy-password-hash
|
||||||
|
|
||||||
|
S3_ACCESS_KEY=replace-with-random-access-key
|
||||||
|
S3_SECRET_KEY=replace-with-at-least-32-random-characters
|
||||||
|
S3_BUCKET=catch-screenshots
|
||||||
|
|
||||||
|
OFFICIAL_RECORDS_URL=https://rf4game.de/records/region/RU/
|
||||||
|
OFFICIAL_RECORDS_REGION=RU
|
||||||
|
OFFICIAL_RECORDS_CATEGORY=records
|
||||||
|
OFFICIAL_IMPORT_REQUIRED=false
|
||||||
|
IMPORT_INTERVAL_SECONDS=3600
|
||||||
|
LOG_LEVEL=INFO
|
||||||
@@ -2,6 +2,7 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.cache/
|
.cache/
|
||||||
|
.env.production
|
||||||
.venv/
|
.venv/
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ RF4 Spotter — неофициальный сервис свежих точек
|
|||||||
|
|
||||||
Подробный план и актуальные чекбоксы находятся в [`docs/ROADMAP.md`](docs/ROADMAP.md). Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в [`docs/UI_UX_AUDIT.md`](docs/UI_UX_AUDIT.md).
|
Подробный план и актуальные чекбоксы находятся в [`docs/ROADMAP.md`](docs/ROADMAP.md). Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в [`docs/UI_UX_AUDIT.md`](docs/UI_UX_AUDIT.md).
|
||||||
|
|
||||||
|
Production-контур для домена `rf4spotter.ru`, TLS, секреты и команды первого запуска описаны в [`deploy/README.md`](deploy/README.md). Он использует отдельный `compose.production.yaml`; локальный `compose.yaml` остаётся средой разработки.
|
||||||
|
|
||||||
Gitea Actions workflow `.gitea/workflows/ci.yml` на каждый push и pull request проверяет Python, миграции на чистой PostgreSQL, Astro build и полный Compose/Playwright-сценарий. При падении E2E сохраняются логи контейнеров и Playwright-артефакты.
|
Gitea Actions workflow `.gitea/workflows/ci.yml` на каждый push и pull request проверяет Python, миграции на чистой PostgreSQL, Astro build и полный Compose/Playwright-сценарий. При падении E2E сохраняются логи контейнеров и Playwright-артефакты.
|
||||||
|
|
||||||
Актуальная инвентаризация источников и правила подключения адаптеров находятся в [`docs/data-source-audit.md`](docs/data-source-audit.md). Разрешённый технический пилот RF4DB/RF4-STAT описан в [`docs/community-source-pilot.md`](docs/community-source-pilot.md), а статус разрешений и лимитов — в [`docs/data-permissions.md`](docs/data-permissions.md). Данные сохраняются только в промежуточный staging и не влияют на индекс без явной проверки и публикации администратором.
|
Актуальная инвентаризация источников и правила подключения адаптеров находятся в [`docs/data-source-audit.md`](docs/data-source-audit.md). Разрешённый технический пилот RF4DB/RF4-STAT описан в [`docs/community-source-pilot.md`](docs/community-source-pilot.md), а статус разрешений и лимитов — в [`docs/data-permissions.md`](docs/data-permissions.md). Данные сохраняются только в промежуточный staging и не влияют на индекс без явной проверки и публикации администратором.
|
||||||
|
|||||||
+22
-1
@@ -1,8 +1,9 @@
|
|||||||
from pydantic import Field
|
from pydantic import Field, model_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
deployment_environment: str = "development"
|
||||||
database_url: str = "postgresql+psycopg://rf4:rf4_local@localhost:5432/rf4_spotter"
|
database_url: str = "postgresql+psycopg://rf4:rf4_local@localhost:5432/rf4_spotter"
|
||||||
admin_token: str = "change-me-in-production"
|
admin_token: str = "change-me-in-production"
|
||||||
s3_endpoint_url: str = "http://localhost:9000"
|
s3_endpoint_url: str = "http://localhost:9000"
|
||||||
@@ -18,7 +19,27 @@ class Settings(BaseSettings):
|
|||||||
import_interval_seconds: int = Field(default=3600, ge=3600)
|
import_interval_seconds: int = Field(default=3600, ge=3600)
|
||||||
rate_limit_secret: str = "change-rate-limit-secret"
|
rate_limit_secret: str = "change-rate-limit-secret"
|
||||||
log_level: str = "INFO"
|
log_level: str = "INFO"
|
||||||
|
cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:4321", "http://127.0.0.1:4321"])
|
||||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def reject_insecure_production_defaults(self) -> "Settings":
|
||||||
|
if self.deployment_environment != "production":
|
||||||
|
return self
|
||||||
|
insecure = {
|
||||||
|
"ADMIN_TOKEN": self.admin_token == "change-me-in-production" or len(self.admin_token) < 32,
|
||||||
|
"RATE_LIMIT_SECRET": self.rate_limit_secret == "change-rate-limit-secret" or len(self.rate_limit_secret) < 32,
|
||||||
|
"S3_ACCESS_KEY": self.s3_access_key == "rf4-local" or len(self.s3_access_key) < 12,
|
||||||
|
"S3_SECRET_KEY": self.s3_secret_key == "rf4-local-secret" or len(self.s3_secret_key) < 32,
|
||||||
|
}
|
||||||
|
invalid = [name for name, failed in insecure.items() if failed]
|
||||||
|
if invalid:
|
||||||
|
raise ValueError(f"insecure production settings: {', '.join(invalid)}")
|
||||||
|
if not self.cors_origins or any(not origin.startswith("https://") for origin in self.cors_origins):
|
||||||
|
raise ValueError("production CORS_ORIGINS must contain only HTTPS origins")
|
||||||
|
if not self.s3_public_endpoint_url.startswith("https://"):
|
||||||
|
raise ValueError("production S3_PUBLIC_ENDPOINT_URL must use HTTPS")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ logger = logging.getLogger("rf4.api")
|
|||||||
app = FastAPI(title="RF4 Spotter API", version="0.1.0")
|
app = FastAPI(title="RF4 Spotter API", version="0.1.0")
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["http://localhost:4321", "http://127.0.0.1:4321"],
|
allow_origins=settings.cors_origins,
|
||||||
allow_methods=["GET", "POST", "PATCH", "DELETE"],
|
allow_methods=["GET", "POST", "PATCH", "DELETE"],
|
||||||
allow_headers=["Authorization", "Content-Type"],
|
allow_headers=["Authorization", "Content-Type"],
|
||||||
)
|
)
|
||||||
@@ -50,6 +50,11 @@ async def structured_request_log(request: Request, call_next):
|
|||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
status_code = response.status_code
|
status_code = response.status_code
|
||||||
response.headers["X-Request-ID"] = request_id
|
response.headers["X-Request-ID"] = request_id
|
||||||
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||||
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||||||
|
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
|
||||||
|
if settings.deployment_environment == "production":
|
||||||
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
||||||
return response
|
return response
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("request failed", extra={"request_id": request_id, "error_type": type(exc).__name__})
|
logger.error("request failed", extra={"request_id": request_id, "error_type": type(exc).__name__})
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
def production_settings(**changes) -> Settings:
|
||||||
|
values = {
|
||||||
|
"deployment_environment": "production",
|
||||||
|
"admin_token": "a" * 32,
|
||||||
|
"rate_limit_secret": "r" * 32,
|
||||||
|
"s3_access_key": "access-key-1234",
|
||||||
|
"s3_secret_key": "s" * 32,
|
||||||
|
"s3_public_endpoint_url": "https://files.rf4spotter.ru",
|
||||||
|
"cors_origins": ["https://rf4spotter.ru"],
|
||||||
|
}
|
||||||
|
return Settings(**(values | changes))
|
||||||
|
|
||||||
|
|
||||||
|
def test_production_settings_accept_real_domains_and_secrets() -> None:
|
||||||
|
settings = production_settings()
|
||||||
|
|
||||||
|
assert settings.cors_origins == ["https://rf4spotter.ru"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("field", "value"), [
|
||||||
|
("admin_token", "change-me-in-production"),
|
||||||
|
("rate_limit_secret", "short"),
|
||||||
|
("s3_access_key", "rf4-local"),
|
||||||
|
("s3_secret_key", "rf4-local-secret"),
|
||||||
|
("cors_origins", ["http://rf4spotter.ru"]),
|
||||||
|
("s3_public_endpoint_url", "http://files.rf4spotter.ru"),
|
||||||
|
])
|
||||||
|
def test_production_settings_reject_insecure_values(field: str, value: object) -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
production_settings(**{field: value})
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
name: rf4-spotter
|
||||||
|
|
||||||
|
services:
|
||||||
|
proxy:
|
||||||
|
image: caddy:2.10.2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
SITE_DOMAIN: ${SITE_DOMAIN:?Set SITE_DOMAIN}
|
||||||
|
FILES_DOMAIN: ${FILES_DOMAIN:?Set FILES_DOMAIN}
|
||||||
|
ACME_EMAIL: ${ACME_EMAIL:?Set ACME_EMAIL}
|
||||||
|
ADMIN_BASIC_USER: ${ADMIN_BASIC_USER:?Set ADMIN_BASIC_USER}
|
||||||
|
ADMIN_BASIC_PASSWORD_HASH: ${ADMIN_BASIC_PASSWORD_HASH:?Set ADMIN_BASIC_PASSWORD_HASH}
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp"
|
||||||
|
volumes:
|
||||||
|
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
depends_on:
|
||||||
|
web:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [edge, backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits: {cpus: "0.50", memory: 256M}
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-rf4_spotter}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-rf4}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rf4} -d ${POSTGRES_DB:-rf4_spotter}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits: {cpus: "1.00", memory: 1G}
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:RELEASE.2025-07-23T15-54-02Z
|
||||||
|
command: server /data
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY}
|
||||||
|
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:?Set S3_SECRET_KEY}
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits: {cpus: "0.75", memory: 1G}
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/api/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DEPLOYMENT_ENVIRONMENT: production
|
||||||
|
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL}
|
||||||
|
ADMIN_TOKEN: ${ADMIN_TOKEN:?Set ADMIN_TOKEN}
|
||||||
|
CORS_ORIGINS: '["https://${SITE_DOMAIN:?Set SITE_DOMAIN}"]'
|
||||||
|
S3_ENDPOINT_URL: http://minio:9000
|
||||||
|
S3_PUBLIC_ENDPOINT_URL: https://${FILES_DOMAIN:?Set FILES_DOMAIN}
|
||||||
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY}
|
||||||
|
S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY}
|
||||||
|
S3_BUCKET: ${S3_BUCKET:-catch-screenshots}
|
||||||
|
OFFICIAL_RECORDS_URL: ${OFFICIAL_RECORDS_URL:-https://rf4game.de/records/region/RU/}
|
||||||
|
OFFICIAL_RECORDS_REGION: ${OFFICIAL_RECORDS_REGION:-RU}
|
||||||
|
OFFICIAL_RECORDS_CATEGORY: ${OFFICIAL_RECORDS_CATEGORY:-records}
|
||||||
|
OFFICIAL_IMPORT_REQUIRED: ${OFFICIAL_IMPORT_REQUIRED:-false}
|
||||||
|
IMPORT_INTERVAL_SECONDS: ${IMPORT_INTERVAL_SECONDS:-3600}
|
||||||
|
RATE_LIMIT_SECRET: ${RATE_LIMIT_SECRET:?Set RATE_LIMIT_SECRET}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
||||||
|
depends_on:
|
||||||
|
db: {condition: service_healthy}
|
||||||
|
minio: {condition: service_healthy}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready')"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
networks: [backend, edge]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits: {cpus: "1.00", memory: 1G}
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: ./apps/web
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PUBLIC_API_URL: https://${SITE_DOMAIN:?Set SITE_DOMAIN}
|
||||||
|
API_INTERNAL_URL: http://api:8000
|
||||||
|
depends_on:
|
||||||
|
api: {condition: service_healthy}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "fetch('http://localhost:4321').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits: {cpus: "0.75", memory: 512M}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
edge:
|
||||||
|
backend:
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
email {$ACME_EMAIL}
|
||||||
|
admin off
|
||||||
|
}
|
||||||
|
|
||||||
|
{$SITE_DOMAIN} {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
@admin path /admin/*
|
||||||
|
handle @admin {
|
||||||
|
basic_auth {
|
||||||
|
{$ADMIN_BASIC_USER} {$ADMIN_BASIC_PASSWORD_HASH}
|
||||||
|
}
|
||||||
|
reverse_proxy web:4321
|
||||||
|
}
|
||||||
|
|
||||||
|
@api path /api/* /health /ready
|
||||||
|
handle @api {
|
||||||
|
reverse_proxy api:8000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy web:4321
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
-Server
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{$FILES_DOMAIN} {
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy minio:9000
|
||||||
|
header {
|
||||||
|
-Server
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
Referrer-Policy "no-referrer"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Развёртывание закрытой альфы rf4spotter.ru
|
||||||
|
|
||||||
|
Production-контур рассчитан на один Linux-сервер с Docker Compose. Наружу публикуются только Caddy `80/443`; PostgreSQL, FastAPI и MinIO не имеют host-портов. Административные страницы защищены одновременно Caddy Basic Auth и API bearer token.
|
||||||
|
|
||||||
|
## 1. DNS и сервер
|
||||||
|
|
||||||
|
Создайте A-записи `rf4spotter.ru` и `files.rf4spotter.ru` на публичный IPv4 сервера. При наличии рабочего IPv6 добавьте AAAA для обоих имён. До запуска убедитесь, что извне доступны TCP 80/443 и UDP 443; SSH ограничьте своим IP или VPN. Порты 4321, 8000, 9000, 9001 и 5432 открывать нельзя.
|
||||||
|
|
||||||
|
Минимум для закрытой альфы: 2 vCPU, 4 ГБ RAM и 40 ГБ SSD. Рекомендуется 4 vCPU, 8 ГБ RAM и отдельное внешнее место для резервных копий.
|
||||||
|
|
||||||
|
## 2. Секреты
|
||||||
|
|
||||||
|
На сервере:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
chmod 600 .env.production
|
||||||
|
openssl rand -base64 36 # повторить для пароля БД, ADMIN_TOKEN, RATE_LIMIT_SECRET и S3_SECRET_KEY
|
||||||
|
docker run --rm caddy:2.10.2-alpine caddy hash-password --plaintext 'ОТДЕЛЬНЫЙ ADMIN-ПАРОЛЬ'
|
||||||
|
```
|
||||||
|
|
||||||
|
Заполните `.env.production`. Если пароль PostgreSQL содержит специальные символы, в `DATABASE_URL` нужна URL-кодированная форма того же пароля. `.env.production` нельзя коммитить или пересылать вместе с логами.
|
||||||
|
|
||||||
|
## 3. Проверка и первый запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml config --quiet
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml build
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml up -d
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml ps
|
||||||
|
curl -fsS https://rf4spotter.ru/health
|
||||||
|
curl -fsS https://rf4spotter.ru/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
API-контейнер перед стартом применяет Alembic-миграции и запускает seed. До альфы демонстрационные уловы следует убрать отдельной задачей; справочники должны остаться.
|
||||||
|
|
||||||
|
Проверка TLS и маршрутизации:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://rf4spotter.ru/
|
||||||
|
curl -I https://files.rf4spotter.ru/minio/health/live
|
||||||
|
```
|
||||||
|
|
||||||
|
MinIO health URL допустим для диагностики, но Console наружу не публикуется. Объекты доступны только по временным подписанным ссылкам.
|
||||||
|
|
||||||
|
## 4. Первичные данные
|
||||||
|
|
||||||
|
Официальный импорт запускается вручную после успешного readiness:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml exec api python -m app.cli import-records
|
||||||
|
```
|
||||||
|
|
||||||
|
Автоматический scheduler не входит в production-файл. RF4MAP и RF4 Posts нельзя опрашивать чаще одного раза в 30 минут; до отдельной эксплуатационной задачи используйте только контролируемые ручные запуски и staging.
|
||||||
|
|
||||||
|
## 5. Обновление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull --ff-only
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml build
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml up -d
|
||||||
|
docker compose --env-file .env.production -f compose.production.yaml ps
|
||||||
|
curl -fsS https://rf4spotter.ru/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
Перед обновлением со сменой схемы обязателен backup PostgreSQL. Не удаляйте volumes и не используйте `down -v`.
|
||||||
|
|
||||||
|
## 6. Что ещё блокирует приглашение альфа-пользователей
|
||||||
|
|
||||||
|
- проверенный backup/restore PostgreSQL и MinIO;
|
||||||
|
- production-safe seed без демонстрационных уловов;
|
||||||
|
- одноразовый секрет для повторной загрузки скриншота вместо одного UUID;
|
||||||
|
- проверка полного bootstrap на пустых volumes;
|
||||||
|
- политика хранения и удаления пользовательских данных;
|
||||||
|
- базовый мониторинг `/ready`, диска и срока TLS-сертификата.
|
||||||
|
|
||||||
|
До закрытия этих пунктов контур можно поднять для технической проверки домена, но не следует открывать форму реальным пользователям.
|
||||||
+1
-1
@@ -79,7 +79,7 @@
|
|||||||
- [ ] Добавить smoke-проверку административной очереди внешних источников на desktop/mobile без публикации реальных записей.
|
- [ ] Добавить smoke-проверку административной очереди внешних источников на desktop/mobile без публикации реальных записей.
|
||||||
- [ ] Обновить README: архитектура, все переменные окружения, импорт, модерация, backup/restore, эксплуатация логов и известные ограничения.
|
- [ ] Обновить README: архитектура, все переменные окружения, импорт, модерация, backup/restore, эксплуатация логов и известные ограничения.
|
||||||
- [ ] Выбрать лицензию кода и политику использования данных.
|
- [ ] Выбрать лицензию кода и политику использования данных.
|
||||||
- [ ] Определить production-профиль Compose/развёртывания: TLS/reverse proxy, домены, CORS, volumes, restart policy, resource limits и порядок обновления миграций.
|
- [ ] Завершить production-профиль для `rf4spotter.ru`: Compose, Caddy/TLS, закрытые внутренние сервисы, CORS, resource limits, fail-fast секреты и runbook добавлены; остаются backup/restore и проверка на целевом сервере.
|
||||||
- [ ] Заменить демонстрационные секреты и определить целевое размещение перед внешней публикацией.
|
- [ ] Заменить демонстрационные секреты и определить целевое размещение перед внешней публикацией.
|
||||||
|
|
||||||
## Источники данных и согласование
|
## Источники данных и согласование
|
||||||
|
|||||||
Reference in New Issue
Block a user