What you need
Trycord runs as a single Node process. The default install needs little:
- Docker path: Docker with Compose. Nothing else on the host.
- Manual path: Node 22 or newer, and a place to keep the SQLite file.
- Either path: a
JWT_SECRET. The server has no default for it and will not start without one.
Generate a secret with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Every instance needs its own value. Changing it later signs everyone out and invalidates outstanding password-reset links.
Install with Docker
This is the recommended path, and the image is built for it.
git clone https://github.com/trycord/.trycord.git
cd .trycord
cp .env.docker.example .env
# edit .env and set JWT_SECRET
docker compose up -d
Three steps, and the only required value is the secret. Then open http://your-host:9971, create the first account, and promote it to platform administrator by adding its username to ADMIN_USERNAMES in .env and restarting.
docker compose logs -f # follow the boot log
docker compose ps # health status
curl -fsS localhost:9971/api/health
/api/health answers 200 only when the database is reachable, so it is a real readiness signal rather than "the process is up".
Install by hand
Choose this if you need systemd units, a specific Node version, or a reverse proxied layout that a container makes awkward.
cd backend
npm install
cp .env.example .env
# edit .env: JWT_SECRET, SERVER_HOST_TYPE and a database profile
npm run seed
npm start
The server finds the web client next to itself in the default layout. If yours differs, point it at the directory holding index.html:
TRYCORD_CLIENT_DIR=/srv/trycord npm start
A relative value resolves against backend/. If no client is found, the API still runs and the server lists every path it looked at.
The settings that matter
backend/.env.example documents every variable; .env.docker.example covers the container. The ones worth understanding:
| Setting | What it does |
|---|---|
JWT_SECRET | Signs sessions and reset tokens. Required, no default. |
SERVER_HOST_TYPE | express when the server is the public endpoint, nginx when a reverse proxy fronts it. Anything else is a startup error. |
TRUST_PROXY | Set to 1 only when a reverse proxy on the same host fronts the server. |
CLIENT_ORIGIN | Comma-separated allowlist of origins allowed to read API responses. Empty means a restricted development profile. |
ADMIN_USERNAMES | Usernames promoted to platform administrator at boot. Idempotent. |
TRYCORD_NAME | The instance's display name. |
Choose a database
The database is a profile, not a different build. SQLite is a single file and is fine for small and mid-size instances; move to MySQL when you outgrow it.
DB_CLIENT=sqlite
DB_FILE=/data/trycord.db
# or
DB_CLIENT=mysql
DB_HOST=db
DB_PORT=3306
DB_NAME=trycord
DB_USER=trycord
DB_PASSWORD=change-me
A database host that is not loopback is refused unless ALLOW_REMOTE_DB is exactly true, so a CI job that inherits a copied .env cannot quietly write to a live database.
Configure mail
Mail powers email verification and password recovery, and nothing else depends on it. There are three modes:
- disabled (the default): nothing is sent, and recovery flows refuse rather than claim success.
- smtp: delivery through a relay. Port 465 implies TLS, 587 implies STARTTLS.
- api: the server posts JSON to an HTTPS endpoint you name field by field, so no vendor is baked in.
With mail off, email verification is not enforceable and the app will not ask anyone to verify an address.
Put it behind a reverse proxy
Forward both HTTP and WebSocket traffic, set SERVER_HOST_TYPE=nginx and TRUST_PROXY=1, and keep the upgrade headers, or the realtime gateway will not connect:
location / {
proxy_pass http://127.0.0.1:9971;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 7d;
}
The first WebSocket frame carries a single-use ticket, so a short idle timeout looks like a silent disconnect every minute. Set the read timeout generously.
Who may call the API
CLIENT_ORIGIN is a comma-separated allowlist. Leave it empty and the server runs a restricted development profile; set it and the allowlist is enforced exactly, which is what a public instance wants. Three forms are accepted:
| Entry | Matches |
|---|---|
https://trycord.example | that exact origin only |
http://*.trycord.example | any single subdomain, not the apex |
trycord://app | the desktop app |
Scheme and port must match exactly, a wildcard never bridges them, and a bare * is rejected. Always include trycord://app if the desktop client should reach the instance.
Back up and upgrade
With Docker, your data lives in two named volumes that survive docker compose down and upgrades: trycord-data for the database and trycord-uploads for avatars and attachments. Back up a volume by copying it out:
docker run --rm -v trycord-data:/data -v "$PWD":/backup \
alpine tar czf /backup/trycord-data.tar.gz -C /data .
To upgrade, pull and rebuild. Your volumes are untouched.
git pull
docker compose build --pull
docker compose up -d
Point the desktop app at it
The packaged desktop app picks its backend at launch. Give it yours and it runs against that instance and nothing else:
Trycord --api-url=https://chat.example.org
The shipped build reaches the official instance by default. That is a default, not a requirement. See the download page for how the app is put together.
Run a status monitor
The status page at /status works on its own: it probes the instance from the reader's browser. You can also run the bundled status monitor, a second process on its own port that checks the instance on a schedule and serves a report. Because it is a separate process, it keeps answering when the API cannot.
cd backend
npm run status-monitor
It checks /, /health and /api/health by default, and exposes an HTML dashboard, /status.json and Prometheus metrics. Every setting is configurable, by environment variable or by a JSON file. Set STATUS_MONITOR_PUBLIC_URL on the server to have the public status page read the monitor's live report instead of probing from the browser.
With Docker, start the same image a second time through the override file:
docker compose -f docker-compose.yml -f docker-compose.monitor.yml up -d
Where the rest lives
The docs/ directory holds the long-form references. They ship with the code, so they are the version that matches the server you are running.
Deployment and running
selfhosting.md covers the whole deployment: the database, mail, the reverse proxy, the volumes, the upgrades. backups.md covers taking a backup and putting it back.
The parts of the system
architecture.md says how the API, the gateway, the client and the jobs fit together. services.md lists the services and what each one is for.
Voice and media
voice.md covers the relay, the credentials it mints, the stunkey another host needs, and the page that says whether the stack is working.
The client and the shell
desktop.md and launcher.md cover the desktop application. theming.md covers the design tokens, pages.md the editable pages, and extensions.md the extension API.
Running a community
community-settings.md covers the settings a community administrator has, and continuum.md covers federation between instances.
Checks and audits
verification-matrix.md says what is checked and how. production-readiness.md, dashboard.md and the two audit files record what was found and what was done about it.
If you find an error in this page, the support page explains where to say so.