Deploy with Docker
Run the full Feeblo stack with docker compose: what each service does, first boot, and the upgrade path.
The docker-compose.yml at the repo root defines a complete single-host deployment: API
server, dashboard, PostgreSQL, and Redis. Both app containers come from prebuilt GHCR
images, so nothing builds locally. The server container runs database migrations before
it starts listening, which turns upgrades into a pull-and-restart.
Prerequisites
- Docker Engine and Docker Compose v2. Check yours with
docker compose version. - DNS records for three hosts: your dashboard URL, your API URL, and a wildcard record
(
*.example.com) covering board subdomains. - SMTP credentials. New accounts must click a verification link by default, so mail has to flow before anyone can register.
- A way to generate random keys (
opensslworks).
The services
| Service | Image | Host port (default) |
|---|---|---|
pg |
pgvector/pgvector:pg17 |
5432 |
redis |
redis:7-alpine |
6379 |
server |
ghcr.io/g3root/feeblo-server:$IMAGE_TAG |
8080 |
web |
ghcr.io/g3root/feeblo-web:$IMAGE_TAG |
4321 |
Details worth knowing before you boot:
- Healthchecks gate startup. Postgres reports ready via
pg_isready, Redis viaredis-cli ping. The server starts only after both pass; the web container waits for the server. - Migrations run on every server start. The container’s start command runs
node ./migrate/index.jsbefore launching the API, so schema changes apply themselves. - Data lives in named volumes called
postgresandredis. Deleting a volume deletes your data. - The server exposes
/health, returning{"status":"ok","release":"..."}once it listens.
First boot
Create your env file
Create .env next to docker-compose.yml:
# Required. Compose refuses to start without it.
# Generate with: openssl rand -hex 32
AUTH_ENCRYPTION_KEY=
# Public origins. These must match reality or sign-in breaks.
APP_URL=https://app.example.com # dashboard
API_URL=https://api.example.com # HTTP API
APP_ROOT_DOMAIN=example.com # boards live on <name>.example.com
# Outgoing email. See the Email page for every option.
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=your-username
SMTP_PASSWORD=your-password
SMTP_FROM_ADDRESS=noreply@example.comYou rarely need to set AUTH_TRUSTED_ORIGINS: compose derives a default of
$APP_URL,$API_URL,*.$APP_ROOT_DOMAIN, and the wildcard covers every board subdomain.
Add an override only when browsers call the API from extra origins; scheme-less patterns
match both http and https hosts.
Start the stack
docker compose up -dPostgres and Redis turn healthy first, the server runs migrations and comes up, then the dashboard follows. Watch progress with:
docker compose psVerify the API is alive
curl https://api.example.com/healthA JSON body with "status":"ok" means migrations finished and the server listens.
Create your account
Open APP_URL in a browser. Email and password sign-up is enabled by default
(AUTH_SIGN_UP_ENABLED), so register, click the verification link that arrives by email,
and sign in. GitHub and Google sign-in switch on as soon as you configure their client
credentials (see OAuth).
Upgrades
New app images arrive continuously; pin IMAGE_TAG to a specific GHCR tag instead of
latest so you decide when to move. To upgrade:
# Back up first
docker compose exec pg pg_dump -U feeblo feeblo > feeblo-backup.sql
# Pull new images and recreate containers; migrations run on server start
docker compose pull
docker compose up -d
To roll back, point IMAGE_TAG at the previous tag and run docker compose up -d
again. If the newer release migrated the schema past what the older image understands,
restore your backup into the postgres volume first.
Changing embedding dimensions
Post embeddings default to OpenAI text-embedding-3-small at 1536 dimensions. When you
switch to a model with a different vector size, reconfigure the database column from the
published server image before restarting it:
docker compose run --rm server \
node ./migrate/configure-embeddings.js \
--dimensions 768 \
--clear-existing
The command rebuilds the vector index for the new size. With --clear-existing it drops
vectors that no longer fit; without it, the command fails safely instead of losing data.