---
title: Self-hosting Overview
description: "What running Feeblo yourself involves: the four services, what they require, and how traffic flows between them."
---

Self-hosted Feeblo is four containers: the API server, the dashboard web app, PostgreSQL,
and Redis. Both app containers ship as prebuilt images, so a working deployment needs a
compose file, a handful of environment variables, and a machine with Docker. Your data
stays in your database, under your backup schedule, behind your firewall.

## What you run

| Service  | Image                            | Job                                                                    |
| -------- | -------------------------------- | ---------------------------------------------------------------------- |
| `server` | `ghcr.io/g3root/feeblo-server`   | HTTP API: auth, dashboard RPC, widget endpoints, webhooks, email       |
| `web`    | `ghcr.io/g3root/feeblo-web`      | Astro app serving the dashboard and every public feature board         |
| `pg`     | `pgvector/pgvector:pg17`         | PostgreSQL 17 with the pgvector extension                              |
| `redis`  | `redis:7-alpine`                 | Shared store for rate limiting across server replicas                  |

Images publish to GHCR automatically on pushes to `main` and on version tags. Pin an
exact tag in production; `latest` moves whenever main moves.

## Requirements

**Runtime.** Docker Engine plus Docker Compose v2 covers everything in this section. If
you build from source instead of using the images, you need Node.js `^26.4.0` and pnpm
`^11.0.3`.

**Database.** Migrations run `CREATE EXTENSION IF NOT EXISTS vector` and add a
`vector(1536)` column to the `post` table, so your PostgreSQL instance must offer the
pgvector extension even if you never turn embeddings on. The compose stack satisfies this
with the `pgvector/pgvector:pg17` image. Bring-your-own databases work as long as the
extension does.

**Redis.** One server instance rate-limits in memory and runs fine without Redis. Once
you run more than one replica, point `REDIS_URL` at a shared instance so limits count
across all of them.

**Domain.** One root domain carries everything. The dashboard lives on the `app`
subdomain, the API on its own host, and each public board gets a subdomain of its own
(`feedback.yourcompany.com`, `acme.yourcompany.com`). `APP_ROOT_DOMAIN` names that root.

**Email.** Sign-up requires clicking a verification link by default
(`AUTH_EMAIL_VERIFICATION_REQUIRED` defaults to true), so SMTP must deliver mail before
anyone can finish creating an account. Set `SMTP_HOST`, `SMTP_PORT`, and
`SMTP_FROM_ADDRESS` before first boot.

**Secrets.** Compose refuses to start until `AUTH_ENCRYPTION_KEY` is set. This key
encrypts auth data and integration credentials at rest, and changing it later invalidates
existing sessions. Generate one with `openssl rand -hex 32`.

### Ports

Compose publishes four ports, each adjustable through `.env`:

| Variable        | Default | Serves                                    |
| --------------- | ------- | ----------------------------------------- |
| `SERVER_PORT`   | 8080    | API server                                |
| `WEB_PORT`      | 4321    | Dashboard and public boards               |
| `POSTGRES_PORT` | 5432    | PostgreSQL (drop the mapping on public hosts) |
| `REDIS_PORT`    | 6379    | Redis (same)                              |

The `server` container always listens on 8080 internally; the image pins the port so the
healthcheck and reverse proxies agree with the compose mapping. If host port 8080 is
already taken, edit the left side of the `ports` mapping in the compose file and leave
`SERVER_PORT` unset: compose feeds that variable into both sides, and setting it makes
the mapping point at a port nothing listens on.

## How the pieces connect

```text
browser ──▶ web :4321      dashboard on app.<root>, boards on <site>.<root>
browser ──▶ server :8080   sign-in callbacks, RPC calls from the dashboard,
                           widget requests, webhook payloads

web ──▶ server             server-side requests follow API_URL
server ──▶ pg              DATABASE_URL
server ──▶ redis           REDIS_URL (rate-limit counters)
server ──▶ outbound        SMTP for email, S3-compatible storage for media
```

Browsers reach `web` and `server` directly, which is why `APP_URL` and `API_URL` must be
real, reachable origins. The web container routes by hostname: any subdomain except `app`
renders the public board registered for that subdomain, and `app` (or no subdomain)
serves the dashboard. Because browsers hit both containers, cookies land correctly only
when both URLs share `APP_ROOT_DOMAIN`.

Startup order takes care of itself: `server` waits for `pg` and `redis` to pass their
healthchecks, and `web` waits for `server`. One `docker compose up -d` brings the stack
up in the right order.

## Next steps

<CardGroup cols={2}>
  <Card title="Deploy with Docker" href="/self-hosting/docker">
    The compose walkthrough, from env file to first account.
  </Card>
  <Card title="Environment variables" href="/self-hosting/environment">
    Every variable Feeblo reads, grouped and marked required or optional.
  </Card>
  <Card title="Database" href="/self-hosting/database">
    Postgres specifics and the migration lifecycle.
  </Card>
  <Card title="Email" href="/self-hosting/email">
    SMTP transports and provider webhooks.
  </Card>
</CardGroup>
