diff --git a/README.md b/README.md index f793fde..023cea3 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,141 @@ _Organize your reaction images._ ![Reactbin UI](.img/reactbin-ui.png) + +A self-hosted reaction image board. Single owner account, tag-based browsing, S3-compatible image storage. + +--- + +## Local development + +```bash +cp .env.example .env +# Edit .env — defaults work out of the box for local dev +docker compose up +``` + +- UI: http://localhost:4200 +- API: http://localhost:8000 +- MinIO console: http://localhost:9001 (minioadmin / minioadmin) + +The API serves on port 8000 directly in dev. In production the nginx ingress routes `/api/` there. + +### Running tests + +```bash +make test-unit # pytest unit tests (no Docker) +make test-integration # builds api-test image, runs full suite against Postgres + MinIO +``` + +### Production image builds + +```bash +make build-prod # builds reactbin-api-prod:latest from api/Dockerfile.prod +make verify-prod # smoke-tests the production image +make build-ui-prod # builds reactbin-ui-prod:latest from ui/Dockerfile.prod +make verify-ui-prod # smoke-tests the production UI image +``` + +--- + +## Production deployment (k3s) + +### Cluster prerequisites + +- nginx ingress controller +- cert-manager with a `letsencrypt-prod` ClusterIssuer +- Vault Secrets Operator (VSO) installed and connected to Vault +- Vault KV v2 secrets populated (see below) + +### Vault secrets + +Two KV v2 paths. VSO syncs these into Kubernetes Secrets automatically. + +**`reactbin/api/config`** → K8s Secret `api-env` + +| Key | Notes | +|-----|-------| +| `DATABASE_URL` | `postgresql+asyncpg://user:pass@host:5432/db` | +| `JWT_SECRET_KEY` | Long random string — `openssl rand -base64 48` | +| `OWNER_USERNAME` | Login username | +| `OWNER_PASSWORD` | Login password | +| `S3_ENDPOINT_URL` | `http://minio.reactbin.svc.cluster.local:9000` | +| `S3_BUCKET_NAME` | `reactbin` | +| `S3_ACCESS_KEY_ID` | Same value as `MINIO_ROOT_USER` | +| `S3_SECRET_ACCESS_KEY` | Same value as `MINIO_ROOT_PASSWORD` | +| `API_BASE_URL` | `https://` | +| `LOGIN_TRUSTED_PROXY_IPS` | Pod CIDR of nginx ingress pods, e.g. `10.42.0.0/16` — needed for per-client login rate limiting behind the ingress | + +**`reactbin/minio/credentials`** → K8s Secret `minio-credentials` + +| Key | Notes | +|-----|-------| +| `MINIO_ROOT_USER` | MinIO admin username | +| `MINIO_ROOT_PASSWORD` | `openssl rand -base64 32` | + +### Apply order + +```bash +# 1. Namespace first +kubectl apply -f k8s/namespace.yaml + +# 2. Vault CRDs — wait for VSO to create api-env and minio-credentials Secrets +kubectl apply -f k8s/vault/ +kubectl get secret -n reactbin api-env minio-credentials # wait until both appear + +# 3. API, UI, Ingress — replace 'latest' tags and first +kubectl apply -f k8s/api/ -f k8s/ui/ -f k8s/ingress.yaml +kubectl rollout status deployment/api -n reactbin # Alembic init container runs here + +# 4. MinIO — wait for StatefulSet ready before running the bucket init Job +kubectl apply -f k8s/minio/service.yaml -f k8s/minio/statefulset.yaml +kubectl rollout status statefulset/minio -n reactbin +kubectl apply -f k8s/minio/init-job.yaml +``` + +Before applying: substitute real image tags in the Deployment manifests and replace `` in `k8s/ingress.yaml`. + +### Updating a secret + +1. Update the value in Vault +2. Force VSO to sync immediately (otherwise waits up to 1 hour): + ```bash + kubectl annotate vaultstaticsecret api-secret -n reactbin \ + secrets.hashicorp.com/force-sync=$(date +%s) --overwrite + ``` +3. Restart the deployment to pick up the new Secret: + ```bash + kubectl rollout restart deployment/api -n reactbin + ``` + +### Validating manifests + +```bash +make validate-k8s # yamllint + kubectl apply --dry-run=client (requires kubeconfig) +``` + +--- + +## Environment variables reference + +All variables are read at startup from environment / `.env`. + +| Variable | Default | Notes | +|----------|---------|-------| +| `DATABASE_URL` | — | Async DSN: `postgresql+asyncpg://...` | +| `JWT_SECRET_KEY` | — | Required; use a long random string in production | +| `JWT_EXPIRY_SECONDS` | `86400` | Token lifetime (24 h) | +| `OWNER_USERNAME` | — | Single owner account username | +| `OWNER_PASSWORD` | — | Single owner account password | +| `S3_ENDPOINT_URL` | — | MinIO or any S3-compatible endpoint | +| `S3_BUCKET_NAME` | `reactbin` | | +| `S3_ACCESS_KEY_ID` | — | | +| `S3_SECRET_ACCESS_KEY` | — | | +| `S3_REGION` | `us-east-1` | | +| `MAX_UPLOAD_BYTES` | `52428800` | 50 MiB | +| `API_BASE_URL` | — | Used for generating public URLs | +| `API_DOCS_ENABLED` | `true` | Set to `false` in production | +| `LOGIN_MAX_FAILURES` | `5` | Failed attempts before cooldown | +| `LOGIN_WINDOW_SECONDS` | `300` | Sliding window for failure count | +| `LOGIN_COOLDOWN_SECONDS` | `900` | Lock duration after threshold hit | +| `LOGIN_TRUSTED_PROXY_IPS` | `""` | Comma-separated CIDRs of trusted upstream proxies |