Self-hosted install
Studios that keep everything on their own network run the Dousen server (DousenCore) themselves with Docker Compose. One container runs the API and web portal, one runs MariaDB; artists' DousenDesktop apps and the Unreal plugin talk to it over plain HTTP/JSON on port 8080.
The deployment bundle ships in the release under deploy/self-hosted/: a docker-compose.yml, an install.sh helper, an annotated .env.example, and SELF_HOSTING.md with operational notes.
install.sh is the fastest path: it checks for Docker, generates random database and JWT secrets, prompts for your license key and Perforce details, writes a chmod 600 .env, and brings the stack up for you. The steps below are the manual equivalent — useful if you want to see every setting before it's applied, or you're scripting the install yourself.
DousenControl — Dousen's own cloud control plane — never has to run on your infrastructure. It exists purely to hand you two things ahead of time: a signed license and, optionally, a daily heartbeat target. Once those are in your .env, your instance is fully self-contained.
Requirements
- A Linux host (Ubuntu 22.04+ or Debian 12+) with Docker 24+ and Docker Compose v2. 2 GB RAM minimum, 4 GB recommended.
- A Dousen license — the token and its public key, issued for your studio. Self-serve issuance isn't live yet; email [email protected] to get one. Running without a license starts the server in an unlicensed dev mode — see Licensing below.
- Optionally: a Perforce server, a Jira Cloud site, and an LDAP/AD directory to hook up.
Install
-
Copy the bundle and create your env file.
Copy
deploy/self-hosted/to the host and duplicate.env.exampleas.env. Every setting below lives in this file. (Skip this if you raninstall.sh— it already wrote~/.dousen/.env.) -
Fill in the required values.
Database passwords, a
DOUSEN_JWT_SECRET, and your license keys. Generate real secrets rather than keeping the placeholders:SHELL
openssl rand -hex 32DOUSEN_JWT_SECRETmust be at least 32 bytes, and the server enforces it: the auth service asserts the length when it builds its state at startup, so a shorter secret aborts the process on boot rather than running with a weak key.openssl rand -hex 32gives you a 64-character (32-byte) value that clears the bar comfortably. -
Start it.
SHELL
docker compose up -dThe API container waits for MariaDB to report healthy, then runs its database migrations automatically on every startup — there's no separate migration step to remember — and comes up on port 8080.
-
Verify.
SHELL
curl http://localhost:8080/healthshould answer, and
http://<server>:8080/portal/serves the admin portal (/and/portalboth redirect there). Sign in and create your first admin user, then follow the Quick start: create a project, connect Jira, and roll DousenDesktop out to artists.
Configuration reference
All server configuration is environment variables in .env. The shipped docker-compose.yml only forwards the variables it explicitly lists in the dousen-api service's environment: block — setting something in .env alone doesn't reach the container. The tables below cover what ships wired up out of the box; a few advanced ones (email/SMTP, cookie and CORS behavior) aren't in the default compose file yet — add them to that environment: block yourself if you need them, following the same VAR: ${VAR:-default} pattern already used there.
Required
| Variable | Purpose |
|---|---|
| MARIADB_ROOT_PASSWORD | Root password for the bundled MariaDB. |
| MARIADB_USER / MARIADB_PASSWORD | The service account the API uses (default user dousen). |
| DOUSEN_JWT_SECRET | Secret for signing session tokens. Generate a real one — see the callout below the install steps; it isn't checked for length or entropy at startup. |
| DOUSEN_LICENSE_KEY | Your license token, pasted verbatim. |
| DOUSEN_LICENSE_PUBLIC_KEY_PEM | The public key issued with the license; the server verifies the license offline with it. |
Optional
| Variable | Purpose |
|---|---|
| DOUSEN_CONTROL_PLANE_URL | Where the daily license heartbeat goes. Default https://control.dousen.io. Heartbeat failures never take the server down — offline networks are fine. |
| DOUSEN_P4PORT / DOUSEN_P4USER / DOUSEN_P4CLIENT / DOUSEN_P4PASSWD | The server's Perforce service connection — used for its changelist operations. Artists' own P4 identities live in DousenDesktop, not here. See Perforce. |
| DOUSEN_WEBHOOK_SECRET | Shared secret for the Jira webhook at /webhooks/jira. |
| DOUSEN_BIND_HOST / DOUSEN_REST_PORT | Where the API port is published on the host. Defaults to 127.0.0.1:8080 — loopback only, expecting a reverse proxy in front. Set DOUSEN_BIND_HOST=0.0.0.0 to expose it directly on a trusted LAN. |
| DOUSEN_VERSION | Image tag to run. Defaults to latest; pin a version for controlled upgrades. |
| RUST_LOG | Log level, default info. |
| DOUSEN_SECURE_COOKIES | Sets the Secure flag on session cookies. Defaults to true. Not wired into the default compose file — if you're testing over plain HTTP without a reverse proxy, add it and set it to false, or login will appear to succeed and then silently bounce back (the browser drops a Secure cookie sent over a non-HTTPS connection). |
| DOUSEN_CORS_ORIGINS | Comma-separated allowed origins. Defaults to same-origin only, which is correct for the default single-container setup. Only needed if you serve the portal UI from a different origin than the API. Not wired into the default compose file. |
Email / password reset (SMTP)
Admins can trigger a password reset for a local (non-LDAP) user from the portal's Users page. Whether that actually sends an email depends on SMTP configuration — also not wired into the default compose file, so add these to the dousen-api service's environment: block to use them.
| Variable | Purpose |
|---|---|
| DOUSEN_PUBLIC_URL | Public base URL used to build the reset link ({public_url}/portal/reset-password?token=…). Defaults to http://localhost:8080 — set this to your real reverse-proxy URL. |
| DOUSEN_SMTP_HOST | SMTP server. Empty (the default) disables email sending entirely. |
| DOUSEN_SMTP_PORT | Default 587 (STARTTLS). |
| DOUSEN_SMTP_USER / DOUSEN_SMTP_PASSWORD | SMTP auth credentials. |
| DOUSEN_SMTP_FROM | From address. Default [email protected]. |
With DOUSEN_SMTP_HOST unset, clicking "Send reset" still works — the server creates the reset token, logs the full reset link to its own output instead of emailing it, and the portal shows an amber "SMTP not configured — link logged to server output" next to the user, rather than silently claiming success. Retrieve the link with docker compose logs dousen-api and pass it to the user yourself. Once SMTP is configured, the same action sends the email over STARTTLS and the portal shows a green "Reset link sent" instead.
Directory sign-in (LDAP / Active Directory)
By default users sign in with passwords stored on the instance (DOUSEN_AUTH_MODE=local). To delegate to your directory, set DOUSEN_AUTH_MODE=ldap — users are then auto-provisioned on first login, and membership in the admin group grants the admin role. See User & auth management.
| Variable | Purpose |
|---|---|
| DOUSEN_LDAP_URL | Directory server, e.g. ldaps://ad.studio.lan. |
| DOUSEN_LDAP_BIND_DN_TEMPLATE | Template for the bind DN of the signing-in user, e.g. {username}@studio.local (AD UPN) or uid={username},ou=people,dc=studio,dc=local (OpenLDAP). |
| DOUSEN_LDAP_USER_SEARCH_BASE / DOUSEN_LDAP_USER_FILTER | Where and how to look users up, e.g. base OU=Staff,DC=studio,DC=local and filter (sAMAccountName={username}) (the default filter). |
| DOUSEN_LDAP_ADMIN_GROUP | Directory group (CN or full DN) whose members become Dousen admins; everyone else signs in as an artist. |
| DOUSEN_LDAP_START_TLS | Enable StartTLS (default false; prefer an ldaps:// URL instead). |
The bind is direct — Dousen authenticates as the signing-in user itself, so no separate service account is needed. Empty passwords are always rejected, closing off anonymous-bind bypasses. If your directory doesn't expose a mail attribute for a user, their login name is used as the Dousen account identifier instead.
Connecting DousenDesktop and Unreal
DousenDesktop's first-run onboarding asks up front whether the studio is Dousen Cloud or Self-hosted. Choosing self-hosted reveals a Server URL field (placeholder http://localhost:8080) and a Test Connection button that checks /health before letting the artist continue. Point it at whatever your reverse proxy (or, on a trusted LAN, the host directly) exposes on port 8080.
- Roll the server out first.
Get
docker compose up -drunning and/healthanswering before handing DousenDesktop to artists. - Install DousenDesktop and choose "Self-hosted" on first launch.
Enter your server's URL — the public one behind your reverse proxy, or the LAN address if you exposed the port directly — and use Test Connection to confirm it resolves before finishing setup.
- Sign in.
With
DOUSEN_AUTH_MODE=local, artists use the email/password an admin created for them (see User & auth management); withDOUSEN_AUTH_MODE=ldap, they sign in with their existing directory credentials instead. - Changed the address later?
The server URL isn't locked in after onboarding — it's editable from DousenDesktop's Settings dialog, under the Connection tab.
The Unreal plugin is configured separately, in Config/BaseDousenUE.ini under [/Script/DousenUE.DousenProjectSettings] — set ServerScheme, ServerHost, and BridgeRelayPort to match your instance and the artist's local DousenDesktop bridge, and ProjectId to the project it should sync against.
Licensing
The license is a single Ed25519-signed JWT (DOUSEN_LICENSE_KEY) plus the matching public key (DOUSEN_LICENSE_PUBLIC_KEY_PEM) that ships with your studio's issued key. Verification happens entirely offline against that public key — no outbound call is required for the server to trust its own license, so it works fine on an air-gapped network. The token itself encodes your tier, seat count, and expiry.
What happens at startup:
- No key set: the server starts anyway, in an unlicensed dev mode — it logs a warning and applies a default 5-seat limit. Don't run production on this.
- Valid, unexpired license: starts normally; logs the tier, seat count, and days until expiry at
infolevel. - Expired, within the 14-day grace window: keeps running — logs a warning pointing to
dousen.io/billingfor renewal, but nothing artists do is blocked. - Expired, past the grace window: the process exits at startup and refuses to come back up until
DOUSEN_LICENSE_KEYis replaced with a renewed token.
License and grace-period status is log-only today — there's no banner or indicator in the admin portal. If you want advance warning before the grace window runs out, watch for "license valid" / "LICENSE EXPIRED" lines in docker compose logs dousen-api (or wire your log shipper to alert on them) rather than relying on the portal to tell you.
Renewal: replace DOUSEN_LICENSE_KEY with the renewed token in .env and restart the stack (docker compose up -d). Because verification is offline with a real grace period, renewing never has to be a fire drill — but you do have to notice it's due, per the note above.
Operational notes
- Database data lives in the named Docker volume
dousen-mariadb— include it in your backup routine. A logical dump:SHELL
Back updocker exec dousen-mariadb mariadb-dump \ -u root -p"$MARIADB_ROOT_PASSWORD" dousen \ > dousen-backup-$(date +%Y%m%d).sql.envtoo (it holds your license key and secrets) — store it somewhere other than the Docker volume itself. - Upgrades: bump
DOUSEN_VERSION, thendocker compose pull && docker compose up -d. Migrations run automatically on start — there's nothing else to trigger by hand. - Perforce in Docker: the compose file includes a commented-out
p4dservice if you want a self-contained evaluation stack. - Kubernetes: a Helm chart (
infra/helm/dousencore/, centralized alongside the other Dousen services' charts) uses the same environment variables under the hood, but it's built for the managed GKE deployment — Cloud SQL, Workload Identity, a cert-manager-issued ingress — that DousenControl's own tenant provisioner drives, not a turnkey chart for an arbitrary cluster. Treat it as a reference to adapt (in particular setmariadb.enabled: trueand replace the ingress/Cloud SQL values) rather than something tohelm installunmodified. Docker Compose is the supported, documented path for self-hosted studios today.
The server binds to loopback by default. If artists can't reach it, that's the first thing to check — either front it with a reverse proxy (recommended, and where TLS should terminate) or set DOUSEN_BIND_HOST=0.0.0.0.
Troubleshooting
| Symptom | Check |
|---|---|
DOUSEN_LICENSE_KEY not set — running in unlicensed mode (dev only) in the logs | Expected if you haven't set a license yet — set DOUSEN_LICENSE_KEY and DOUSEN_LICENSE_PUBLIC_KEY_PEM in .env and restart. Fine for evaluation, not for production. |
| API container won't come up / DB connection errors | docker compose ps — the mariadb service should show healthy before dousen-api will start against it. |
| Login appears to succeed, then bounces back to the login page | You're testing over plain HTTP without a reverse proxy while DOUSEN_SECURE_COOKIES defaults to true — the browser silently drops a Secure cookie sent over non-HTTPS. Add the variable to the compose file's environment: block and set it to false for local testing, or put TLS in front. |
| DousenDesktop or the Unreal plugin can't reach the server | Confirm port 8080 (or your reverse proxy's port) is reachable from the workstation — check firewall rules on the host, and that DOUSEN_BIND_HOST isn't still loopback-only if you're not fronting with a proxy. |
| Need the current logs | docker compose logs -f dousen-api |