Self-Hosting
Layout is fully open source and designed to run on your own infrastructure. This guide covers everything you need to deploy it, from environment variables to Docker builds to platform-specific notes.
Prerequisites
Before deploying, make sure you have the following:
Clone & Configure
Clone the repository and create your local environment file:
git clone https://github.com/uselayout/app.git
cd app
cp .env.example .env.localOpen .env.local and fill in the values below. Variables marked as required must be set before the app will start.
| Variable | Required | Description |
|---|---|---|
| ANTHROPIC_API_KEY | Yes | Anthropic API key for Claude, used for layout.md generation and the test panel. |
| BETTER_AUTH_SECRET | Yes | Auth secret used to sign sessions. Generate with: openssl rand -base64 32 |
| DATABASE_URL | Yes | PostgreSQL connection string for Better Auth user tables. |
| NEXT_PUBLIC_SUPABASE_URL | Yes | Supabase project URL, e.g. https://your-project.supabase.co |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes | Supabase anonymous (public) key for client-side project data access. |
| NEXT_PUBLIC_APP_URL | No | Full URL of your deployment. Defaults to http://localhost:3000 if unset. |
| BETTER_AUTH_URL | No | Should match NEXT_PUBLIC_APP_URL. Used by Better Auth for redirect URLs. |
| FIGMA_DEFAULT_TOKEN | No | Figma Personal Access Token for extraction. Users can also provide their own in-app. |
| STRIPE_SECRET_KEY | No | Only needed if you want to enable the billing / Pro tier features. |
| STRIPE_WEBHOOK_SECRET | No | Stripe webhook signing secret. Only needed for billing. |
| NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | No | Stripe publishable key for client-side checkout. Only needed for billing. |
Database Setup
Better Auth (PostgreSQL)
Better Auth manages user accounts and sessions. It creates its own tables on first run, with no migration command needed. The four tables it creates are:
layout_user
layout_session
layout_account
layout_verification
Your DATABASE_URL must point to a PostgreSQL database that the app has permission to create tables in. A fresh database is fine, and no manual schema setup is required.
Supabase (project data)
Supabase stores extraction results and layout.md content per project. You need a projects table. Run the migration from supabase/migrations/ in the repo, or apply it manually via the Supabase SQL editor.
ssl option to your connection config. Self-hosted Supabase does not use SSL by default. The standard connection string without SSL options is correct.Docker Deployment
The recommended way to deploy is via Docker. The included Dockerfile is a multi-stage build: deps installs node modules, builder runs next build, and runner produces the final slim image with Playwright Chromium pre-installed.
NEXT_PUBLIC_* variables are baked into the client JavaScript bundle at build time. They must be passed as --build-arg values during docker build, not set as runtime environment variables. All other variables (API keys, secrets) are runtime only and should be passed via --env-file.docker build \
--build-arg NEXT_PUBLIC_SUPABASE_URL=https://your-supabase.example.com \
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \
--build-arg NEXT_PUBLIC_APP_URL=https://studio.yourdomain.com \
-t layout-studio .
docker run -d \
--env-file .env.local \
-p 3000:3000 \
layout-studioThe container exposes port 3000. Put Nginx, Caddy, or a load balancer in front of it to handle TLS termination.
Platform Notes
Coolify
Connect your GitHub repo and set the branch to main. Coolify auto-detects the Dockerfile. In the Environment Variables section, add the NEXT_PUBLIC_* vars and enable the Build Arg toggle for each one. All other vars can be added as regular runtime environment variables.
Railway
Connect the GitHub repo. Railway detects the Dockerfile automatically. Add all environment variables in the Variables tab. The NEXT_PUBLIC_* vars must be set before the first deploy so they are available during the build stage.
VPS (docker-compose)
For a self-managed VPS, use a docker-compose.yml that passes the build args from your .env.local file:
services:
studio:
build:
context: .
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
env_file: .env.local
ports:
- "3000:3000"
restart: unless-stoppedRun docker-compose up -d --build from the repo root. Docker Compose reads .env.local automatically for variable substitution in the args block.
Playwright Setup
Playwright is required for website extraction only. Figma extraction, layout.md generation, and the test panel all work without it.
The Dockerfile already handles Playwright installation in the runner stage:
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers
RUN npx playwright install --with-deps chromiumIf you are running the app without Docker (e.g. for local development or a bare Node.js deployment), install Chromium manually and set the environment variable:
npx playwright install --with-deps chromium
export PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers--with-deps flag installs the required OS-level dependencies (libnss3, libatk, etc.) alongside Chromium. This is needed on headless Linux servers. On macOS for local development, you can omit it.Stripe (Optional)
Stripe integration is entirely optional. If you are running a personal or internal instance with no billing requirement, leave all STRIPE_* environment variables unset. The app starts and runs normally without them.
If you want to enable the billing / Pro tier:
- Create your products and prices in the Stripe Dashboard.
- Set
STRIPE_SECRET_KEYandNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYin your environment. - Create a webhook endpoint in the Stripe Dashboard pointing to
https://studio.yourdomain.com/api/webhooks/stripe, then setSTRIPE_WEBHOOK_SECRETto the signing secret.
stripe listen --forward-to localhost:3000/api/webhooks/stripe. This forwards live events to your local instance without needing a public URL.Verify Your Deployment
Once deployed, run through these checks to confirm everything is working:
Marketing page loads
Visit the root URL. You should see the marketing homepage, not an error or blank screen.
Auth is working
Visit /api/auth. Better Auth should return a JSON response. A 404 means the auth route is not registered.
Website extraction works
Sign in, click "Extract now", enter a URL, and start extraction. Progress events should stream in.
layout.md generates
After extraction completes, click Generate. The editor panel should stream in the layout.md content.
Test panel responds
Open the test panel, type a prompt, and send it. You should get a streamed response from Claude.
PLAYWRIGHT_BROWSERS_PATH errors and confirm that npx playwright install --with-deps chromium ran successfully during the build.