YYayaw

Self-Hosting

Portable Docker runtime for running Yayaw without Vercel or Supabase storage.

Scope

The first supported self-host target is a single Docker deployment:

  • Next.js standalone server
  • Postgres
  • MinIO or another S3-compatible object store
  • Page AI database worker
  • Caddy or another reverse proxy that preserves the original host headers

This path proves portability. It does not replace Stripe, OpenAI, Resend, GitHub code access, or every deployment platform feature. Analytics can use self-hosted Umami or an external provider such as PostHog.

Files

  • Dockerfile
  • docker-compose.self-host.yml
  • .env.self-host.example
  • deploy/self-host/Caddyfile

next.config.ts uses output: "standalone" so the production image can run the traced Next.js server with node server.js.

Quick Start

cp .env.self-host.example .env.self-host
perl -0777 -i -pe "s/^BETTER_AUTH_SECRET=$/BETTER_AUTH_SECRET=$(openssl rand -hex 32)/m" .env.self-host
docker compose --env-file .env.self-host -f docker-compose.self-host.yml --profile setup run --rm migrate
docker compose --env-file .env.self-host -f docker-compose.self-host.yml up --build

Set BETTER_AUTH_SECRET before the first build. The Dockerfile refuses to build with an empty secret because Better Auth routes are evaluated during next build.

The setup profile runs drizzle-kit push to sync the current Drizzle schema into the self-host database, then runs the idempotent seed script for roles, feature flags, billing catalog, themes, and starter CMS content.

Open http://localhost:8080. MinIO is exposed on http://localhost:9000 for S3 API access, and its console is exposed on http://localhost:9001. Public asset URLs are served through Caddy on the app origin by default.

Runtime Environment

Important self-host values:

NEXT_PUBLIC_BASE_URL=http://localhost:8080
BETTER_AUTH_SECRET=<generate-a-long-random-secret>
DATABASE_URL=postgresql://yayaw:yayaw@postgres:5432/yayaw
STORAGE_PROVIDER=s3
STORAGE_MEDIA_BUCKET=media
STORAGE_PUBLIC_BASE_URL=http://localhost:8080
S3_ENDPOINT=http://minio:9000
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=yayaw
S3_SECRET_ACCESS_KEY=yayaw-minio-password
S3_FORCE_PATH_STYLE=true
PUBLIC_DOMAIN_PROVIDER=manual-dns
PAGE_AI_QUEUE_DRIVER=db-worker
DEPLOYMENT_PROVIDER=static
DEPLOYMENT_URL=http://localhost:8080
NEXT_PUBLIC_ANALYTICS_PROVIDER=none
NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=hybrid
NEXT_PUBLIC_UMAMI_HOST_URL=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
UMAMI_API_URL=
UMAMI_WEBSITE_ID=
UMAMI_API_TOKEN=
UMAMI_API_KEY=
UMAMI_USERNAME=
UMAMI_PASSWORD=
RESEND_API_KEY=
EMAIL_SENDER=team@yayaw.app
EMAIL_SUPPORT=support@yayaw.app
EMAIL_USERNAME=Yayaw Team

The EMAIL_* values let transactional email work before an admin can sign in. After setup, Admin > Site Settings > Email overrides them at runtime.

NEXT_PUBLIC_* values are baked into the browser bundle at build time. The compose commands above pass .env.self-host as a build env file so those values reach Docker build args. Build a new image when they change. BETTER_AUTH_SECRET is also passed to the Docker build so Next.js can evaluate Better Auth routes safely during next build; keep it present at runtime too. Other server-only values can be changed by updating the orchestrator secret or .env.self-host and restarting the affected containers.

Docker builds default NEXT_BUILD_WORKERS to 1 to keep Next.js static generation stable on small self-hosted machines. Increase it as a build-time environment variable or Docker build arg only after confirming the host has enough CPU and memory headroom.

Documentation pages render dynamically in self-hosted builds instead of being pre-generated during image builds. This keeps small Coolify or Docker hosts from spending most of their deployment budget on docs pages while still serving docs normally at runtime.

YAYAW_APP_IMAGE and YAYAW_WORKER_IMAGE control the Compose image tags used for the app and Bun worker targets. The migrate service intentionally uses the same worker image tag as the long-running worker so Docker and Coolify can reuse that build target across services.

docker-compose.coolify.yml is optimized for the managed Yayaw Coolify deployment path and expects prebuilt app and worker images to already exist on the Coolify server. GitHub Actions builds those images for linux/arm64, loads them onto the Mac mini VM, sets YAYAW_APP_IMAGE and YAYAW_WORKER_IMAGE through the Coolify API, and then triggers the compose deployment. Use docker-compose.self-host.yml for portable local or customer self-host installs that should build images directly from source.

In Coolify, those image tag variables must be available at build time as well as runtime because Compose resolves image: interpolation before services start.

Reverse Proxy

The proxy must preserve:

  • Host
  • X-Forwarded-Host
  • X-Forwarded-Proto
  • client IP forwarding headers

Organization public pages route by hostname, so losing the original Host breaks custom-domain resolution. Keep proxy buffering friendly to streaming responses and Next.js image optimization.

When STORAGE_PUBLIC_BASE_URL points to the app or CDN origin, route every public object bucket prefix to MinIO before the app fallback. The bundled Caddy file handles the built-in public buckets:

handle /media/* {
	reverse_proxy minio:9000
}

handle /organization-logos/* {
	reverse_proxy minio:9000
}

If STORAGE_PUBLIC_BASE_URL points directly to a dedicated object-storage or CDN origin, configure that origin to serve the same /<bucket>/<key> paths instead.

Storage

The compose stack starts MinIO and creates public media and organization-logos buckets. Uploaded media and thumbnails use S3-compatible operations; published pages use the stored public URLs.

Existing Supabase media URLs are not migrated by the self-host stack. Keep the old bucket reachable or plan a separate media migration.

Public Domains

Self-hosted domains use manual DNS verification:

  1. Add the hostname from organization settings or MCP.
  2. Publish the TXT challenge shown by the dashboard.
  3. Point the domain to the ingress with the configured CNAME or A records.
  4. Re-check or verify the domain.
  5. Configure TLS at the proxy.

Use PUBLIC_DOMAIN_CNAME_TARGET and PUBLIC_DOMAIN_IPV4_TARGETS to show the operator-facing routing hints. Use APP_MANAGED_HOSTS and RESERVED_PUBLIC_DOMAIN_SUFFIXES to prevent customers from claiming app-owned or reserved hosts.

Worker

Run one or more Page AI workers with:

bun run worker:page-ai

The worker polls Postgres for durable Page AI runs. Vercel Queues remain available on Vercel, but the self-host production default is db-worker.

Operations

  • Run the setup profile before first boot and after schema changes so the database schema is synchronized and seeds are repaired.
  • Persist Postgres, MinIO, Caddy data, and .next/cache volumes.
  • Back up Postgres and object storage together so media rows and objects stay in sync.
  • Keep NEXT_SERVER_ACTIONS_ENCRYPTION_KEY and a stable build ID strategy in a follow-up before scaling to multiple app instances.
  • Pick an analytics provider with NEXT_PUBLIC_ANALYTICS_PROVIDER. Supported values are posthog, umami, and none; Umami needs NEXT_PUBLIC_UMAMI_* for tracking and UMAMI_* API variables for dashboard data.
  • Choose an analytics capture mode with NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE. Use server when you want no browser analytics script; use hybrid when you want browser product analytics plus server-side billing conversions.
  • Keep Stripe, OpenAI, Resend, and GitHub provider risks documented until their provider contracts exist.

Production Checks

After a production deploy, verify the runtime configuration from the running app container rather than only from the orchestrator UI. The important self-host signals are:

  • STORAGE_PROVIDER=s3
  • STORAGE_PUBLIC_BASE_URL points at the public app, CDN, or object-storage origin that serves /<bucket>/<key> paths
  • S3_ENDPOINT is reachable from the app and worker containers
  • NEXT_PUBLIC_ANALYTICS_PROVIDER=umami when using self-hosted Umami
  • NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=server when no browser analytics script should be rendered

For media, upload a small object through the same S3 credentials used by the app, then fetch its public URL. The public URL should use STORAGE_PUBLIC_BASE_URL, for example /media/<key> when Caddy routes the bucket prefix to MinIO. If this storage check succeeds but the dashboard upload still fails, inspect the active organization, media permissions, plan quota, file type, and file size before changing storage providers.

For no-browser analytics, fetch a public page and confirm that the HTML does not contain the Umami script URL or data-website-id. Dashboard analytics still need Umami API credentials through UMAMI_API_TOKEN, the legacy alias UMAMI_API_KEY, or UMAMI_USERNAME/UMAMI_PASSWORD; server-side event capture uses Umami /api/send and does not require the browser script.

Validation

docker compose --env-file .env.self-host -f docker-compose.self-host.yml config
bun test src/config/storage.config.test.ts src/lib/storage.test.ts
bun test src/lib/server/services/organization-domains/organization-public-domains.test.ts
bun test src/lib/server/services/pages/page-ai-runner.test.ts