Self-hosting
The full Doop on your own server — one Docker container plus Postgres.
Doop is open source under AGPL-3.0. The canvas, the MCP server agents connect to, the Doop Agent, imports, exports, prototypes and integrations all ship in one image. On your own instance billing is off, so every workspace is unlimited.
This page covers the Community edition, which is free. If you need a commercial license, SAML and SCIM, or a support SLA, see Enterprise.
Quick start with Docker Compose
The repository's docker-compose.yml builds the app and starts it next to a Postgres 16 container.
You need Docker with Compose. The image includes Chromium, which renders frame
screenshots and exports.
$ git clone https://github.com/kgoedecke/doop && cd doop $ BETTER_AUTH_SECRET=$(openssl rand -hex 32) docker compose up -d
Open http://localhost:4400 and create an account. Doop runs database migrations on every boot, so
there is no setup step. The first account is a regular user; see admins to give yourself the
admin role.
Keep the secret. BETTER_AUTH_SECRET signs sessions and agent tokens, so if you change it, everyone is
signed out and every connected agent has to authenticate again. Put it in a .env file next to
docker-compose.yml instead of typing it each time:
# .env — read by docker compose
BETTER_AUTH_SECRET=paste-a-long-random-string
BETTER_AUTH_URL=https://doop.example.com Keep uploads across restarts
Postgres data lives in the doop-pg volume. Uploaded images, fonts and imported assets go to
/app/data/assets inside the app container unless you configure
object storage. The stock compose file does not mount that path, so
add a volume before you rely on it. Without one, uploads are lost when the container is rebuilt:
services:
doop:
volumes:
- doop-data:/app/data
volumes:
doop-data: Deploy to production
Any host that runs a container works. Railway, Fly.io, Render, Dokploy and Coolify all detect the
Dockerfile on their own, and a plain VM running Docker Compose works too. For a deployment people
depend on:
- Use a real Postgres. Set
DATABASE_URLto a managed Postgres (or the composedbservice). With noDATABASE_URL, Doop falls back to an embedded, single-process Postgres (PGlite) under/app/data/pg. That is fine for trying Doop out, but it only suits one instance with a persistent volume. - Set the two required variables.
BETTER_AUTH_SECRETis a long random string.BETTER_AUTH_URLis the public origin, for examplehttps://doop.example.com. Agent OAuth URLs are built from it, so a wrong value is the most common reason agents can't connect. If people reach the app on more than one origin, list the extras inTRUSTED_ORIGINS, comma-separated. - Terminate TLS in front of it. The server listens on port
4400and trusts one proxy hop, so a platform edge, Caddy, nginx or a load balancer can handle HTTPS. Allow WebSocket upgrades on/ws, because live cursors and streaming designs run over them. - Point health checks at
GET /healthz. The image already declares a DockerHEALTHCHECKagainst it. - Configure SMTP. Without a mailer, signup is open and unverified, and the admin role can't be granted safely. See accounts and sign-in.
To check the exact production image locally before you deploy it:
$ docker build -t doop . $ docker run -p 4400:4400 -e BETTER_AUTH_URL=http://localhost:4400 -e BETTER_AUTH_SECRET=dev-only doop
Without Docker
Doop installs and builds with Bun and the server runs
on Node, so you need both on your PATH. A production build serves the app, API, WebSocket and MCP
endpoint from one process on :4400:
$ bun install $ bun run build && bun run start
Frame screenshots and exports need Chrome or Chromium on the machine. It is detected automatically, or you can set
CHROME_PATH.
Local development
bun run dev starts the web app on http://localhost:4300 and the API, WebSocket and MCP
server on :4400, with an embedded Postgres in data/pg. It needs no configuration at all.
Configuration
Configuration is entirely environment variables. Each one is validated once at boot, so a misconfigured instance
refuses to start rather than half-working. Every variable is optional except the two marked required, and
each optional feature turns off quietly until its variables are set. This is the short list most self-hosters
touch; .env.example documents the rest.
| What | Variables | When unset |
|---|---|---|
| Core | BETTER_AUTH_SECRET, BETTER_AUTH_URL (both required in production), DATABASE_URL, TRUSTED_ORIGINS, PORT | Embedded Postgres in data/pg, localhost origins |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE, EMAIL_FROM | Emails print to the server log; no verification | |
| Sign-in | OIDC_*, GOOGLE_*, MICROSOFT_*, APPLE_*, SIGNUP_EMAIL_DOMAINS, ADMIN_EMAILS | Email and password only, open signup |
| Object storage | BUCKET, ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, REGION | Uploads stored on disk under data/assets |
| Doop Agent | ANTHROPIC_API_KEY, RESIDENT_TASK_LIMIT, DOOP_AGENT_*, AZURE_OPENAI_* | Runs only on model accounts people connect |
| Image generation | OPENAI_API_KEY, GEMINI_API_KEY, ARK_API_KEY, DOOP_IMAGE_MODEL | Runs only on accounts people connect |
| Agent search | PEXELS_API_KEY (photos), BRANDFETCH_CLIENT_ID (logos), CONTEXT_DEV_API_KEY | No photo search; open-source and favicon logos |
| Integrations | SLACK_*, LINEAR_*, META_APP_*, GITHUB_APP_* | That integration is hidden; GitHub import uses a personal token |
| Billing | STRIPE_SECRET_KEY, STRIPE_PRICE_TEAM_*, STRIPE_WEBHOOK_SECRET | Billing off, every workspace active |
| Webhooks | WEBHOOK_ALLOW_PRIVATE_URLS | Webhook URLs on private networks are refused |
Object storage
Set BUCKET, ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT and
REGION to store uploads in any S3-compatible store: AWS S3, Cloudflare R2, MinIO, Backblaze B2 or
Railway buckets. Use this if you run more than one app instance or don't want to manage a data volume.
Accounts and sign-in
People sign in with an email and password by default, and signup is open. Canvases are private to their owner until shared, just like on doop.design. Agents connected over MCP act as the account that approved them and get exactly that person's access.
Set the SMTP_* variables and EMAIL_FROM. Signups then need email verification, and
password resets send real links. Without SMTP, every email, links included, is printed to the server log, so the
flows still work while you test. Set REQUIRE_EMAIL_VERIFICATION=false if you want people to get in
before they verify.
Restrict signup
SIGNUP_EMAIL_DOMAINS=example.com limits new accounts to one email domain. Use a comma-separated list
for several domains. It applies to every sign-in method, and existing accounts are unaffected.
SSO and social sign-in
These sit alongside email and password; they don't replace it. Each one needs its variables set together:
- OIDC: Okta, Zitadel, Authentik, Keycloak or any OIDC provider. Set
OIDC_ISSUER,OIDC_CLIENT_IDandOIDC_CLIENT_SECRET.OIDC_PROVIDER_NAMElabels the button. - Google:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET, with<BETTER_AUTH_URL>/api/auth/callback/googleas the redirect URI. - Microsoft:
MICROSOFT_CLIENT_ID,MICROSOFT_CLIENT_SECRET, and optionallyMICROSOFT_TENANT_IDto admit only your organisation. - Apple:
APPLE_CLIENT_IDandAPPLE_CLIENT_SECRETfor the web, andAPPLE_APP_BUNDLE_IDENTIFIERfor the iOS app.
A partial set refuses to boot. If an SSO sign-in's email matches an existing account and the provider marks that email verified, it signs in to that account.
Admins
ADMIN_EMAILS (comma-separated) names the accounts that get the admin role. You can name someone before
or after they sign up. Admins get /admin, which lists every canvas and account on the instance and
offers a read-only “view as” session for support.
An address is promoted only once it is verified, so in production this needs SMTP. Otherwise anyone could sign up with your address and take the role. An instance without SMTP promotes nobody and warns at boot; in that case, set the role in the database yourself.
The Doop Agent on your server
The built-in Doop Agent works on a self-hosted instance with no server key. Each
person connects their own model account in Settings, and their runs bill to it. To let the server
pay for some runs instead, give it a key and raise the free-task limit (it is 0 by default):
ANTHROPIC_API_KEY=sk-ant-... RESIDENT_TASK_LIMIT=20
If your organisation's credits or compliance rules are on Azure OpenAI, the server-paid runs can use it instead:
DOOP_AGENT_PROVIDER=azure AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com AZURE_OPENAI_API_KEY=... AZURE_OPENAI_DEPLOYMENT=my-deployment
People can also connect a custom provider. By default its URL must
be a public HTTPS address. For models on your own network (Ollama, vLLM, LM
Studio), set DOOP_CUSTOM_PROVIDER_PRIVATE_HOSTS=true to allow private and loopback hosts and plain
HTTP. Don't do this on an instance strangers can sign up to. Set DOOP_CUSTOM_PROVIDERS=false to turn the feature
off.
Agents you bring yourself over MCP (Claude Code, Codex, Cursor, ChatGPT) need no key on the server, because they run on their own subscriptions.
Connect agents to your instance
Every setup in Get started works the same against your server. Replace
https://doop.design/mcp with your own origin:
$ claude mcp add --transport http doop https://doop.example.com/mcp
The MCP endpoint uses OAuth with dynamic client registration, so there is nothing to register by hand. The first
tool call opens a browser where you sign in to your instance and approve the agent. If the approval page points
at the wrong host, BETTER_AUTH_URL is wrong.
Integrations
On a self-hosted instance you register the third-party apps yourself, and each integration appears once its
variables are set. The steps are in the Integrations and
Automations docs: Slack and Linear apps, the Meta app for ad automations, and a
GitHub App for GitHub import. Webhooks need no setup, but by default they refuse URLs on private networks. Set
WEBHOOK_ALLOW_PRIVATE_URLS=true to send them to internal services.
Workspaces and billing
Billing is off unless you turn it on. Without STRIPE_SECRET_KEY, every
workspace is active with unlimited members and canvases. You need
Stripe only if you resell Doop to your own customers. In that case, the repository's
scripts/stripe-setup.mjs creates the products, and the README covers the webhook.
Upgrades and backups
To upgrade, pull the latest code and rebuild. Migrations run automatically on boot, so there is no separate migrate step:
$ git pull $ docker compose up -d --build
Changes are listed in the
changelog. Back up two things: the Postgres database and your uploads (the /app/data volume or
your bucket). With the compose setup, this dumps the database:
$ docker compose exec db pg_dump -U doop doop > doop-$(date +%F).sql Troubleshooting
- Signup or password reset hangs. The host is almost certainly blocking outbound SMTP. Railway and
most PaaS block ports 25, 465 and 587. Many providers, Resend included, also listen on
2587; trySMTP_PORT=2587. - Agents get a 401 or the approval page is on the wrong host.
BETTER_AUTH_URLmust be the public HTTPS origin people actually use. Fix it, restart the app, and connect the agent again. - Cursors and streaming don't update live. Your proxy isn't passing WebSocket upgrades on
/ws. - The server refuses to boot. Read the log. It names the variable that failed validation, usually one half of a pair (an OIDC, Google or Microsoft client ID without its secret).
- Hosted web fonts look wrong in exports. Exports load roster fonts from Google Fonts, so an instance without outbound internet renders them as the fallback. Upload them to a font library instead.
Stuck? Ask on Discord or open an issue on GitHub.
License
Doop is licensed under the GNU AGPL v3. You can run it, modify it and use it inside your company freely. If you offer a modified version to others over a network, you must publish your changes under the same license. The Doop name and logo are trademarks, so please rebrand a service you derive from it.
The Enterprise license removes the AGPL obligations and adds SAML and SCIM, audit logs, a support SLA, and help with deployment and upgrades.