Self-Hosting Guide
basefyio is built to be self-hosted. The repository ships two Compose files: docker-compose.yml builds the applications from source and publishes every port on localhost (development, a single machine), and docker-compose.prod.yml runs pre-built images behind Traefik with Let's Encrypt certificates (production). This page covers both. The complete, step-by-step version lives in the repository: docs/guides/self-hosting.md (Turkish: docs/KURULUM.md).
What runs
| Service | Image | Purpose | Port (dev file) |
|---|---|---|---|
| postgres | pgvector/pgvector:pg16 (dev) · ghcr.io/<owner>/basefyio-postgres (prod, adds PostGIS) | The platform database and every project database | 5433 |
| keycloak | quay.io/keycloak/keycloak:24.0 | Identity: the dashboard realm and one realm per project | 8080 |
| redis | redis:7-alpine | Cache, one-time codes, background job queue | 6379 |
| minio | minio/minio | S3-compatible object storage | 9000 / 9001 (console) |
| pgbouncer | edoburu/pgbouncer | Connection pooling; the host customers connect their SQL tools to | 6432 |
| mongodb (prod) · nosql/couchdb (dev) | mongo:7 · couchbase/server:community-7.6.2 · apache/couchdb:3 | Document store behind the Data Engine; optional (DATA_ENGINE_PROVIDER) | 8091 / 5984 |
| traefik (prod) | traefik:v3.1 | Reverse proxy, TLS termination, HTTP→HTTPS redirect | 80 / 443 |
| platform-api | built from apps/platform-api/Dockerfile · ghcr.io/<owner>/basefyio-api | The API (NestJS) at /api; applies its own schema on start | 4000 |
| admin-ui | built from apps/admin-ui/Dockerfile · ghcr.io/<owner>/basefyio-ui | The dashboard (Next.js) | 3000 |
| website | built from apps/website/Dockerfile · ghcr.io/<owner>/basefyio-website | This site | 3002 |
Development: everything on one machine
git clone https://github.com/talyaglobal/basefyio.git basefyio
cd basefyio
cp .env.example .env
# Generate a value for every secret in .env. DB_CRED_ENC_KEY is required: the API refuses
# to store a project's database credential it cannot encrypt, so project creation fails without it.
openssl rand -base64 32 # run once per secret and paste the value
docker compose up -d --builddocker compose up -d --build also starts the Couchbase nosql service, which is heavy. If you do not need the document store, start the services you need by name:
docker compose up -d --build postgres keycloak redis minio pgbouncer platform-api admin-uiOpen http://localhost:3000/login and sign in with KEYCLOAK_ADMIN_USER / KEYCLOAK_ADMIN_PASSWORD from your .env (defaults admin / admin). The dashboard signs in against the Keycloak master realm, so the Keycloak admin is the first platform account. The API answers at http://localhost:4000/api/health.
Production: Traefik, TLS and pre-built images
1. DNS
Create A records for these names (replace example.com with your DOMAIN):
| Name | Service |
|---|---|
example.com | Website |
app.example.com | Dashboard (also admin.example.com for the admin console) |
api.example.com | Platform API |
auth.example.com | Keycloak |
storage.example.com | MinIO S3 API (public file URLs) |
minio-console.example.com | MinIO console |
db.example.com | PgBouncer, port 6432 (customers' SQL clients) |
traefik.example.com | Traefik dashboard (basic auth) |
Ports 80 and 443 must reach the host; Traefik obtains certificates with the HTTP challenge.
2. Environment
There is no .env.production in the repository (the pattern is git-ignored). Create it from the template and add the production-only variables the compose file interpolates:
cp .env.example .env.production
# Added for production (docker-compose.prod.yml reads them):
DOMAIN=example.com
ACME_EMAIL=ops@example.com
TRAEFIK_DASHBOARD_AUTH='admin:$apr1$...' # htpasswd line; quote it
GHCR_OWNER=talyaglobal # namespace of the images below
DATA_ENGINE_PROVIDER=mongodb # or disabled
NOSQL_USERNAME=basefyio
NOSQL_PASSWORD=<strong random>
NOSQL_CONNSTR=mongodb://basefyio:<password>@mongodb:27017/?replicaSet=rs0&authSource=admin
PGBOUNCER_ADMIN_PASSWORD=<strong random>
METRICS_TOKEN=<strong random> # optional: publishes /api/metrics
# From the template, now with real values: POSTGRES_PASSWORD, KEYCLOAK_ADMIN_PASSWORD,
# MINIO_ROOT_PASSWORD, DB_CRED_ENC_KEY, RESEND_* (mail), STRIPE_* (optional), OAuth client ids (optional)Any value containing <, >, &, | or spaces must be quoted, or a shell that sources the file stops reading at that line. MongoDB runs as a one-node replica set and needs a keyfile once: bash scripts/init-mongo-keyfile.sh writes secrets/mongo-keyfile.
3. Images
The production file uses image: and never builds. Either pull a release and tag it as the:latest the compose file expects, or build the same Dockerfiles yourself with those names:
# Option A — a signed release (see docs/guides/supply-chain.md to verify it)
for i in api ui website postgres; do
docker pull ghcr.io/talyaglobal/basefyio-$i:v2.9.0
docker tag ghcr.io/talyaglobal/basefyio-$i:v2.9.0 ghcr.io/$GHCR_OWNER/basefyio-$i:latest
done
# Option B — build locally
docker build -t ghcr.io/$GHCR_OWNER/basefyio-postgres:latest docker/postgres
docker build -t ghcr.io/$GHCR_OWNER/basefyio-api:latest -f apps/platform-api/Dockerfile .
docker build -t ghcr.io/$GHCR_OWNER/basefyio-ui:latest apps/admin-ui
docker build -t ghcr.io/$GHCR_OWNER/basefyio-website:latest --build-arg NEXT_PUBLIC_SITE_URL=https://example.com -f apps/website/Dockerfile .4. Start and verify
docker compose -f docker-compose.prod.yml --env-file .env.production up -d
docker compose -f docker-compose.prod.yml --env-file .env.production ps
curl -s https://api.example.com/api/health # {"status":"ok", ...}
open https://app.example.com/loginAlways pass -f docker-compose.prod.yml --env-file .env.production. A bare docker compose picks the development file, with different networks and default passwords.
Updating
# new images (pull + retag, or rebuild as above), then:
docker compose -f docker-compose.prod.yml --env-file .env.production up -dThe API applies its schema on every start (prisma db push), so there is no separate migration step. Compose recreates only the containers whose image or configuration changed; avoid recreating Keycloak for an application update, because that signs every dashboard user out.
Backups
- Project export — Dashboard → project → Backup & Export, or
POST /api/projects/:id/export, produces a ZIP with the database dump, realm users and storage objects. - Automatic daily backups — kept per project and restorable from the same page (
GET /api/projects/:id/backups). - Point-in-time recovery — continuous WAL archiving with a seven-day window; see POINT_IN_TIME_RECOVERY.md and
scripts/enable-pitr.sh.
Monitoring
GET /api/health— always 200; readstatus(okordegraded) andchecks.email.GET /api/metrics— Prometheus text, published only whenMETRICS_TOKENis set and sent as a bearer token.GET /api/v1/projects/:id/data-engine/health(anon key) — whether the document store is reachable.docker compose ps— every stateful service has a health check; the API waits for them at start.
Details: docs/guides/health-and-metrics.md.
Troubleshooting
Creating a project fails
The API log says DB_CRED_ENC_KEY is not set. Set it (32 random bytes, base64 or 64 hex characters) in .env / .env.production and recreate the API container. Keep the key safe: project database passwords are encrypted with it and cannot be recovered without it.
Keycloak does not start
It needs a healthy Postgres and its own keycloak database, created by scripts/init-db.sql on the first start of an empty data volume. Read docker compose logs keycloak --tail=50.
The API stays down
In production it waits for Postgres, Keycloak, Redis and MongoDB to report healthy. MongoDB's health check initiates the replica set on first start; give it 30 seconds and check docker compose logs mongodb --tail=50.
No certificate
Traefik needs port 80 reachable and the DNS names resolving to this host. Check docker compose logs traefik --tail=50.