Professional VPN subscription management and aggregation system
A production-ready FastAPI-based service for managing, aggregating, and serving VPN proxy subscriptions with multi-source support, intelligent caching, and comprehensive security features.
π Part of the V2Hub Ecosystem
This package is one component of V2Hub β see the full project overview, architecture, and all related repositories.
- Multi-Source Aggregation: Combine proxy configs from direct URIs, external subscription URLs, and internal references
- Per-Subscription Comments: Add custom comments to individual configs within each subscription
- Per-Source Visibility & Nesting Control: Hide individual sources from resolved output and limit how deep internal references are followed (
is_hidden,max_depth) - Recursive Resolution: Automatically resolve nested subscription references with circular detection
- Two-Tier Caching: Redis + PostgreSQL for optimal performance and reliability
- Background Refresh: Celery workers automatically update external sources every 15 minutes
- HMAC Signature Verification: Secure admin endpoints with SHA-256 request signing
- IP Whitelisting: Restrict admin access to authorized IP addresses
- Rate Limiting: Configurable per-endpoint rate limits with Redis-backed counters
- Ban System: Automatic and manual IP banning with time-based expiration
- Security Headers: HSTS, CSP, and other production-ready security headers
- Async Architecture: Full async/await support with SQLAlchemy 2.0 and asyncpg
- Connection Pooling: Optimized database connection management
- Graceful Degradation: Continues operation even if Redis is unavailable
- Health Checks: Built-in health monitoring for all services
- Production-Ready: Docker Compose setup with Nginx reverse proxy
- Quick Start
- Architecture
- Installation
- Configuration
- API Documentation
- Usage Examples
- Monitoring
- Development
- Deployment
- Contributing
- License
- Docker & Docker Compose
- Python 3.11+ (for local development)
- PostgreSQL 16+ (if running without Docker)
- Redis 7+ (if running without Docker)
# Clone the repository
git clone https://github.com/nestthub/v2hub-api.git
cd v2hub-api
# Create environment file
cp .env.example .env
# Edit .env with your configuration
# Start all services
docker-compose up -d
# Check service health
docker-compose ps
# View logs
docker-compose logs -f apiThe API will be available at http://localhost (Nginx) or http://localhost:8000 (direct API).
# Install dependencies
pip install -e .
# Run database migrations
alembic upgrade head
# Start the API server
uvicorn v2hub_api.main:app --reload --host 0.0.0.0 --port 8000
# Start Celery worker (in another terminal)
celery -A worker.celery_app worker --loglevel=info
# Start Celery beat scheduler (in another terminal)
celery -A worker.celery_app beat --loglevel=infoβββββββββββββββ
β Nginx β β Reverse proxy, SSL termination, /grafana/ proxy
ββββββββ¬βββββββ
β
ββββββββΌβββββββ
β FastAPI β β Main API application
β (Uvicorn) β
ββββββββ¬βββββββ
β
βββββββββββββββββββ¬ββββββββββββββββββ
β β β
ββββββββΌβββββββ ββββββββΌβββββββ βββββββΌββββββ
β PostgreSQL β β Redis β β Celery β
β (Data) β β (Cache) β β Workers β
βββββββββββββββ βββββββββββββββ βββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Monitoring Stack β
β Prometheus β Grafana β
β Docker logs β Alloy β Loki β Grafana β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
| Component | Technology | Purpose |
|---|---|---|
| API Framework | FastAPI 0.136+ | High-performance async web framework |
| Database | PostgreSQL 16 | Primary data storage |
| Cache | Redis 7 | Fast caching and rate limiting |
| Task Queue | Celery 5.6 | Background jobs and scheduling |
| ORM | SQLAlchemy 2.0 | Async database access |
| Validation | Pydantic 2.13 | Request/response validation |
| HTTP Client | aiohttp 3.13 | Async external URL fetching |
| Migrations | Alembic 1.18 | Database schema management |
| Web Server | Uvicorn 0.45 | ASGI server |
| Reverse Proxy | Nginx 1.27 | Load balancing, SSL |
| Metrics | Prometheus | Metrics collection and storage |
| Logs | Loki + Alloy | Log aggregation and shipping |
| Dashboards | Grafana | Visualization and alerting |
v2hub-api/
βββ src/
β βββ v2hub_api/
β βββ api/
β β βββ dependencies.py # FastAPI dependencies
β β βββ endpoints/
β β βββ public.py # Public endpoints (/sub/{token})
β β βββ subscriptions.py # Subscription CRUD
β β βββ admin.py # Admin management
β βββ core/
β β βββ config.py # Settings & configuration
β β βββ enums.py # Enumerations
β β βββ exceptions.py # Custom exceptions
β βββ db/
β β βββ models/ # SQLAlchemy models
β β βββ repositories/ # Data access layer
β β βββ session.py # Database connection
β βββ schemas/
β β βββ base_models/ # Pydantic models (user-facing)
β β βββ admin_models/ # Pydantic models (admin)
β βββ services/
β β βββ subscription_service.py # Business logic
β β βββ resolver_service.py # Subscription resolution
β β βββ cache_service.py # Redis caching
β β βββ user_service.py # User management
β β βββ ban_service.py # IP banning
β β βββ whitelist_service.py # IP whitelisting
β βββ middlewares/
β β βββ rate_limit_middleware.py # Rate limiting
β β βββ security_headers_middleware.py # Security headers
β βββ utils/
β β βββ config_parser.py # Proxy config parsing
β β βββ http_client.py # HTTP client wrapper
β β βββ rate_limiter.py # Redis-based rate limiting
β β βββ url_request_limiter.py # External URL fetch limiting
β β βββ url_validator.py # URL / SSRF validation
β βββ py.typed
β βββ main.py # Application entry point
βββ worker/
β βββ celery_app.py # Celery configuration
β βββ tasks/ # Background tasks
βββ alembic/
β βββ env.py
β βββ versions/ # Database migrations
βββ monitoring/
β βββ alloy/
β β βββ config.alloy # Grafana Alloy log pipeline
β βββ grafana/
β β βββ datasources.yml # Auto-provisioned datasources
β βββ prometheus.yml # Scrape config
β βββ loki.yml # Log storage config
βββ nginx/
β βββ nginx.conf
β βββ conf.d.templates/
β β βββ api.conf.template # Rendered via envsubst at container start
β βββ conf-test.d/
β β βββ api.conf # Domain-free config for local testing
β βββ entrypoint/
β β βββ 10-generate-limit-key-map.sh # Builds the rate-limit exemption map
β βββ grafana.htpasswd # Basic auth for /grafana/ (optional)
βββ certbot-renew/
β βββ Dockerfile # SSL certificate renewal
βββ .github/
β βββ workflows/
β βββ ci.yml # Lint, type-check, tests, Docker build
β βββ deploy.yml # SSH deploy + migrations + health check
βββ docs/
β βββ API_DOCUMENTATION.md # Complete API reference
β βββ TYPES.md # Type reference
β βββ index.html
βββ tests/ # Test suite
βββ serve_docs.py # Local docs server
βββ docker-compose.yml # Docker orchestration
βββ Dockerfile # Container definition
βββ pyproject.toml # Project dependencies
βββ alembic.ini # Alembic configuration
βββ README.md # This file
Client Request
β
βΌ
βββββββββββββββββββββββ
β Public Endpoint β /sub/{token}
ββββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Resolver Service β Recursive resolution engine
ββββββββββββ¬βββββββββββ
β
ββββ Check Redis Cache
β
ββββ Fetch from Database
β
ββββ Resolve INTERNAL_TOKEN references
β (recursive, with depth limit)
β
ββββ Fetch EXTERNAL_URL sources
β (with caching)
β
ββββ Aggregate CONFIG sources
β
βΌ
βββββββββββββββββββ
β Apply Comments β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Base64 Encode β
ββββββββββ¬βββββββββ
β
βΌ
Return to Client
-
Clone the repository:
git clone https://github.com/nestthub/v2hub-api.git cd v2hub-api -
Configure environment:
cp .env.example .env nano .env # Edit with your settings -
Start services:
docker-compose up -d
-
Run migrations:
docker-compose exec api alembic upgrade head -
Verify installation:
curl http://localhost/health # Expected: {"status":"ok"}
-
Clone and setup:
git clone https://github.com/nestthub/v2hub-api.git cd v2hub-api python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate
-
Install dependencies:
pip install -e . -
Setup database:
# Create PostgreSQL database createdb v2hub # Run migrations alembic upgrade head
-
Configure environment:
cp .env.example .env # Edit .env with local settings -
Start services:
# Terminal 1: API uvicorn v2hub_api.main:app --reload # Terminal 2: Celery Worker celery -A worker.celery_app worker --loglevel=info # Terminal 3: Celery Beat celery -A worker.celery_app beat --loglevel=info
Create a .env file in the project root:
# βββββββββββββββββββββββββββββ
# Core
# βββββββββββββββββββββββββββββ
APP_NAME=v2hub
APP_VERSION=1.0.0
ENVIRONMENT=production
DEBUG=false
DOMAIN=YOURDOMAIN.com
# Comma-separated IPs exempt from nginx rate limiting (optional, e.g.
# a monitoring probe or internal health-checker). Consumed by
# nginx/entrypoint/10-generate-limit-key-map.sh at container start.
TRUSTED_NOLIMIT_IPS=
# βββββββββββββββββββββββββββββ
# Server
# βββββββββββββββββββββββββββββ
HOST=0.0.0.0
PORT=8000
WORKERS=2
# βββββββββββββββββββββββββββββ
# Database (Docker service name)
# βββββββββββββββββββββββββββββ
POSTGRES_PASSWORD=STRONG_PASSWORD
DATABASE_URL=postgresql+asyncpg://postgres:STRONG_PASSWORD@postgres:5432/v2hub
DB_POOL_SIZE=20
DB_MAX_OVERFLOW=40
DB_POOL_TIMEOUT=30
DB_ECHO=false
# βββββββββββββββββββββββββββββ
# Redis
# βββββββββββββββββββββββββββββ
REDIS_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/1
REDIS_TTL=600
# βββββββββββββββββββββββββββββ
# Security
# βββββββββββββββββββββββββββββ
SECRET_KEY=STRONG_PASSWORD
ADMIN_SECRET_KEY=STRONG_PASSWORD
API_TOKEN_LENGTH=32
ADMIN_ALLOWED_IPS=["127.0.0.1"]
# βββββββββββββββββββββββββββββ
# Business rules
# βββββββββββββββββββββββββββββ
MAX_NESTING_DEPTH=3
MAX_SUBSCRIPTIONS_PER_USER=3
MAX_CONFIGS_PER_SUBSCRIPTION=150
MAX_SOURCES_PER_SUBSCRIPTION=150
# βββββββββββββββββββββββββββββ
# Fetching
# βββββββββββββββββββββββββββββ
FETCH_TIMEOUT=3
FETCH_USER_AGENT=v2hub/1.0.0
FETCH_MAX_REDIRECTS=0
# βββββββββββββββββββββββββββββ
# CORS
# βββββββββββββββββββββββββββββ
CORS_ORIGINS=["https://YOURDOMAIN.com","https://www.YOURDOMAIN.com"]
CORS_CREDENTIALS=true
CORS_METHODS=["GET","POST","PUT","PATCH","DELETE","OPTIONS"]
CORS_HEADERS=["*"]
# βββββββββββββββββββββββββββββ
# Logging
# βββββββββββββββββββββββββββββ
LOG_LEVEL=INFO
LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s| Category | Variable | Default | Description |
|---|---|---|---|
| Limits | MAX_SUBSCRIPTIONS_PER_USER |
3 | Max subscriptions per user |
MAX_SOURCES_PER_SUBSCRIPTION |
150 | Max sources per subscription | |
MAX_CONFIGS_PER_SUBSCRIPTION |
150 | Max resolved configs | |
MAX_NESTING_DEPTH |
3 | Max depth for nested references | |
| Performance | DB_POOL_SIZE |
10 | Database connection pool size |
REDIS_TTL |
600 | Cache TTL in seconds | |
FETCH_TIMEOUT |
3 | HTTP fetch timeout (seconds) | |
| Security | API_TOKEN_LENGTH |
16 | Length of generated tokens |
ADMIN_ALLOWED_IPS |
- | Comma-separated IP whitelist | |
| Rate Limits | PUBLIC_RPS |
3 | Requests/second for public endpoints |
INTERNAL_WITH_TOKEN_RPS |
3 | Requests/second for authenticated | |
TRUSTED_NOLIMIT_IPS |
- | Comma-separated IPs exempt from nginx rate limiting |
Comprehensive API documentation is available in docs/API_DOCUMENTATION.md, with additional type reference in docs/TYPES.md.
A static rendering of the docs can also be served locally:
python serve_docs.pyBase URL: https://api.example.com
GET /sub/{token}- Get resolved subscription (no auth required)
POST /api/v1/subs- Create subscriptionGET /api/v1/subs- List subscriptionsGET /api/v1/subs/{token}- Get subscription detailsPATCH /api/v1/subs/{token}- Update subscriptionDELETE /api/v1/subs/{token}- Delete subscriptionPOST /api/v1/subs/{token}/sources- Add sourcesPUT /api/v1/subs/{token}/sources- Replace sourcesDELETE /api/v1/subs/{token}/sources- Remove sourcesPATCH /api/v1/subs/{token}/config- Update config (comment,is_hidden,max_depth)PATCH /api/v1/subs/{token}/comments- Update config comment (deprecated, use/configabove)POST /api/v1/subs/{token}/refresh- Refresh external sources
POST /api/v1/admin/users- Create userGET /api/v1/admin/users/{user_id}- Get userDELETE /api/v1/admin/users/{user_id}- Delete userPATCH /api/v1/admin/users/{user_id}/status- Update user statusPOST /api/v1/admin/users/{user_id}/token/refresh- Refresh tokenPOST /api/v1/admin/bans- Ban IPPOST /api/v1/admin/unbans- Unban IPGET /api/v1/admin/bans- List banned IPsPOST /api/v1/admin/whitelist- Add to whitelistDELETE /api/v1/admin/whitelist- Remove from whitelistGET /api/v1/admin/whitelist- List whitelisted IPs
When running in debug mode (DEBUG=true), interactive API documentation is available at:
- Scalar UI:
http://localhost:8000/docs(Modern, dark-themed) - OpenAPI JSON:
http://localhost:8000/openapi.json
# Using curl with HMAC signature
timestamp=$(date +%s000)
body='{"user_id":12345}'
payload="${timestamp}POST/api/v1/admin/users${body}"
signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$ADMIN_SECRET" | awk '{print $2}')
curl -X POST http://localhost/api/v1/admin/users \
-H "Content-Type: application/json" \
-H "X-Signature: $signature" \
-H "X-Timestamp: $timestamp" \
-d "$body"Response:
{
"user_hash": "a1b2c3...",
"user_id": 12345,
"api_token": "12345:a1b2c3d4e5f6...",
"is_active": true
}curl -X POST http://localhost/api/v1/subs \
-H "API-Token: 12345:a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{
"name": "My Servers",
"description": "Personal collection",
"sources": [
"vless://uuid@server1.com:443?encryption=none&security=tls#Server1",
"vmess://base64config#Server2",
"https://provider.com/subscription"
]
}'Response:
{
"token": "abc123xyz",
"name": "My Servers",
"description": "Personal collection",
"sources": [...],
"sources_count": 15,
"created_at": "2026-04-27T10:00:00Z",
"updated_at": "2026-04-27T10:00:00Z"
}curl http://localhost/sub/abc123xyzThe response is base64-encoded configs ready to import into VPN clients.
import requests
class VPNClient:
def __init__(self, base_url, api_token):
self.base_url = base_url
self.headers = {
"API-Token": api_token,
"Content-Type": "application/json"
}
def create_subscription(self, name, sources):
response = requests.post(
f"{self.base_url}/api/v1/subs",
headers=self.headers,
json={"name": name, "sources": sources}
)
return response.json()
def add_sources(self, token, sources):
response = requests.post(
f"{self.base_url}/api/v1/subs/{token}/sources",
headers=self.headers,
json={"sources": sources}
)
return response.json()
# Usage
client = VPNClient("http://localhost", "12345:token...")
sub = client.create_subscription(
name="My VPN",
sources=["vless://...", "https://provider.com/sub"]
)
print(f"Created: {sub['token']}")
print(f"URL: http://localhost/sub/{sub['token']}")The monitoring stack runs as separate Docker services. All ports are internal β no monitoring service is exposed to the internet directly.
| Service | Image | Internal port | Description |
|---|---|---|---|
| Prometheus | prom/prometheus |
9090 | Scrapes /metrics from the API |
| Loki | grafana/loki |
3100 | Log storage, 7-day retention |
| Alloy | grafana/alloy |
12345 | Log collector (successor to Promtail) |
| Grafana | grafana/grafana |
3000 | Dashboards, exposed via nginx |
Grafana is proxied through nginx at /grafana/ and restricted by IP allowlist:
http://your-server/grafana/
Default credentials are configured via environment variables in docker-compose.yml:
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=your_password
GF_SERVER_ROOT_URL=http://your-server/grafana/
GF_SERVER_SERVE_FROM_SUB_PATH=trueGrafana's built-in login screen is used for authentication. The nginx IP allowlist acts as the outer perimeter β unauthorized IPs receive 403 before reaching Grafana at all.
An additional Basic Auth layer can be added in nginx. It requires a password file inside the nginx container.
Install htpasswd (if not available):
apt install apache2-utilsCreate the password file:
# First time β creates the file
htpasswd -c ./nginx/grafana.htpasswd admin
# Add or update a user
htpasswd ./nginx/grafana.htpasswd adminThe file is mounted into nginx as read-only:
volumes:
- ./nginx/grafana.htpasswd:/etc/nginx/grafana.htpasswd:roWarning: Basic Auth and Grafana's own login conflict over the
AuthorizationHTTP header. If you enable Basic Auth, Grafana may redirect to its login page and receive a 403 in return, creating a loop. It is recommended to use the IP allowlist only and skip Basic Auth.
The API exposes Prometheus metrics at /metrics. This endpoint is:
- blocked externally via nginx (
deny all) - scraped internally by Prometheus over the Docker network
Metrics exposed:
| Metric | Type | Labels |
|---|---|---|
fastapi_requests_total |
Counter | method, path, app_name |
fastapi_responses_total |
Counter | method, path, status_code, app_name |
fastapi_requests_duration_seconds |
Histogram | method, path, app_name |
fastapi_requests_in_progress |
Gauge | method, path, app_name |
fastapi_exceptions_total |
Counter | method, path, exception_type, app_name |
fastapi_app_info |
Info | app_name |
Grafana Alloy collects Docker container logs and ships them to Loki:
Docker containers β Alloy (discovery.docker) β Loki β Grafana
Logs are labeled with container_name and compose_service. Query examples in Grafana Explore:
# All services
{compose_service=~"v2hub_.+"}
# Errors only
{compose_service=~"v2hub_.+", level="error"}
# API logs only
{compose_service="v2hub_api"}
# Nginx logs, non-200
{compose_service="v2hub_nginx"} | status != "200"
Import dashboard 16110 from grafana.com/dashboards for a pre-built FastAPI observability view covering request rates, durations, error ratios, and live logs. The $app_name variable is auto-populated from the fastapi_app_info metric.
The Alloy UI (port 12345) is not exposed publicly. Access it via SSH tunnel:
ssh -L 12345:localhost:12345 user@your-server
# Then open http://localhost:12345# Check all monitoring services
docker compose ps prometheus loki alloy grafana
# Prometheus logs
docker compose logs -f prometheus
# Loki logs
docker compose logs -f loki
# Alloy logs
docker compose logs -f alloy
# Grafana logs
docker compose logs -f grafana
# Verify nginx can reach Grafana
docker compose exec nginx wget -q -O- http://grafana:3000/api/health
# Verify Prometheus can reach the API
docker compose exec prometheus wget -q -O- http://api:8000/metrics | head -20# Install test dependencies
pip install pytest pytest-asyncio
# Run all tests
pytest
# Run with coverage
pytest --cov=src --cov-report=html
# Run a specific test file
pytest tests/test_smoke.py
# Run with verbose output
pytest -v# Format code
black src/ tests/
# Lint
ruff check src/ tests/
# Type checking
mypy src/# Create new migration
alembic revision --autogenerate -m "Add new table"
# Apply migrations
alembic upgrade head
# Rollback one migration
alembic downgrade -1
# View migration history
alembic history# Install pre-commit
pip install pre-commit
# Setup hooks
pre-commit install
# Run manually
pre-commit run --all-files- Set
DEBUG=false - Use strong
SECRET_KEY,ADMIN_SECRET_KEY, andPOSTGRES_PASSWORD(min 32 chars) - Set
DOMAINto your real domain β it's used to render the nginx config and request the SSL certificate - Configure
ADMIN_ALLOWED_IPSwith specific IPs - Configure proper CORS origins (not
*) - Set appropriate rate limits for your use case (
nginx/conf.d.templates/api.conf.template) - Set a real
TRUSTED_NOLIMIT_IPSif you have monitoring/health-check IPs that shouldn't be rate-limited - Issue SSL certificates with certbot before first start (see below)
- Set up
nginx/grafana.htpasswdif the Grafana location is enabled - Configure backup strategy for PostgreSQL
- Setup Redis persistence if needed (already configured via
--save 60 1) - Review and adjust resource limits (pool sizes, timeouts) for your traffic
The project ships a single docker-compose.yml covering the full stack β no separate prod override file is needed:
| Service | Purpose |
|---|---|
api |
FastAPI app (Uvicorn), internal port 8000 |
worker |
Celery worker β background refresh of external sources |
beat |
Celery beat β schedules periodic tasks |
postgres |
PostgreSQL 16, data persisted to ./data/db_data |
redis |
Redis 7, AOF-style persistence via --save 60 1 |
nginx |
Reverse proxy, SSL termination, rate limiting |
certbot-renew |
Automatic Let's Encrypt certificate renewal (every 12h) |
Monitoring services (Prometheus, Loki, Alloy, Grafana) are present but commented out in docker-compose.yml β uncomment them if you want the full observability stack (see Monitoring).
# Start everything
docker compose up -d
# Start only what you need locally (e.g. no nginx/certbot for local dev)
docker compose up -d api worker beat postgres redisNginx configs live under nginx/conf.d.templates/*.conf.template and are not committed with real values baked in β they use placeholders (${DOMAIN}) that get filled in automatically at container start via nginx's built-in envsubst-on-templates entrypoint. You never need to hand-edit an nginx config file after cloning.
What you configure instead, in .env:
DOMAIN=YOURDOMAIN.com
# Comma-separated list of IPs exempt from rate limiting (optional)
TRUSTED_NOLIMIT_IPS=1.1.1.1,2.2.2.2On container start:
nginx/entrypoint/10-generate-limit-key-map.shreadsTRUSTED_NOLIMIT_IPSand generates/etc/nginx/conf.d/00-limit-key-map.conf(themap $remote_addr $limit_key { ... }block).- Nginx's built-in entrypoint renders every
*.templatefile innginx/conf.d.templates/into/etc/nginx/conf.d/, substituting${DOMAIN}with the real value from your.env. - Nginx starts with the fully rendered config.
To inspect what was generated inside a running container:
docker compose exec nginx cat /etc/nginx/conf.d/api.conf
docker compose exec nginx cat /etc/nginx/conf.d/00-limit-key-map.confThere's also nginx/conf-test.d/api.conf β a minimal, domain-free config (no SSL, server_name localhost) for quick local testing without certificates.
Before the HTTPS server block in api.conf.template will work, you need a real certificate at /etc/letsencrypt/live/${DOMAIN}/. A common bootstrap flow:
# 1. Start nginx with only the HTTP (port 80) block active, so certbot's
# HTTP-01 challenge can be served via /.well-known/acme-challenge/
docker compose up -d nginx
# 2. Request the certificate (webroot method, matches the acme-challenge
# location already present in api.conf.template)
docker compose run --rm certbot-renew \
certbot certonly --webroot -w /var/www/certbot \
-d ${DOMAIN} -d www.${DOMAIN} \
--email you@example.com --agree-tos --no-eff-email
# 3. Reload nginx to pick up the new certificate, then start everything else
docker compose exec nginx nginx -s reload
docker compose up -dAfter this, certbot-renew keeps the certificate current automatically (checks every 12 hours, reloads nginx via docker exec nginx nginx -s reload on renewal).
By default, Postgres data is stored via a bind mount at ./data/db_data. If you'd rather use the postgres_data named volume already declared in docker-compose.yml:
# 1. Dump the existing database (works regardless of Postgres version later)
docker compose exec -T postgres pg_dump -U postgres -d v2hub --format=custom --file=/tmp/v2hub.dump
docker compose cp postgres:/tmp/v2hub.dump ./v2hub.dump
# 2. Stop everything
docker compose down
# 3. In docker-compose.yml, change the postgres service's volume from
# ./data/db_data:/var/lib/postgresql/data
# to
# postgres_data:/var/lib/postgresql/data
# 4. Start only postgres β it will initialize a fresh, empty volume
docker compose up -d postgres
docker compose logs -f postgres # wait for "database system is ready to accept connections"
# 5. Restore the dump into the new volume
docker compose cp ./v2hub.dump postgres:/tmp/v2hub.dump
docker compose exec postgres pg_restore -U postgres -d v2hub --clean --if-exists /tmp/v2hub.dump
# 6. Bring everything back up
docker compose up -d
curl http://localhost/healthGitHub Actions workflows live under .github/workflows/:
ci.ymlβ runs on every push/PR tomain. Spins up Postgres and Redis service containers, lints withruff, type-checks withmypy, applies Alembic migrations, runs the test suite, and validates that the Docker image builds.deploy.ymlβ triggered automatically after a successful CI run onmain(or manually viaworkflow_dispatch). SSHes into the production server, pulls the latest code, rebuilds and recreates containers, runsalembic upgrade headinside theapicontainer, reloads nginx, and polls/healthuntil it returns200.
Required repository secrets for deployment: HOST, PORT, USER, SSH_KEY.
- API Tokens: Always transmitted over HTTPS
- Admin Endpoints: Dual protection (IP + signature)
- Token Rotation: Regularly refresh user tokens
- Inactive Accounts: Automatically disable unused accounts
- All inputs validated using Pydantic models
- URL validation prevents SSRF attacks
- Config parsing prevents injection attacks
- Length limits on all text fields
- Configurable per-endpoint limits
- Redis-backed for distributed deployments
- Automatic ban on excessive violations
- Whitelist support for trusted IPs
Automatically applied in production:
Strict-Transport-Security(HSTS)Content-Security-Policy(CSP)X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=block
- All monitoring ports (Prometheus :9090, Loki :3100, Alloy :12345, Grafana :3000) are internal only β declared with
expose, notports /metricsendpoint is blocked externally viadeny allin nginx- Grafana access is restricted by IP allowlist in nginx
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
- Follow PEP 8 style guide
- Write tests for new features
- Update documentation
- Add type hints to all functions
- Run linters before committing
- All PRs require review
- CI must pass (tests, linting)
- Documentation must be updated
- Changes must be backward compatible (unless major version)
This project is licensed under the MIT License - see the LICENSE file for details.
Built with:
- FastAPI - Modern web framework
- SQLAlchemy - SQL toolkit and ORM
- Pydantic - Data validation
- Celery - Distributed task queue
- Redis - In-memory data structure store
- PostgreSQL - Relational database
- Prometheus - Metrics and alerting
- Grafana - Observability dashboards
- Loki - Log aggregation
- Grafana Alloy - Telemetry collector
- Documentation: docs/API_DOCUMENTATION.md
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with β€οΈ