This is the recommended path for self-hosting — one container, your own Postgres, your own blob storage. Works on any Linux host, NAS, or Docker-based PaaS (Coolify, Dokploy, Portainer, etc.).
- A Linux host with Docker 20.10+
- A Postgres 17+ with the
vectorextension (for AI features) - Any S3-compatible blob storage (AWS S3, Cloudflare R2, MinIO, Alibaba OSS, Backblaze B2, …) if you want to allow file uploads on feedback posts. Leave unset to use ephemeral local filesystem storage.
Official multi-arch image (linux/amd64 + linux/arm64):
ghcr.io/linkcraftstudio/feedlog:latest
Tags:
latest— most recentmainbuildv0.1.0(and futurevX.Y.Z) — release versionssha-<short>— immutable per-commit tag, recommended for production pinning
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="admin@example.com" \
-e BETTER_AUTH_URL="http://localhost:3000" \
ghcr.io/linkcraftstudio/feedlog:latestOn first start, the container automatically runs database migrations against
DATABASE_URL before serving traffic. Expect a ~5-second delay on first boot
while the schema is created.
Visit http://localhost:3000 — the app is ready.
The repo ships a compose.yml that brings up the
app plus a pgvector-enabled Postgres. Three lines to start:
git clone https://github.com/linkcraftstudio/feedlog.git && cd feedlog
echo "BETTER_AUTH_SECRET=$(openssl rand -hex 32)" > .env
echo "SYSTEM_ADMIN_EMAILS=you@example.com" >> .env
docker compose up -dApp at http://localhost:3000. Optional variables (OAuth, AI,
transactional email) can be added to the same .env — compose.yml
passes them through to the app container.
The shipped compose.yml is intentionally minimal — single replica,
default Postgres password, port 3000:3000 exposed to the host. For
production behind a reverse proxy / TLS terminator, copy it to
compose.override.yml and adjust:
- Bind app to a private network only (drop the public
ports:mapping) - Rotate
POSTGRES_PASSWORDand updateDATABASE_URLto match - Add a backup sidecar or external Postgres
- Pin to a
sha-<short>image tag for reproducibility
See the configuration reference for the full
list grouped by purpose, or .env.example for the
annotated source. Quick reference:
| Variable | Required? | Purpose |
|---|---|---|
DATABASE_URL |
✅ | Postgres connection string |
BETTER_AUTH_SECRET |
✅ | 32+ char session encryption secret |
SYSTEM_ADMIN_EMAILS |
✅ | Comma-separated — these users get admin role on first sign-up. Without it the install has no admin. |
BETTER_AUTH_URL |
⬜ | Public URL of the app (no trailing slash). Usually inferred, but set it behind Host-rewriting proxies or when OAuth/email links need a fixed origin. |
S3_ACCESS_KEY_ID + S3_SECRET_ACCESS_KEY + S3_BUCKET (+ S3_ENDPOINT / S3_REGION) |
⬜ for persistent off-host uploads | Enables S3-compatible blob storage at runtime. See Enabling S3 storage. |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
⬜ | Enable Google OAuth |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
⬜ | Enable GitHub OAuth |
OPENAI_API_KEY + OPENAI_BASE_URL |
⬜ | Enable similar-idea merge and AI changelog drafting |
EMAIL_PROVIDER / RESEND_API_KEY / EMAIL_FROM |
⬜ | Transactional email |
The modules/blob-s3 Nuxt module registers an S3-compatible blob
provider when the relevant env vars are present. Works with the
official image — no rebuild needed.
Set in .env (or -e flags):
# Required (always 3)
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_BUCKET=feedlog
# Plus AT LEAST ONE of:
S3_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com # non-AWS services
S3_REGION=oss-cn-hangzhou # AWS or strict-region serviceFor providers that require virtual-hosted style, keep S3_BUCKET set
and use the bucket endpoint directly, for example
S3_ENDPOINT=https://<bucket>.oss-cn-hangzhou.aliyuncs.com.
Restart the container; on boot the plugin logs
Registered blob provider: s3 (bucket=...). Rotation is just an
.env edit + restart.
Service-specific endpoint / region pairs (Cloudflare R2, Alibaba OSS, Tencent COS, MinIO, Backblaze B2, Wasabi, Qiniu, ...) are listed in the configuration reference.
Without these vars, uploads fall through to NuxtHub's local-filesystem
driver (writes to /app/.data/blob). The bundled compose.yml mounts
an app-data named volume on /app/.data so local uploads survive
container restarts. Single-host only — no multi-replica or cross-host
sharing.
The container exposes a standard liveness probe at /health:
$ curl -s http://localhost:3000/health
{"status":"ok","timestamp":"2026-04-22T07:11:27.241Z"}Kubernetes liveness probe example:
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20Migrations run automatically on container start. To upgrade:
docker pull ghcr.io/linkcraftstudio/feedlog:latest
docker stop feedlog && docker rm feedlog
# ...re-run with the same env varsPin to a specific version tag (e.g. v0.1.0) in production to avoid
surprise updates.
Your Postgres image doesn't have pgvector. Use pgvector/pgvector:pg17 (or
later) instead of the stock postgres image, or install the extension manually:
CREATE EXTENSION IF NOT EXISTS vector;Check docker logs feedlog. Most common cause: DATABASE_URL points at a
host the container can't reach. If using docker-compose, use the service
name (db), not localhost.
Verify S3_ENDPOINT is reachable from inside the container (try
docker compose exec app wget -O- $S3_ENDPOINT). Also confirm the
bucket permits the PutObject and GetObject actions for the
credentials supplied. Check container logs for the
Registered blob provider: s3 (bucket=..., endpoint=...) line on
startup — if missing, one of S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY
/ S3_BUCKET is empty.
docker compose down -v # -v drops the Postgres volume
docker compose up -d- Cloudflare Workers deploy — serverless edge deploy
- Vercel deploy — managed Node.js deploy with zero config