# reactbin _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 |