Skip to content

Self-Hosting

The editor runs entirely in the browser, so you can host it for your team with no database at all. Choose the path that fits.

CoreWith workspaces
Draw, save to the browser and to files
Live sessions, by sharing a link
Shared team storage
Sign in with your identity provider
Live sessions only for people who signed in
Documents open from any browser you sign in on
Serviceseditor, relayeditor, relay, store, PostgreSQL
Stores documents on your serverNoYes, encrypted - the server cannot read them
You look aftera TLS certificatea certificate, a database, backups, and a recovery key
Setup time~10 minutes~30 minutes

Not sure? Start with Core. Moving to workspaces later means adding services, not replacing anything, and documents people already have stay in their browsers until they choose to save them to the workspace.

Both paths use one domain and one TLS proxy with every service behind it by path. That keeps browsers' cookie and cross-origin rules out of the way, so there are fewer settings to get wrong.

Container images

ImageService
ghcr.io/jbraunsmajr/system-designThe editor, with this documentation at /docs/
ghcr.io/jbraunsmajr/system-design-relayThe relay, for live sessions
ghcr.io/jbraunsmajr/system-design-storeThe workspace store - with workspaces only

Each is tagged latest and with its release date. The three are released together; pin the same date on all of them.

Networks without direct paths

Live sessions connect browsers directly to each other. On air-gapped networks, or where firewalls block that, they need a TURN server to relay through. Add one beside either deployment:

yaml
services:
  # Other services here
  coturn:
    image: coturn/coturn:latest
    container_name: coturn
    network_mode: host
    volumes:
      - './turnserver.conf:/etc/coturn/turnserver.conf:ro'
    restart: unless-stopped
ini
listening-port=3478
fingerprint
lt-cred-mech
realm=editor.internal
user=webrtc:CHANGEME-long-random-string
min-port=49160
max-port=49200
no-tls
no-dtls
verbose
log-file=stdout
no-stdout-log=false

Then set ICE_SERVERS in .env, using the same user name and password as the user= line above:

ini
ICE_SERVERS=turn:design.example.gov:3478|webrtc|CHANGEME-long-random-string

Replace CHANGEME-long-random-string in both places with a real secret, and open port 3478 plus the relay range (49160–49200) on the host.

Subpath Hosting

The Docker container is built with relative base paths (--base=./). It can be served from any domain root (https://example.com/) or subpath (https://example.com/system-design/) without rebuilding. Set APP_URL to the full public URL so shared session links format correctly.

Caching

Nginx inside the container automatically configures:

assets/* - Long-term immutable caching (Cache-Control: public, immutable). index.html & env-config.js - Revalidated on every request (Cache-Control: no-cache).

Documentation in the container

This documentation is built into the image and served at /docs/ alongside the editor, so a deployment carries its own copy and needs no internet access to read it.

The path is applied when the container starts, not when the image is built, so one image works anywhere:

SettingResult
Nothing setDocs at /docs/
APP_URL=https://example.gov/system-design/Docs at /system-design/docs/
DOCS_BASE=/help/Docs at /help/

DOCS_BASE wins where both are set. A value without a leading or trailing slash is corrected rather than rejected.

Why this is not a relative path like the editor's

The editor is built with a relative base, so a single build resolves its assets against wherever index.html was loaded from. VitePress cannot do that: it writes absolute asset URLs and hands its own client router a base to compare against the address bar. Getting it wrong is confusing rather than obviously broken - the page loads, then the router rewrites the URL to the path the build assumed. So the image is built with a placeholder, and the entrypoint replaces it with the path you are actually serving from.

System Design Editor Documentation