Self-hosted install

Updated July 2026

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

Install

  1. Copy the bundle and create your env file.

    Copy deploy/self-hosted/ to the host and duplicate .env.example as .env. Every setting below lives in this file. (Skip this if you ran install.sh — it already wrote ~/.dousen/.env.)

  2. Fill in the required values.

    Database passwords, a DOUSEN_JWT_SECRET, and your license keys. Generate real secrets rather than keeping the placeholders:

    SHELLopenssl rand -hex 32

    DOUSEN_JWT_SECRET must 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 32 gives you a 64-character (32-byte) value that clears the bar comfortably.

  3. Start it.
    SHELLdocker compose up -d

    The 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.

  4. Verify.
    SHELLcurl http://localhost:8080/health

    should answer, and http://<server>:8080/portal/ serves the admin portal (/ and /portal both 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

VariablePurpose
MARIADB_ROOT_PASSWORDRoot password for the bundled MariaDB.
MARIADB_USER / MARIADB_PASSWORDThe service account the API uses (default user dousen).
DOUSEN_JWT_SECRETSecret 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_KEYYour license token, pasted verbatim.
DOUSEN_LICENSE_PUBLIC_KEY_PEMThe public key issued with the license; the server verifies the license offline with it.

Optional

VariablePurpose
DOUSEN_CONTROL_PLANE_URLWhere 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_P4PASSWDThe server's Perforce service connection — used for its changelist operations. Artists' own P4 identities live in DousenDesktop, not here. See Perforce.
DOUSEN_WEBHOOK_SECRETShared secret for the Jira webhook at /webhooks/jira.
DOUSEN_BIND_HOST / DOUSEN_REST_PORTWhere 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_VERSIONImage tag to run. Defaults to latest; pin a version for controlled upgrades.
RUST_LOGLog level, default info.
DOUSEN_SECURE_COOKIESSets 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_ORIGINSComma-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.

VariablePurpose
DOUSEN_PUBLIC_URLPublic 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_HOSTSMTP server. Empty (the default) disables email sending entirely.
DOUSEN_SMTP_PORTDefault 587 (STARTTLS).
DOUSEN_SMTP_USER / DOUSEN_SMTP_PASSWORDSMTP auth credentials.
DOUSEN_SMTP_FROMFrom 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.

VariablePurpose
DOUSEN_LDAP_URLDirectory server, e.g. ldaps://ad.studio.lan.
DOUSEN_LDAP_BIND_DN_TEMPLATETemplate 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_FILTERWhere 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_GROUPDirectory group (CN or full DN) whose members become Dousen admins; everyone else signs in as an artist.
DOUSEN_LDAP_START_TLSEnable 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.

  1. Roll the server out first.

    Get docker compose up -d running and /health answering before handing DousenDesktop to artists.

  2. 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.

  3. Sign in.

    With DOUSEN_AUTH_MODE=local, artists use the email/password an admin created for them (see User & auth management); with DOUSEN_AUTH_MODE=ldap, they sign in with their existing directory credentials instead.

  4. 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:

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

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

SymptomCheck
DOUSEN_LICENSE_KEY not set — running in unlicensed mode (dev only) in the logsExpected 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 errorsdocker 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 pageYou'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 serverConfirm 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 logsdocker compose logs -f dousen-api