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

ServiceImagePurposePort (dev file)
postgrespgvector/pgvector:pg16 (dev) · ghcr.io/<owner>/basefyio-postgres (prod, adds PostGIS)The platform database and every project database5433
keycloakquay.io/keycloak/keycloak:24.0Identity: the dashboard realm and one realm per project8080
redisredis:7-alpineCache, one-time codes, background job queue6379
miniominio/minioS3-compatible object storage9000 / 9001 (console)
pgbounceredoburu/pgbouncerConnection pooling; the host customers connect their SQL tools to6432
mongodb (prod) · nosql/couchdb (dev)mongo:7 · couchbase/server:community-7.6.2 · apache/couchdb:3Document store behind the Data Engine; optional (DATA_ENGINE_PROVIDER)8091 / 5984
traefik (prod)traefik:v3.1Reverse proxy, TLS termination, HTTP→HTTPS redirect80 / 443
platform-apibuilt from apps/platform-api/Dockerfile · ghcr.io/<owner>/basefyio-apiThe API (NestJS) at /api; applies its own schema on start4000
admin-uibuilt from apps/admin-ui/Dockerfile · ghcr.io/<owner>/basefyio-uiThe dashboard (Next.js)3000
websitebuilt from apps/website/Dockerfile · ghcr.io/<owner>/basefyio-websiteThis site3002

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 --build

docker 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-ui

Open 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):

NameService
example.comWebsite
app.example.comDashboard (also admin.example.com for the admin console)
api.example.comPlatform API
auth.example.comKeycloak
storage.example.comMinIO S3 API (public file URLs)
minio-console.example.comMinIO console
db.example.comPgBouncer, port 6432 (customers' SQL clients)
traefik.example.comTraefik 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/login

Always 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 -d

The 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; read status (ok or degraded) and checks.email.
  • GET /api/metrics — Prometheus text, published only when METRICS_TOKEN is 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.