Basic Architecture of Sport Web

This commit is contained in:
Nông Đức Huy
2026-08-11 13:37:25 +07:00
commit 8032fff6ac
262 changed files with 20348 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
# ---------------------------------------------------------------------------
# Local development.
#
# Default (`pnpm infra:up`): backing services only — PostgreSQL, Redis, MinIO,
# Mailpit. The three applications run on the host via `pnpm dev`, because
# native file watching and HMR are dramatically faster than bind-mounted
# containers, especially on macOS.
#
# Full stack (`docker compose --profile full up`): everything containerised
# behind Nginx. This mirrors production topology and is what you use to verify
# routing, headers and container builds before shipping.
# ---------------------------------------------------------------------------
name: sport-store
services:
postgres:
image: postgres:17-alpine
container_name: sport-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-sport}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sport}
POSTGRES_DB: ${POSTGRES_DB:-sport_store}
# Deterministic collation: index ordering must not depend on the host.
POSTGRES_INITDB_ARGS: "--locale=C --encoding=UTF8"
ports:
- "${POSTGRES_PORT:-5433}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sport} -d ${POSTGRES_DB:-sport_store}"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
container_name: sport-redis
restart: unless-stopped
# Cache-first policy: evict rather than refuse writes. Nothing in Redis is
# a system of record, so eviction is always preferable to an outage.
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --appendonly no
ports:
- "${REDIS_PORT:-6380}:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
# Stands in for Cloudflare R2. Same S3 API, so application code is identical.
minio:
image: minio/minio:latest
container_name: sport-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY_ID:-sportminio}
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_ACCESS_KEY:-sportminio}
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 5s
retries: 10
minio-init:
image: minio/mc:latest
container_name: sport-minio-init
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 ${STORAGE_ACCESS_KEY_ID:-sportminio} ${STORAGE_SECRET_ACCESS_KEY:-sportminio};
mc mb --ignore-existing local/${STORAGE_BUCKET:-sport-media};
mc anonymous set download local/${STORAGE_BUCKET:-sport-media};
echo 'MinIO bucket ready';
"
# Catches every outgoing email locally. UI on http://localhost:8025
mailpit:
image: axllent/mailpit:latest
container_name: sport-mailpit
restart: unless-stopped
ports:
- "1025:1025"
- "8025:8025"
# ------------------------------------------------------------------------
# Application containers — `--profile full` only.
# ------------------------------------------------------------------------
api:
profiles: ["full"]
build:
context: .
dockerfile: infrastructure/docker/api.Dockerfile
container_name: sport-api
restart: unless-stopped
env_file: apps/api/.env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-sport}:${POSTGRES_PASSWORD:-sport}@postgres:5432/${POSTGRES_DB:-sport_store}?schema=public
REDIS_URL: redis://redis:6379
STORAGE_ENDPOINT: http://minio:9000
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
expose:
- "4000"
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:4000/api/v1/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
storefront:
profiles: ["full"]
build:
context: .
dockerfile: infrastructure/docker/storefront.Dockerfile
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost/api}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost}
container_name: sport-storefront
restart: unless-stopped
environment:
# Server-side rendering talks to the API container directly.
API_INTERNAL_URL: http://api:4000
depends_on:
api:
condition: service_healthy
expose:
- "3000"
admin:
profiles: ["full"]
build:
context: .
dockerfile: infrastructure/docker/admin.Dockerfile
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost/api}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_ADMIN_URL:-http://admin.localhost}
container_name: sport-admin
restart: unless-stopped
environment:
API_INTERNAL_URL: http://api:4000
depends_on:
api:
condition: service_healthy
expose:
- "3001"
nginx:
profiles: ["full"]
image: nginx:1.27-alpine
container_name: sport-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./infrastructure/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./infrastructure/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
- storefront
- admin
- api
volumes:
postgres-data:
redis-data:
minio-data: