d883b76c0d55be173dffeb0dfbd8d6789404c61c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reactbin
Organize your reaction images.
A self-hosted reaction image board. Single owner account, tag-based browsing, S3-compatible image storage.
Local development
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
make test-unit # pytest unit tests (no Docker)
make test-integration # builds api-test image, runs full suite against Postgres + MinIO
Production image builds
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-prodClusterIssuer - 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://<your-domain> |
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
# 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 <your-domain> 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 <your-domain> in k8s/ingress.yaml.
Updating a secret
- Update the value in Vault
- Force VSO to sync immediately (otherwise waits up to 1 hour):
kubectl annotate vaultstaticsecret api-secret -n reactbin \ secrets.hashicorp.com/force-sync=$(date +%s) --overwrite - Restart the deployment to pick up the new Secret:
kubectl rollout restart deployment/api -n reactbin
Validating manifests
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 |
Description
Languages
Python
32.7%
TypeScript
32.1%
Shell
26.6%
PowerShell
6.9%
JavaScript
0.5%
Other
1.2%
