Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ POSTGRES_PASSWORD=flowix
POSTGRES_DB=flowix

# MinIO
# ⚠️ dev-only значения — обязательно заменить в prod (issue #55: дефолтные креды minioadmin запрещены,
# compose не поднимется без MINIO_ACCESS_KEY/MINIO_SECRET_KEY)
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_ACCESS_KEY=change-me-minio-access-key
MINIO_SECRET_KEY=change-me-minio-secret-key
MINIO_SECURE=false
VIDEO_STORAGE_BUCKET=videos

Expand All @@ -31,10 +33,9 @@ UPLOAD_MAX_BYTES=5368709120
# Transcoder (Phase 10) — limit CPU/RAM per job, sequential renditions
FFMPEG_THREADS=2
FFMPEG_PRESET=veryfast
# Phase 12: adaptive ladder + HW + pipe streaming
# Phase 12: adaptive ladder + HW
ENCODE_MODE=cbr
FFMPEG_HWACCEL=auto
TRANSCODER_PIPE_INPUT=true
# Phase 15 cost: delete raw/{id}/original.mp4 after ready unless true; lifecycle 7d via mc ilm is safety net
KEEP_RAW=false
# Phase 15: set DATABASE_URL to pgbouncer to enable pooled connections (transaction mode, statement_cache_size=0)
Expand All @@ -60,6 +61,10 @@ MINIO_PUBLIC_ENDPOINT=http://localhost:9000
MINIO_API_CORS_ALLOW_ORIGIN=*

# Redis — rate-limit storage (auth slowapi + gateway fixed-window, issue #52)
# issue #55: requirepass обязателен — compose не поднимется без REDIS_PASSWORD.
# Сервисы в compose получают redis://:$REDIS_PASSWORD@redis:6379/0 автоматически;
# для dev на хосте задай REDIS_URL=redis://:<REDIS_PASSWORD>@localhost:6379/0
REDIS_PASSWORD=change-me-redis-password
REDIS_URL=redis://redis:6379/0
REDIS_PORT=6379

Expand All @@ -72,16 +77,17 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000
#TRUSTED_PROXY_CIDRS=

# App ports
# issue #55: auth/metadata/upload/transcoder НЕ публикуются на хост — клиенты идут
# через gateway (:8080), Prometheus скрейпит по внутренней сети compose.
# Локальный dev: make dev-auth (порт 8001 фикс. в uvicorn), dev-metadata, dev-upload
GATEWAY_PORT=8080
AUTH_PORT=8001
METADATA_PORT=8002
UPLOAD_PORT=8003
# Issue #43: nginx-vod is internal-only (no published port) — HLS goes through gateway /hls/*
# NGINX_VOD_PORT=8081
FRONTEND_PORT=3000
PROMETHEUS_PORT=9090
GRAFANA_PORT=3001
GRAFANA_USER=admin
GRAFANA_PASSWORD=admin
# ⚠️ dev-only значения — обязательно заменить в prod (issue #55: дефолт admin/admin запрещён)
GRAFANA_USER=change-me-grafana-user
GRAFANA_PASSWORD=change-me-grafana-password
TRANSCODER_METRICS_PORT=8004
LOKI_PORT=3100
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
### Стек и структура (кратко)

- **Go (chi `go-chi/chi/v5`):** `services/gateway`, `services/metadata`, `services/upload` — `golang:1.27-alpine`, `pgx`, `zerolog`, `amqp091-go`. Версионирование `/api/v1`.
- **Python (FastAPI + Celery + uv):** `services/auth` (`src/main.py:1`), `services/transcoder` (`app/celery_app.py:1`, `app/tasks.py:1`) — `python:3.14-slim`, `uv`, `argon2`, `aiobotocore`, `ffmpeg-python`.
- **Python (FastAPI + pika + uv):** `services/auth` (`src/main.py:1`), `services/transcoder` (`app/consumer.py:1`) — `python:3.14-slim`, `uv`, `argon2`, `aiobotocore`, `ffmpeg-python`.
- **Infra:** `deploy/docker-compose.yml:1` — `postgres:16-alpine :5432`, `minio/minio :9000/:9001`, `rabbitmq:3-management :5672/:15672`, `nginx-vod :8081`, `gateway :8080`, `frontend :3000`.
- **Frontend:** `frontend/` — Next.js 14 App Router + `hls.js` + `zustand` + `tailwind`.
- **Transcoding JIT:** храним 3 MP4/рип (360/720/1080) с `-force_key_frames "expr:gte(t,n_forced*2)"` и `-sc_threshold 0` (`docs/spec.md:82`), nginx-vod режет на сегменты.
Expand All @@ -98,7 +98,7 @@ make swagger # swag init для metadata+upload
make e2e # scripts/e2e.sh — upload→transcode→hls
```

Локально без Docker: `make dev-auth` (`:8001`), `make dev-transcoder` (Celery), `make dev-metadata` (`:8002`), `make dev-upload` (`:8003`), `make dev-gateway` (`:8080`), `make dev-frontend` (`:3000`).
Локально без Docker: `make dev-auth` (`:8001`), `make dev-transcoder` (pika-воркер), `make dev-metadata` (`:8002`), `make dev-upload` (`:8003`), `make dev-gateway` (`:8080`), `make dev-frontend` (`:3000`).

### Кодстайл

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
### Стек и структура (кратко)

- **Go (chi `go-chi/chi/v5`):** `services/gateway`, `services/metadata`, `services/upload` — `golang:1.27-alpine`, `pgx`, `zerolog`, `amqp091-go`. Версионирование `/api/v1`.
- **Python (FastAPI + Celery + uv):** `services/auth` (`src/main.py:1`), `services/transcoder` (`app/celery_app.py:1`, `app/tasks.py:1`) — `python:3.14-slim`, `uv`, `argon2`, `aiobotocore`, `ffmpeg-python`.
- **Python (FastAPI + pika + uv):** `services/auth` (`src/main.py:1`), `services/transcoder` (`app/consumer.py:1`) — `python:3.14-slim`, `uv`, `argon2`, `aiobotocore`, `ffmpeg-python`.
- **Infra:** `deploy/docker-compose.yml:1` — `postgres:16-alpine :5432`, `minio/minio :9000/:9001`, `rabbitmq:3-management :5672/:15672`, `nginx-vod :8081`, `gateway :8080`, `frontend :3000`.
- **Frontend:** `frontend/` — Next.js 14 App Router + `hls.js` + `zustand` + `tailwind`.
- **Transcoding JIT:** храним 3 MP4/рип (360/720/1080) с `-force_key_frames "expr:gte(t,n_forced*2)"` и `-sc_threshold 0` (`docs/spec.md:82`), nginx-vod режет на сегменты.
Expand All @@ -98,7 +98,7 @@ make swagger # swag init для metadata+upload
make e2e # scripts/e2e.sh — upload→transcode→hls
```

Локально без Docker: `make dev-auth` (`:8001`), `make dev-transcoder` (Celery), `make dev-metadata` (`:8002`), `make dev-upload` (`:8003`), `make dev-gateway` (`:8080`), `make dev-frontend` (`:3000`).
Локально без Docker: `make dev-auth` (`:8001`), `make dev-transcoder` (pika-воркер), `make dev-metadata` (`:8002`), `make dev-upload` (`:8003`), `make dev-gateway` (`:8080`), `make dev-frontend` (`:3000`).

### Кодстайл

Expand Down
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ COMPOSE=docker compose --env-file .env -f deploy/docker-compose.yml

up:
$(COMPOSE) up --build -d
@echo "infra: postgres :5432, minio :9000/:9001, rabbit :5672/:15672, gateway :8080 (hls; nginx-vod internal), prometheus :9090, grafana :3001, loki :3100, transcoder :8004/metrics"
@echo "infra: postgres :5432 (loopback), minio :9000/:9001 (loopback), rabbit :5672/:15672 (loopback), redis :6379 (loopback, requirepass), gateway :8080, frontend :3000, prometheus :9090, grafana :3001, loki :3100 (loopback); auth/metadata/upload 8001-8003 не публикуются (issue #55) — e2e через gateway"

up-frontend:
$(COMPOSE) up --build -d frontend
Expand Down Expand Up @@ -100,8 +100,6 @@ dev-auth:

dev-transcoder:
uv run --project services/transcoder python -m app.consumer
dev-transcoder-celery: # legacy, Phase 10 — celery deprecated, use dev-transcoder (pika)
uv run --project services/transcoder celery -A app.celery_app worker --loglevel=info

dev-metadata:
set -a; . ./.env 2>/dev/null || true; export ENV=dev; cd services/metadata && go run ./cmd/server
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- **JIT packaging** — only 3 MP4s stored, HLS/DASH segmented on-the-fly by `nginx-vod` (`vod_mode mapped`, `vod_segment_duration 2000`)
- **Async pipeline** — `upload → RabbitMQ (video.uploaded) → pika + FFmpeg (sequential, -threads 2 -preset veryfast, heartbeat 600) → MinIO → metadata (ready) → HLS`
- **Secure & scalable** — `INTERNAL_TOKEN` for `/internal/*`, `MaxBytesReader 5GB` (`UPLOAD_MAX_BYTES`), `private` raw bucket (`renditions/thumbnails` download), owner attribution (`owner_email`)
- **Microservices** — Go (`chi`) for gateway/metadata/upload, Python (`FastAPI` + `pika`/`Celery` legacy) for auth/transcoder
- **Microservices** — Go (`chi`) for gateway/metadata/upload, Python (`FastAPI` + `pika`) for auth/transcoder
- **Modern frontend** — Next.js 14 App Router + `hls.js` + `zustand` + `tailwind`, `playbackRate 0.5–2×`, owner badges, delete (`DELETE /api/v1/videos/:id` owner only + S3 cleanup)
- **Production-ready infra** — Postgres 16 + `golang-migrate` (`deploy/migrations`), MinIO, RabbitMQ, Docker Compose, healthchecks, JWT (HS256, Argon2)

Expand All @@ -42,7 +42,7 @@
| Layer | Tech |
|-------|------|
| Gateway / Metadata / Upload | Go 1.27, `go-chi/chi/v5`, `pgx`, `zerolog`, `amqp091-go`, `minio-go` (metadata delete) |
| Auth / Transcoder | Python 3.14, FastAPI, `pika` (primary) / `Celery` legacy, `uv`, `argon2`, `minio`, `ffmpeg` (`-threads 2 -preset veryfast`, `heartbeat 600`) |
| Auth / Transcoder | Python 3.14, FastAPI, `pika`, `uv`, `argon2`, `minio`, `ffmpeg` (`-threads 2 -preset veryfast`, `heartbeat 600`) |
| Infra | Postgres 16 + `golang-migrate` (`deploy/migrations`), MinIO (private raw, download renditions), RabbitMQ 3, `nginx-vod-module` (`envsubst` INTERNAL_TOKEN), FFmpeg |
| Frontend | Next.js 14, `hls.js` 1.5 (`maxBufferLength 4`), `zustand` 4, Tailwind |
| Tooling | Docker Compose, `golangci-lint`, `black`/`ruff`/`mypy`, `eslint`/`prettier` |
Expand Down Expand Up @@ -93,7 +93,7 @@ uv sync --project services/transcoder

make migrate-up # golang-migrate up (if postgres is running without compose)
make dev-auth # uvicorn src.main:app --reload --port 8001
make dev-transcoder # python -m app.consumer (pika, heartbeat 600) — legacy: make dev-transcoder-celery
make dev-transcoder # python -m app.consumer (pika, heartbeat 600)
make dev-metadata # :8002 (needs MINIO_ENDPOINT, INTERNAL_TOKEN)
make dev-upload # :8003
make dev-gateway # :8080
Expand Down
1 change: 0 additions & 1 deletion deploy/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ services:
FFMPEG_PRESET: ${FFMPEG_PRESET:-veryfast}
ENCODE_MODE: ${ENCODE_MODE:-cbr}
FFMPEG_HWACCEL: ${FFMPEG_HWACCEL:-auto}
TRANSCODER_PIPE_INPUT: ${TRANSCODER_PIPE_INPUT:-true}
deploy:
resources:
limits:
Expand Down
58 changes: 31 additions & 27 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ services:
# Phase 15: pgbouncer uses md5, so postgres must store md5 hashes (scram breaks pgbouncer md5)
POSTGRES_INITDB_ARGS: "--auth-host=md5 --auth-local=md5"
POSTGRES_HOST_AUTH_METHOD: md5
# issue #55: postgres/pgbouncer/rabbit/redis/loki/prometheus/grafana публикуются
# только на loopback — dev-доступ с хоста, но не с внешних интерфейсов
ports:
- "${POSTGRES_PORT:-5432}:5432"
- "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
volumes:
- pgdata:/var/lib/postgresql/data
# Legacy fallback for fresh volume — primary migrations are golang-migrate (deploy/migrations)
Expand Down Expand Up @@ -39,7 +41,7 @@ services:
SERVER_RESET_QUERY: DISCARD ALL
IGNORE_STARTUP_PARAMETERS: extra_float_digits,search_path
ports:
- "${PGBOUNCER_PORT:-6432}:5432"
- "127.0.0.1:${PGBOUNCER_PORT:-6432}:5432"
depends_on:
postgres:
condition: service_healthy
Expand All @@ -63,8 +65,9 @@ services:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
# issue #55: дефолтные креды minioadmin запрещены — значения обязательны
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY is required — set it in .env (see .env.example)}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required — set it in .env (see .env.example)}
MINIO_API_CORS_ALLOW_ORIGIN: ${MINIO_API_CORS_ALLOW_ORIGIN:-*}
# Issue #43: loopback-only publication — reachable for browser presigned PUT/GET
# (MINIO_PUBLIC_ENDPOINT), not exposed on external interfaces. Bucket is fully private.
Expand Down Expand Up @@ -97,14 +100,20 @@ services:
exit 0
"
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY is required — set it in .env (see .env.example)}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required — set it in .env (see .env.example)}
VIDEO_STORAGE_BUCKET: ${VIDEO_STORAGE_BUCKET:-videos}

redis:
image: redis:7-alpine
# issue #55: Redis без requirepass на 0.0.0.0 — классический путь компрометации;
# пароль обязателен (compose упадёт без REDIS_PASSWORD), порт — только loopback
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD is required — set it in .env (see .env.example)}"]
environment:
# redis-cli в healthcheck аутентифицируется через REDISCLI_AUTH
REDISCLI_AUTH: ${REDIS_PASSWORD:?REDIS_PASSWORD is required — set it in .env (see .env.example)}
ports:
- "${REDIS_PORT:-6379}:6379"
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
volumes:
- redisdata:/data
healthcheck:
Expand All @@ -118,8 +127,8 @@ services:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-flowix}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-flowix}
ports:
- "${RABBITMQ_PORT:-5672}:5672"
- "${RABBITMQ_MGMT_PORT:-15672}:15672"
- "127.0.0.1:${RABBITMQ_PORT:-5672}:5672"
- "127.0.0.1:${RABBITMQ_MGMT_PORT:-15672}:15672"
volumes:
- rabbitdata:/var/lib/rabbitmq
healthcheck:
Expand Down Expand Up @@ -150,7 +159,7 @@ services:
build: ../services/auth
environment:
DATABASE_URL: postgres://flowix:flowix@postgres:5432/flowix?sslmode=disable
REDIS_URL: redis://redis:6379/0
REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required — set it in .env (see .env.example)}@redis:6379/0
# Phase 15: set PGBOUNCER_ENABLED=true to route via pgbouncer:5432 (pooled)
DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-5}
DATABASE_MAX_OVERFLOW: ${DATABASE_MAX_OVERFLOW:-10}
Expand All @@ -163,10 +172,9 @@ services:
# X-Real-IP от gateway (внутри docker-сети) — ключ auth-лимитера по
# реальному клиенту; см. services/auth/src/core/limiter.py
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-172.16.0.0/12}
ports:
# ревью фазы 17 (H1): auth не для внешнего доступа — клиенты идут через
# gateway (rate-limit + CORS); порт только для localhost (e2e/dev)
- "127.0.0.1:${AUTH_PORT:-8001}:8001"
# issue #55: auth/metadata/upload не публикуются — клиенты идут только через
# gateway (rate-limit + CORS); Prometheus скрейпит по внутренней сети,
# e2e ходит через gateway. Локальный dev: make dev-auth (:8001 на хосте)
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -200,8 +208,7 @@ services:
VIDEO_STORAGE_BUCKET: ${VIDEO_STORAGE_BUCKET:-videos}
# Issue #43: host for presigned thumbnail URLs handed to browsers (loopback-published MinIO)
MINIO_PUBLIC_ENDPOINT: ${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}
ports:
- "${METADATA_PORT:-8002}:8002"
# issue #55: порт не публикуется — доступ только через gateway (см. auth)
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -235,8 +242,7 @@ services:
MINIO_PUBLIC_ENDPOINT: ${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}
# grace для Shutdown меньше stop_grace_period (issue #63)
SHUTDOWN_GRACE: 4m
ports:
- "${UPLOAD_PORT:-8003}:8003"
# issue #55: порт не публикуется — доступ только через gateway (см. auth)
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -268,11 +274,9 @@ services:
FFMPEG_PRESET: ${FFMPEG_PRESET:-veryfast}
ENCODE_MODE: ${ENCODE_MODE:-cbr}
FFMPEG_HWACCEL: ${FFMPEG_HWACCEL:-auto}
TRANSCODER_PIPE_INPUT: ${TRANSCODER_PIPE_INPUT:-true}
KEEP_RAW: ${KEEP_RAW:-false}
METRICS_PORT: ${TRANSCODER_METRICS_PORT:-8004}
ports:
- "${TRANSCODER_METRICS_PORT:-8004}:8004"
# issue #55: метрики скрейпит Prometheus по внутренней сети — порт не публикуется
depends_on:
rabbitmq:
condition: service_healthy
Expand Down Expand Up @@ -309,7 +313,7 @@ services:
# Если появится LB — указать его CIDR в .env.
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000}
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-}
REDIS_URL: redis://redis:6379/0
REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required — set it in .env (see .env.example)}@redis:6379/0
ports:
- "${GATEWAY_PORT:-8080}:8080"
depends_on:
Expand Down Expand Up @@ -358,7 +362,7 @@ services:
- ./prometheus/alert.rules.yml:/etc/prometheus/alert.rules.yml:ro
- promdata:/prometheus
ports:
- "${PROMETHEUS_PORT:-9090}:9090"
- "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:9090/-/healthy"]
Expand All @@ -369,15 +373,15 @@ services:
grafana:
image: grafana/grafana:11.4.0
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:?GRAFANA_USER is required — set it in .env (see .env.example)}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:?GRAFANA_PASSWORD is required — set it in .env (see .env.example)}
GF_SERVER_HTTP_PORT: 3000
volumes:
- grafanadata:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/etc/grafana/dashboards:ro
ports:
- "${GRAFANA_PORT:-3001}:3000"
- "127.0.0.1:${GRAFANA_PORT:-3001}:3000"
depends_on:
prometheus:
condition: service_healthy
Expand All @@ -397,7 +401,7 @@ services:
- ./loki/loki.yml:/etc/loki/loki.yml:ro
- lokidata:/loki
ports:
- "${LOKI_PORT:-3100}:3100"
- "127.0.0.1:${LOKI_PORT:-3100}:3100"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3100/ready | grep -q ready || curl -f http://localhost:3100/ready"]
Expand Down
Loading
Loading