Skip to content

Latest commit

 

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

webstudio-self-host

Docker Compose setup to self-host Webstudio, the open-source visual development platform.

Requirements: Linux server · Docker ≥ 24 · Compose v2 · 2 GB RAM · 10 GB disk

This repo provides two compose files:

  • docker-compose.yml: plain Docker Compose (any Linux server)
  • docker-compose.coolify.yml: optimised for Coolify with Traefik and auto-generated secrets

What's included

Service Role
app Webstudio builder (Remix app)
db PostgreSQL 15
db-setup Grants PostgREST permissions on DB tables (runs once)
postgrest PostgREST (REST API over the DB)
migrate Runs Prisma migrations on startup
minio S3-compatible object storage for assets
nginx Serves published static sites
publisher Generates static HTML when you click Publish

Deploy with plain Docker Compose

git clone https://github.com/webstudio-community/webstudio-self-host.git
cd webstudio-self-host

cp .env.example .env
# Edit .env -> change every "change-me" value

Generate the required secrets:

# POSTGRES_PASSWORD
openssl rand -hex 16

# AUTH_SECRET
openssl rand -hex 32

# PGRST_JWT_SECRET (must be ≥ 64 characters)
openssl rand -hex 64

# TRPC_SERVER_API_TOKEN (shared secret between builder and publisher)
openssl rand -hex 32

Also set DEPLOYMENT_URL in your .env to your builder's public URL (required in production regardless of login mode):

DEPLOYMENT_URL=https://webstudio.your-domain.com

Then start:

docker compose up -d

The builder is available at http://localhost:3000.


Deploy with Coolify (recommended)

Coolify manages SSL certificates, the Traefik reverse proxy, and automatic restarts.

Overview of the domain architecture

The setup uses three domain scopes, each with its own wildcard:

Domain Purpose Service
webstudio.your-domain.com Main builder UI app (Coolify-managed)
*.webstudio.your-domain.com Canvas preview iframes (one per project) app (Traefik label)
*.wstdwork.your-domain.com Published static sites nginx (Traefik label)

The first domain is configured via Coolify's UI. The other two require Traefik labels in the compose file. Coolify routes based on these labels but does not manage them through its UI.

1. DNS records

Create these records in your DNS provider (no proxy/CDN. Orange cloud off if using Cloudflare):

Type Name Content
A webstudio.your-domain.com your server IP
A *.webstudio.your-domain.com your server IP
A *.wstdwork.your-domain.com your server IP

wstdwork is the subdomain prefix for published sites. You can pick any name — just make sure it matches PUBLISHER_HOST in your env and the labels in the compose file.

2. Configure Traefik in Coolify

Webstudio needs wildcard TLS certificates, which require a DNS challenge. Coolify uses Traefik as its reverse proxy — you need to add the Cloudflare resolver and the gzip middleware to Traefik's static configuration.

In Coolify: Server → Proxy → Configuration

Under the command: block, add these lines (replace with your own email and cert resolver name):

command:
  # ... existing Coolify args ...
  - "--certificatesresolvers.cloudflare.acme.email=you@your-domain.com"
  - "--certificatesresolvers.cloudflare.acme.storage=/traefik/acme.json"
  - "--certificatesresolvers.cloudflare.acme.dnschallenge=true"
  - "--certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare"
  - "--certificatesresolvers.cloudflare.acme.dnschallenge.resolvers=1.1.1.1:53,1.0.0.1:53"

Under the environment: block, add your Cloudflare API token:

environment:
  - CF_DNS_API_TOKEN=your-cloudflare-api-token

To create a Cloudflare API token: Cloudflare dashboard → My Profile → API Tokens → Create Token → use the "Edit zone DNS" template → restrict it to your domain.

Also add a dynamic config file so Traefik knows about the gzip middleware (referenced in the compose labels). In Coolify: Server → Proxy → Dynamic Configuration, create a new file webstudio.yaml with:

http:
  middlewares:
    gzip:
      compress: {}

Important: The resolver name cloudflare used in --certificatesresolvers.cloudflare.* must exactly match the value in tls.certresolver=cloudflare in the Traefik labels of the compose file. If you change one, change the other.

3. Update Traefik labels in the compose file

Coolify does not interpolate environment variables inside Traefik labels. You must edit docker-compose.coolify.yml directly and replace the placeholder domains with your actual values before deploying.

In the app service, replace webstudio.example.com with your builder domain:

# Before
- "traefik.http.routers.ws-canvas.rule=HostRegexp(`^.+[.]webstudio[.]example[.]com`)"
- "traefik.http.routers.ws-canvas.tls.domains[0].main=*.webstudio.example.com"
- "traefik.http.routers.ws-canvas.tls.domains[0].sans=webstudio.example.com"

# After (example)
- "traefik.http.routers.ws-canvas.rule=HostRegexp(`^.+[.]webstudio[.]your-domain[.]com`)"
- "traefik.http.routers.ws-canvas.tls.domains[0].main=*.webstudio.your-domain.com"
- "traefik.http.routers.ws-canvas.tls.domains[0].sans=webstudio.your-domain.com"

In the nginx service, replace wstdwork.example.com with your publish domain (must match PUBLISHER_HOST):

# Before
- "traefik.http.routers.ws-publisher.rule=HostRegexp(`^.+[.]wstdwork[.]example[.]com`)"
- "traefik.http.routers.ws-publisher.tls.domains[0].main=*.wstdwork.example.com"
- "traefik.http.routers.ws-publisher.tls.domains[0].sans=wstdwork.example.com"

# After (example)
- "traefik.http.routers.ws-publisher.rule=HostRegexp(`^.+[.]wstdwork[.]your-domain[.]com`)"
- "traefik.http.routers.ws-publisher.tls.domains[0].main=*.wstdwork.your-domain.com"
- "traefik.http.routers.ws-publisher.tls.domains[0].sans=wstdwork.your-domain.com"

4. Create the resource in Coolify

In your Coolify project: New resource → Docker Compose → From a Git repository

  • Repository: https://github.com/webstudio-community/webstudio-self-host
  • Compose file: docker-compose.coolify.yml

Coolify auto-generates these variables — leave them as-is:

  • SERVICE_PASSWORD_DB, SERVICE_PASSWORD_AUTH, SERVICE_BASE64_64_PGRST, SERVICE_BASE64_64_TRPC
  • SERVICE_FQDN_APP_3000, SERVICE_FQDN_MINIO_9000, SERVICE_URL_APP

Set these manually in the Coolify environment:

# Builder domain (used to construct canvas iframe URLs)
APP_FQDN=webstudio.your-domain.com

# Published sites domain suffix (must match the nginx Traefik label)
PUBLISHER_HOST=wstdwork.your-domain.com

# How users log in — choose one:

# Option A: simple password login (password = AUTH_SECRET value, shown in Coolify)
DEV_LOGIN=true
DEV_LOGIN_EMAIL=admin@example.com

# Option B: GitHub OAuth
# GH_CLIENT_ID=...
# GH_CLIENT_SECRET=...
# Note: DEPLOYMENT_URL is automatically set from SERVICE_URL_APP by the compose file

5. Deploy

Click Deploy in Coolify. First deploy takes 2–3 min (pulls images, runs DB migrations).


Updating

# Docker Compose
docker compose pull
docker compose up -d

# Coolify: click "Redeploy" in the UI

DB migrations run automatically on every restart.


Authentication

Three login modes are available. Choose one and set the corresponding variables.

Option A: Simple password login (default)

No external provider needed. Set in .env (plain Compose) or Coolify environment:

DEV_LOGIN=true
DEV_LOGIN_EMAIL=admin@example.com

The password is the value of AUTH_SECRET (visible in Coolify under the service's environment variables).

Also set DEPLOYMENT_URL in your .env (plain Compose — Coolify sets it automatically): the compose file always passes the variable through, and the app refuses to start if it's empty. With Coolify this is set automatically from SERVICE_URL_APP; with plain Compose, set it to your builder's URL (e.g. http://localhost:3000 for local testing, see Testing locally over plain HTTP).

Option B: GitHub OAuth

  1. Go to github.com → Settings → Developer Settings → OAuth Apps → New OAuth App
  2. Fill in:
    • Homepage URL: https://webstudio.your-domain.com
    • Authorization callback URL: https://webstudio.your-domain.com/auth/github/callback
  3. Copy the Client ID and generate a Client Secret
  4. Remove DEV_LOGIN from your env, then add:
GH_CLIENT_ID=your-client-id
GH_CLIENT_SECRET=your-client-secret
DEPLOYMENT_URL=https://webstudio.your-domain.com

With Coolify: DEPLOYMENT_URL is automatically set from SERVICE_URL_APP by the compose file — no need to add it manually.

Option C: Google OAuth

  1. Go to console.cloud.google.com → APIs & Services → Credentials → Create OAuth 2.0 Client ID
  2. Application type: Web application
  3. Under Authorized redirect URIs, add: https://webstudio.your-domain.com/auth/google/callback
  4. Copy the Client ID and Client Secret
  5. Remove DEV_LOGIN from your env, then add:
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
DEPLOYMENT_URL=https://webstudio.your-domain.com

With Coolify: DEPLOYMENT_URL is automatically set from SERVICE_URL_APP by the compose file — no need to add it manually.

Testing locally over plain HTTP

Session cookies use the __Host- prefix, which requires HTTPS. If you're running docker-compose.yml on your own machine (http://localhost, no reverse proxy/TLS in front), browsers silently refuse to set these cookies — both dev login and OAuth fail (OAuth shows "Missing state on session"). To test locally over plain HTTP, set in .env:

DEPLOYMENT_ENVIRONMENT=development
DEPLOYMENT_URL=http://localhost:3000
ALLOW_INSECURE_COOKIES=true

DEPLOYMENT_URL is required here too, even for dev login: the compose file always passes it through, and the app refuses to start on an empty value.

Never set DEPLOYMENT_ENVIRONMENT/ALLOW_INSECURE_COOKIES like this on a real deployment — ALLOW_INSECURE_COOKIES is refused at startup when DEPLOYMENT_ENVIRONMENT=production (the default).


Custom domains

Once a project is published you can attach any custom domain — including an apex (root) domain like example.com — via the Publish panel in the builder.

Subdomain (www.example.com)

Standard setup: create a CNAME record pointing to your server hostname. The builder UI will show you the exact CNAME target and TXT verification record to add.

Apex domain (example.com)

Apex domains cannot use a CNAME record with most DNS providers. The builder detects this and shows an A record instruction instead.

DNS records to create:

Type Name Value
A @ your server IP
TXT _webstudio_is the token shown in the Publish panel

Steps:

  1. In the builder, open Publish → Add a new domain and enter example.com
  2. The panel shows the A record and TXT record to create. Copy the TXT value
  3. In your DNS provider, create both records
  4. Wait for DNS propagation (a few minutes to an hour), then click Verify
  5. Publish. The site is served from example.com with an automatic Let's Encrypt certificate

Each apex domain is independent: multiple projects can each have their own apex domain pointing to the same server.


Publish targets

The Publish panel has two selectors (shown when PUBLISHER_HOST is set), remembered per project:

  • RenderingStatic (SSG) (prerendered HTML) or Dynamic (SSR) (a Node app, one Docker container per site)
  • Hosting — where the result is served
Rendering Hosting What it does Served by
Static / Dynamic This Webstudio instance served locally: static files, or a docker run container publisher proxy / nginx
Static Cloudflare Pages wrangler pages deploy (needs CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID on the publisher) Cloudflare
Static Remote server (SSH) prerenders, then rsync to a server you own your server
Dynamic Coolify (remote) builds the SSR image, pushes it to a registry, then triggers a deploy webhook your (or a client's) Coolify

Unavailable combinations are greyed out in the panel (e.g. Dynamic + Cloudflare, or a hosting option the publisher isn't configured for).

Remote server (SSH)

Deploy the static build to any server you control over rsync/SSH. TLS and the web-server config on that server are your responsibility — the publisher only ships files.

On the target server: install rsync, and configure a web server (nginx, Caddy…) to serve the deploy directory for your domain.

Generate a deploy key and authorize it:

ssh-keygen -t ed25519 -f ./ws_deploy -N ''
ssh-copy-id -i ./ws_deploy.pub deploy@your-server

Register the target with the publisher (once per site — domain is the project slug). The publisher's build API listens on port 4000 and is reachable only from inside the compose network (it has no authentication and the private key travels over it in plaintext — never expose port 4000 publicly), so run this from the server hosting the stack:

docker compose exec publisher \
  curl -sS -X POST http://localhost:4000/targets/ssh-setup \
    -H 'content-type: application/json' \
    -d "$(jq -n --arg key "$(cat ./ws_deploy)" '{
          domain:"my-project", sshHost:"your-server", sshUser:"deploy",
          sshPath:"/var/www/my-project", sshPort:22, sshPrivateKey:$key,
          publicUrl:"https://my-project.com" }')"

(jq builds the JSON so the multi-line private key is escaped correctly. curl and jq are both present in the publisher image.)

sshHost, sshUser and sshPath must be plain values (hostname/IP, a normal user name, an absolute path) — they are used as rsync/ssh arguments and the endpoint rejects anything with shell metacharacters or spaces.

The private key is stored (chmod 600) on the publisher-work volume and never leaves the publisher; ssh-keyscan pre-seeds known_hosts at setup. publicUrl (optional) is the origin baked into og: tags and sitemap.xml; it defaults to the project's first custom domain.

Then pick Static (SSG) + Remote server (SSH) in the Publish panel and publish. Each publish runs rsync -az --delete, so the target directory mirrors the build exactly. Unpublishing or switching targets forgets the site locally but leaves the remote files in place.

Coolify (remote SSR)

Host a Dynamic (SSR) or Static (SSG) site on any Coolify instance — yours, or a different one per client, potentially on another server. The publisher builds a Docker image (the SSR app, or dist/client wrapped in nginx:alpine) and pushes it to a registry; the target Coolify pulls it and redeploys when its deploy webhook is triggered.

1. Give the publisher a registry. Set on the publisher (.env / Coolify env):

REGISTRY_URL=ghcr.io/my-org        # a registry every target Coolify can pull from
REGISTRY_USER=my-org               # omit both if the images are public
REGISTRY_TOKEN=ghp_…               # a write token

Use a public registry (ghcr.io, Docker Hub) — simplest, clients need no credentials — or run your own (registry:2 behind Traefik; not bundled in this repo). The publisher also needs the Docker socket — uncomment the /var/run/docker.sock mount in the publisher service.

2. On the target Coolify, the site owner creates a Docker Image application:

  • Image: ${REGISTRY_URL}/ws-<project-slug> — the slug is the project id shown in the Publish panel URL; the tag is latest
  • Port: 3000 for SSR (the image sets EXPOSE 3000 and IPX_HTTP_ALLOW_ALL_DOMAINS=true), 80 for SSG (the nginx:alpine image)
  • SSR only: optional persistent storage at /var/cache/ipx so optimized images survive restarts
  • Set the app's domain(s) and let Coolify handle TLS

Then they copy the app's deploy webhook URL (Settings → Webhooks, or https://<coolify>/api/v1/deploy?uuid=<app-uuid> + an API token scoped to deploy).

3. In the Publish panel, pick Dynamic (SSR) or Static (SSG) + Coolify (remote), paste the webhook URL (and token, if the URL needs one). Publish is blocked until the URL is filled. For SSG, publish with a custom domain selected so the og: / sitemap.xml URLs use it.

Each publish: docker builddocker push ${REGISTRY_URL}/ws-<slug>:latestPOST the webhook. The publisher never calls the Coolify API itself and doesn't wait for the deploy to finish — a 2xx from the webhook means it's queued. Unpublishing forgets the site locally; the Coolify app and the registry images are left in place (they belong to the site owner). The webhook URL must be https to a public host.


Environment variables

Variable Required Default Description
POSTGRES_PASSWORD - PostgreSQL password
PGRST_JWT_SECRET - Secret for PostgREST JWT auth (≥ 64 chars)
AUTH_SECRET - Session cookie signing secret
APP_FQDN ✅ (Coolify) - Builder public domain (e.g. webstudio.your-domain.com)
DEV_LOGIN - - true = password login using AUTH_SECRET as the password.
DEV_LOGIN_EMAIL - admin@example.com Email used for dev login
GH_CLIENT_ID / GH_CLIENT_SECRET - - GitHub OAuth
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET - - Google OAuth
DEPLOYMENT_URL - Builder's public URL with protocol (e.g. https://webstudio.your-domain.com). Required in production for all login modes. Auto-set from SERVICE_URL_APP in Coolify.
DEPLOYMENT_ENVIRONMENT - production Set to production automatically by both compose files. Override to development in .env (plain Compose only) for local HTTP testing.
ALLOW_INSECURE_COOKIES - - true = drop the __Host-/Secure cookie requirements, for local testing over plain HTTP. Refused when DEPLOYMENT_ENVIRONMENT=production. Never use on a real deployment.
AUTH_WS_CLIENT_ID ✅ (prod) - OAuth server credential for the webstudio CLI. Any non-empty value. Auto-set in Coolify.
AUTH_WS_CLIENT_SECRET ✅ (prod) - OAuth server secret for the webstudio CLI. Any strong random value. Auto-set in Coolify.
PUBLISHER_HOST - wstd.work Domain suffix for published project URLs
TRPC_SERVER_API_TOKEN - - Service token shared between builder and publisher
SELF_HOSTED_PUBLISHER_URL - http://publisher:4000 Internal publisher URL
FEATURES - * Feature flags (* = all enabled)
USER_PLAN - pro Plan level for all users
MAX_ASSETS_PER_PROJECT - 50 Asset upload limit per project
S3_ENDPOINT / S3_REGION / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY / S3_BUCKET - MinIO defaults S3-compatible storage
ENTRI_APPLICATION_ID / ENTRI_SECRET - - Entri automatic DNS setup (optional)
CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID - - Enables the "Cloudflare Pages" hosting option
REGISTRY_URL - - Registry the publisher pushes images to — enables "Coolify (remote)" hosting for SSR and SSG (details)
REGISTRY_USER / REGISTRY_TOKEN - - Registry write credentials (omit for public images)
BUILDER_IMAGE - ghcr.io/webstudio-community/builder:latest Builder Docker image
PUBLISHER_IMAGE - ghcr.io/webstudio-community/webstudio-publisher:latest Publisher Docker image

Related repositories

About

How to self-host webstudio with docker-compose

Resources

Stars

18 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors