Self-hosting
Install, configure and maintain your own FeedLog instance — all three pages on one page.
Individual pages are the canonical, indexed versions — this aggregate is noindex so the two do not compete as duplicate content.
Self-hosting overview
FeedLog can be deployed with Docker, Vercel, or Cloudflare Workers. Each option runs the same application and supports the same product features. The platform determines the runtime environment, upload storage integration, and database migration timing.
Use this page to choose a platform and identify the minimum required configuration. Detailed deployment steps remain in the repository so they stay versioned with the release you deploy.
Before you start
FeedLog requires a PostgreSQL 17 or later database with the vector extension. File storage, transactional email, AI, and OAuth are optional. You can configure them later, although applying a configuration change may require a restart or redeployment, depending on the platform.
Similar-feedback detection uses the vector extension. Neon and Supabase let you enable it from their dashboards. For other providers, run the following statement against the database before deploying FeedLog:
CREATE EXTENSION IF NOT EXISTS vector;Without this extension, the database migration fails when it tries to create a table containing a vector column. The deployment reports this as a migration error.
Choose a platform
| Docker | Vercel | Cloudflare Workers | |
|---|---|---|---|
| Postgres | A database you provide; the bundled Compose file can start one | A database you provide, reachable over the internet | A database you provide, connected through a Hyperdrive binding |
| File uploads | S3-compatible storage or the local filesystem | Vercel Blob | R2 bucket |
| Migrations run | When the container starts | During the build | During initial setup on the first request |
| When to choose it | You want full infrastructure control | You want the least platform setup and per-branch previews | You want a serverless deployment on Cloudflare's network |
Docker is the recommended default for self-hosting. Choose it when the application, database, and uploaded files need to remain on infrastructure you control. It runs on Linux hosts, NAS devices, and Docker-based platforms such as Coolify and Dokploy. The repository includes a compose.yml that starts the application and a Postgres database with vector enabled, so Docker is the only option that does not require you to provision a separate database.
The bundled Compose file uses one replica, a default Postgres password, and a port published to the host. Review and change those settings before using it in production.
Vercel requires the least platform-specific setup. It detects the framework and provides HTTPS and preview deployments for each branch. You provide the Postgres database. Neon and Supabase are available through the Vercel Marketplace and both support the vector extension.
Cloudflare Workers runs FeedLog on Cloudflare's serverless network. It requires more platform-specific configuration: a POSTGRES Hyperdrive binding provides the database connection instead of DATABASE_URL, and an R2 binding provides file storage instead of S3 credentials.
The repository contains the complete instructions for each platform: Docker · Vercel · Cloudflare Workers
Required configuration
Docker and Vercel require three environment variables. On Cloudflare Workers, the POSTGRES Hyperdrive binding replaces DATABASE_URL; the other two values are Worker secrets.
| Variable | Purpose |
|---|---|
DATABASE_URL | PostgreSQL connection string. Do not set it on Cloudflare Workers; configure the POSTGRES Hyperdrive binding instead. |
BETTER_AUTH_SECRET | Protects sessions and cookies. Use at least 32 random characters. Changing it later invalidates all existing sessions. |
SYSTEM_ADMIN_EMAILS | Comma-separated addresses that become Owners of the default workspace when their accounts are created. Configure this before registering an account. |
docker run -d --name feedlog -p 3000:3000 \
-e DATABASE_URL="postgresql://user:password@host:5432/feedlog" \
-e BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
-e SYSTEM_ADMIN_EMAILS="[email protected]" \
ghcr.io/linkcraftstudio/feedlog:latestvercel env add DATABASE_URL
vercel env add BETTER_AUTH_SECRET # Generate with: openssl rand -hex 32
vercel env add SYSTEM_ADMIN_EMAILS# Bind Hyperdrive as POSTGRES in wrangler.toml; DATABASE_URL is not used.
wrangler secret put BETTER_AUTH_SECRET # Generate with: openssl rand -hex 32
wrangler secret put SYSTEM_ADMIN_EMAILSWith the default authentication settings, FeedLog automatically enables email and password sign-in when no OAuth provider is configured.
Set SYSTEM_ADMIN_EMAILS before registering
FeedLog checks SYSTEM_ADMIN_EMAILS when it creates an account. If the new account's address is on the list, FeedLog adds that account to the default workspace as an Owner.
Adding an address after its account already exists does not change that account's role. Restarting the application does not repeat the check.
If the account already exists
You do not need to edit the database. Add a different address to SYSTEM_ADMIN_EMAILS, apply the configuration change for your platform, and register a new account with that address. The address must not have been used to register on this instance before. Every listed address receives Owner access when its account is created, not only the first address.
When migrations run
Each platform runs database migrations at a different stage. This determines where a migration failure appears:
- Docker: Migrations run when the container starts. Initial setup normally adds about five seconds. If a migration fails, the container does not finish starting; check its logs for the migration error.
- Vercel: Migrations run during the build, before the Nuxt build step. A migration failure stops the deployment, while the previous deployment continues serving traffic.
- Cloudflare Workers: For a new database, the first request opens
/setup. That page runs the migrations and redirects to the application when the schema is ready. Thevectorextension must already exist because/setupcannot install it.
For migration behavior during later releases, see Upgrading and backups.
What to configure next
Before inviting users, review the optional services and prepare an upgrade and backup process.
Configuring external services explains OAuth, transactional email, file storage, and AI, including the behavior when each service is not configured. Without an email provider, password-reset and status notification messages are not delivered to recipients.
Upgrading and backups explains how to deploy a new version, when migrations run, and what to back up first.
Configuring external services
FeedLog can use four optional external services: OAuth sign-in, transactional email, file storage, and a model endpoint. The instance runs without them, but related features may be disabled or use fallback behavior without producing a startup error. Each section explains what to configure, what happens when the service is not configured, and how to identify common failures. The full variable list is in the repository: docs/configuration.md.
Login providers
Google and GitHub are configured independently. A provider is enabled only when both variables in its pair are present:
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=The sign-in dialog hides providers that are not fully configured. If only one variable in a pair is present, its button does not appear.
If you skip it: email and password sign-in remains available by default.
Email and password sign-in is enabled automatically only when no OAuth provider is configured. If you add Google or GitHub to an existing instance, the password form disappears after the next restart. Set AUTH_EMAIL_ENABLED=true to keep both sign-in methods available.
Callback URLs
Register the callback with the provider exactly as FeedLog will send it:
https://feedback.yourdomain.com/api/auth/callback/google
https://feedback.yourdomain.com/api/auth/callback/githubThe scheme, host, port, and path must all match. Use the public origin through which users access FeedLog. A GitHub OAuth app accepts one callback URL, so a staging environment requires a separate app.
Behind a reverse proxy
By default, FeedLog infers its public origin from the request's Host header and uses X-Forwarded-Proto for the scheme when that header is present. X-Forwarded-Host is not consulted. The origin inferred from the first request after a restart is cached for the lifetime of the process.
This works when the proxy preserves the original host, as Caddy and Traefik do by default. A plain nginx proxy_pass replaces Host with the upstream address. FeedLog will then use that internal address in absolute URLs, including the OAuth redirect_uri, password reset and verification links, and links in notification emails. If a health check reaches the container directly before any public request, FeedLog may cache the container's internal origin.
Preserve the original host at the proxy (proxy_set_header Host $host;), or set the public URL explicitly:
BETTER_AUTH_URL=https://feedback.yourdomain.comDo not include a trailing slash. Setting this value explicitly is recommended for deployments behind a reverse proxy.
Common errors
redirect_uri_mismatch from Google, or "The redirect_uri is not associated with this application" from GitHub — the error page's URL contains the redirect_uri FeedLog sent. Register it verbatim. If it names an internal host or starts with http, fix the proxy, not the OAuth app.
Sign-in completes and lands on the wrong domain — FeedLog inferred the wrong public origin. Set BETTER_AUTH_URL.
The Google or GitHub button isn't there — only one variable in the provider's pair is set.
Password sign-in disappeared after adding OAuth — this is the default when an OAuth provider is configured. Set AUTH_EMAIL_ENABLED=true.
The app won't start after changing AUTH_EMAIL_ENABLED — startup validation fails if email sign-in is disabled and no OAuth provider is configured. An empty value counts as off, not as unset.
Transactional email
RESEND_API_KEY=
EMAIL_FROM=[email protected]
EMAIL_PROVIDER=resend # optional — resend is the only provider todayResend is currently the only implemented provider. EMAIL_PROVIDER selects a registered provider by name, so RESEND_API_KEY is sufficient to register Resend.
Set EMAIL_FROM to an address on a domain you have verified with Resend. Without it, mail is sent from Resend's shared onboarding address, which may only deliver to the account owner.
FeedLog sends these five types of email:
| Sent when | |
|---|---|
| Email verification | A new account is created |
| Password reset | Someone asks for one |
| Password added | A password is set on an account that signed up with OAuth |
| Workspace invitation | An admin invites a member |
| Notification | Feedback someone follows changes status, or gets an official reply |
Notifications go to people following the feedback: its author and anyone who upvoted it. Workspace owners, managers, and the person who caused the event are excluded. Status notifications are sent only when an admin chooses to announce the change; changing a status without announcing it sends no email. FeedLog does not send email for new feedback or new comments.
If you skip it: verification, password reset, and invitation email is not sent. Notifications are written to the log instead of being sent to recipients ([DEV EMAIL] to=… subject=…, followed by the link). No error is returned. Users who cannot sign in cannot recover access through password reset, and admins must copy invitation links manually from the Members page.
AUTH_EMAIL_VERIFY controls whether an email address must be verified before sign-in. When unset, verification is enabled if RESEND_API_KEY is set. Use true or false to override that default.
Common errors
Mail reaches you and nobody else — EMAIL_FROM isn't set, so the shared Resend onboarding sender is in use.
Nothing arrives and no error is shown — look for Registered email provider: resend in the startup log. If the registered provider is console, RESEND_API_KEY is not available to the process.
Signed up, then couldn't sign in — AUTH_EMAIL_VERIFY=true is set without a mail provider. The account exists, but its address cannot be verified because no verification link is sent or written to the log. Password reset is also unavailable.
Links in mail point at the wrong host — FeedLog inferred the wrong public origin from the Host header. Set BETTER_AUTH_URL. Invitation links fall back to http://localhost:3000 when this value is unavailable.
File storage
Feedback and changelog entries support image attachments up to 10 MB each. The storage backend and its configuration depend on the deployment platform; only Docker and Node deployments use the S3_* variables:
| Platform | Backend | What you set |
|---|---|---|
| Docker / Node | S3-compatible, or local disk | S3_* |
| Vercel | Vercel Blob | Connect a Blob store; the token is injected for you |
| Cloudflare Workers | R2 | Bind a bucket as BLOB in wrangler.toml |
Three variables are always required, plus one of two others:
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_BUCKET=feedlog
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # non-AWS services
S3_REGION=us-east-1 # AWS S3FeedLog supports S3-compatible services including R2, MinIO, Backblaze, Wasabi, Alibaba OSS, Tencent COS, and Qiniu. Credentials are read at runtime, so restart the application after rotating them; a rebuild is not required.
The bucket does not need to be public. Files are stored under <UPLOAD_PREFIX>/<workspace id>/ and served through FeedLog's /api/files/ route. UPLOAD_PREFIX defaults to uploads and can separate environments that share a bucket. Each file's full path is stored with it, so changing the prefix does not affect existing attachments.
If you skip it on Docker: uploads are written to /app/.data/blob inside the container. The files are removed with the container unless /app/.data is mounted on a volume; the bundled compose.yml includes this mount. Vercel and Workers do not provide a local-disk fallback, so uploads fail without a Blob store or an R2 binding.
Common errors
Uploads work, then vanish after a redeploy — the local-disk fallback is in use and /app/.data isn't on a volume.
S3 is configured but files still go to disk — the provider is registered only when S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and S3_BUCKET are all present. Missing variables do not produce an error. Confirm that the startup log contains Registered blob provider: s3.
S3 disabled: set S3_REGION (for AWS) or S3_ENDPOINT (for R2/MinIO/OSS) — the three required variables are set but neither of the other two is, and the driver can't build a hostname from that.
Uploads fail against Alibaba OSS — Alibaba OSS may require virtual-hosted style. Keep S3_BUCKET and point S3_ENDPOINT at the bucket host, https://<bucket>.oss-cn-hangzhou.aliyuncs.com.
AI features
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_TEXT_MODEL=OPENAI_API_KEY determines whether AI features are enabled. The other two variables have defaults and are not checked independently, so setting only the base URL does not enable these features.
Three features use the model endpoint, with different behavior when the key is not configured:
| Feature | Without a key |
|---|---|
| Duplicate detection and search | Falls back to trigram text matching |
| AI changelog drafting | Button still there; clicking it returns an error |
| Widget message triage | Message submission fails |
With a key, new feedback is embedded and matched by meaning, so "can't log in with Google" can match "SSO broken". Without a key, search uses PostgreSQL trigram distance over the stored text. It still returns the closest results, but matches wording rather than meaning and can miss duplicates phrased differently. The interface does not indicate which matching method is in use.
The changelog editor continues to show the AI draft button when the key is missing. After a moderator selects a style and starts generation, the dialog shows AI generation is not configured (OPENAI_API_KEY missing).
The feedback widget requires a model endpoint to turn a user's message into structured feedback. Without a key, message submission fails and the end user sees Could not send that. Please try again. Configure the key before enabling the widget.
Feedback created while AI was disabled has no embedding. FeedLog does not create one retroactively, so that feedback is excluded from meaning-based matching until you run the repository's scripts/backfill-search.ts backfill script.
Pointing at a model you run yourself
OPENAI_BASE_URL accepts any OpenAI-compatible endpoint, including a LiteLLM or vLLM gateway, Ollama, or a regional provider. If that endpoint runs inside your network, feedback text sent to the model remains inside the network.
The same base URL serves both chat completions and embeddings, so an Azure URL scoped to one deployment can support only one of those requests. FeedLog requests text-embedding-3-large embeddings with 768 dimensions, and the database column also requires 768 dimensions. If the endpoint cannot return that shape, duplicate detection uses trigram text matching instead. OPENAI_TEXT_MODEL selects only the chat model and does not affect embeddings.
Common errors
"AI generation is not configured" in the changelog editor — OPENAI_API_KEY is not available to the process. On Workers it is a secret (wrangler secret put OPENAI_API_KEY), not a [vars] entry.
Older feedback is missing from semantic duplicate suggestions after adding a key — it was created without an embedding. Run the backfill script.
A gateway 404s on one feature and works on the other — the base URL is scoped to a single deployment. Point it at a gateway that routes both /chat/completions and /embeddings.
Widget messages fail with only a generic error on screen — check the server log for the underlying error.
Upgrading and backups
An upgrade replaces the application code and may also change the database schema. You can restore the previous code by deploying the old image tag, but deploying an old image does not reverse schema changes.
Self-hosting overview explains when each platform runs migrations during a fresh installation. Upgrades use the same schedule, but must also preserve the data already stored in the database.
Back up before every upgrade
Back up Postgres. Boards, feedback, votes, comments, changelog entries, accounts, sessions and similarity embeddings are all stored there. Without a database backup, you cannot restore this data.
pg_dump --format=custom "$DATABASE_URL" > feedlog-$(date +%F).dumpYou can run this command while the application is running; no downtime is required. Test the backup by restoring it to a temporary database before using it as a recovery point.
Back up uploaded files separately. Their location depends on your storage configuration:
| Storage | Where files live | Risk during an upgrade |
|---|---|---|
S3-compatible (S3_* set), Cloudflare R2, or Vercel Blob | Your bucket or store | A deployment does not modify these files. Back them up separately. |
| Nothing configured (Docker) | Inside the container, under /app/.data | Removing the container removes the files unless this path is mounted as a volume. |
If no S3_* variables are set, uploads use a local-filesystem driver that writes inside the container. The bundled compose.yml mounts a named volume at /app/.data, so these files persist when the container is removed. If you used the one-line docker run command without adding -v, the files do not persist, and the docker rm step in a Docker upgrade removes them.
Keep a copy of your environment variables. In particular, preserve BETTER_AUTH_SECRET: changing it signs out all users and invalidates pending password-reset and email-verification links.
Upgrading
docker pull ghcr.io/linkcraftstudio/feedlog:v0.2.0
docker stop feedlog && docker rm feedlog
docker run -d --name feedlog -p 3000:3000 \
... same env vars as before ... \
ghcr.io/linkcraftstudio/feedlog:v0.2.0
docker logs -f feedlog # wait for "Database migration completed"
# With the bundled compose file: edit the image: tag, then
docker compose pull app && docker compose up -d appgit pull upstream main
git push # Vercel rebuilds; migrations run in the build stepgit pull upstream main
pnpm install
pnpm build:cf && pnpm deploy:cf
# then visit the site as a signed-in admin — see belowFull per-platform instructions stay in the repository, versioned with the release you're deploying: Docker · Vercel · Cloudflare Workers
Which image tag to deploy
| Tag | What it points at |
|---|---|
latest | The most recent build of main. Changes when a new build is published. |
v0.2.0 | A tagged release. Never changes. |
0.2 | The newest patch in that minor line. Changes when a new patch is published. |
sha-1a2b3c4 | One exact commit. Does not change. |
You can use latest while evaluating FeedLog. In production, use a vX.Y.Z or sha- tag. Otherwise, running docker compose up -d after an unrelated host restart may deploy a new version and run migrations unexpectedly. The bundled compose.yml therefore uses a version tag.
Migrations during an upgrade
Migrations use the same schedule as a fresh installation, but an upgrade must account for existing data if a migration fails.
- Docker — the container applies pending migrations at startup. A Postgres advisory lock serializes them, so two containers starting at once won't apply the same migration twice; the second waits and then finds no pending migrations. The lock does not prevent an old application version from running against the new schema, so stop the old version before starting the new one instead of replacing replicas one at a time. Wait for
Database migration completedin the logs before sending traffic to the new container./healthreturns 200 when the process is running, which may happen before migrations finish. - Vercel — migrations run during the build, before the Nuxt build step. A migration failure stops the deployment, and the previous deployment continues serving, so the failed schema change does not reach production. This requires
DATABASE_URLto be set for both the Production and Preview scopes, and your Postgres to be reachable from Vercel's build machines. - Cloudflare Workers — a Worker has no startup phase, so migrations run on request. An upgrade therefore requires one manual step, described below.
Cloudflare Workers requires an admin to confirm the upgrade
After pnpm deploy:cf, if the release contains migrations that haven't been applied, the Worker puts the site into setup mode: every page request redirects to /setup, and every /api/ request returns 503 with NOT_INITIALIZED. The /setup page then attempts to run the migration. Because this is an upgrade rather than a first installation, the endpoint requires a signed-in admin. Anonymous callers receive "Administrator required". This restriction prevents visitors from triggering schema changes.
In setup mode, the Worker redirects every request except /setup, /api/auth/**, /health and static assets. This includes the pages that contain the sign-in dialog. If you are not already signed in as an admin when the new Worker goes live, you cannot sign in through the interface.
Sign in before you deploy
Open your instance and confirm you're signed in as an admin before running pnpm deploy:cf. The session cookie remains valid after the deployment. When you next load a page, the Worker redirects you to /setup, runs the migration, and returns you to the original page. If you deploy before signing in, the site only serves /setup, which returns "Administrator required".
If /setup does not continue, request /api/_migrate/status directly. It reports state, the number of applied migrations and the number of expected migrations.
Rolling back
To roll back the application code, deploy the previous image tag, redeploy the previous Vercel deployment from the dashboard, or check out the old commit and run pnpm deploy:cf again.
FeedLog does not support rolling back the schema. Migrations are recorded in a tracking table and only move forward; FeedLog ships no down-migrations. Past releases have dropped indexes and unique constraints and tightened columns to NOT NULL, and an older build starting against that schema either misbehaves or refuses to start.
Before rolling back, determine which case applies:
- The new version has a bug and applied no migrations. Deploy the old tag. No database restore is required. This covers most rollbacks.
- The new version applied migrations. Deploy the old tag and restore the Postgres backup you took before the upgrade. Data written between the upgrade and the rollback is lost, so upgrade at a quiet hour.
On Docker and Vercel, the deployment log shows which migrations ran. On Cloudflare, check /api/_migrate/status after the deployment.
Restore the database, not just the image
Rolling the container back to a previous tag does not undo a migration. If the new version migrated your schema, you need the database backup taken before the upgrade.
Common errors
The container starts, immediately exits and repeatedly restarts. A migration failed. The Database migration failed line in docker logs feedlog contains the specific error. The schema is partly applied at this point. Fix the cause before restarting, or restore the backup and try the upgrade again.
Pages return 500 right after a container restart, then recover. The process starts listening while migrations are still running. Wait for Database migration completed before sending traffic to the container. A 200 response from /health does not mean the schema is ready.
The Vercel deploy failed on the migrate step. The previous deployment continues to serve traffic. Common causes: DATABASE_URL isn't checked for the Production scope, or the database isn't reachable from Vercel's build IPs (a self-hosted Postgres behind a VPN won't be).
/setup returns "Administrator required" and you cannot sign in. The Worker was deployed before an admin session was established. Use a browser that still has a valid admin session, then reload /setup.
Uploaded images 404 after a Docker upgrade. The instance used local filesystem storage without a volume on /app/.data, so docker rm removed the files. They cannot be recovered. Configure S3_* storage before the next upgrade. See Configuring external services.
Everyone was signed out after the upgrade. BETTER_AUTH_SECRET changed, usually because the new deployment generated a new value instead of reusing the old one. Restore the original value to restore the existing sessions.