Four supported methods. They differ only in how the code gets onto the machine and who starts it — the requirements, the environment variables and the upgrade story are the same for all of them.
| Method | Pick this if | Section |
|---|---|---|
| Docker Compose | You want the database and Temporal brought up for you | Docker Compose |
| Bare metal | You run your own machine and want a systemd service | Bare metal |
| Pterodactyl | You use a Pterodactyl panel | Pterodactyl and Pelican |
| Pelican | You use a Pelican panel | Pterodactyl and Pelican |
Node.js 20 or newer (22 LTS recommended). Set by engines in
package.json and enforced at startup.
A glibc platform. Linux x64/arm64 on a glibc distribution (Debian, Ubuntu,
Rocky, …), macOS x64/arm64, or Windows x64. Alpine and other musl distributions
do not work: Temporal's native @temporalio/core-bridge addon ships no musl
build. The bot checks for this at startup and says so, rather than failing at
dlopen with a relocation error.
MySQL 8 or PostgreSQL 14+. SQLite is not supported — see MIGRATING.md if you are coming from a SQLite install.
A Temporal cluster. This is not optional. Every scheduled or long-running
job — inactivity warnings, auto-close, the reopen grace window, guild
export/import, cron automations, stats — runs as a Temporal workflow. The bot
refuses to start without TEMPORAL_ADDRESS and TEMPORAL_PORT. See
Reaching Temporal.
Disk: about 500 MB installed, most of it Temporal's prebuilt native addon.
Two directories, and nothing depends on the working directory:
| Contents | Override | |
|---|---|---|
| App directory | The code. Only written to when the schema changes. | DT_APP_DIR |
| Data directory | .env, user/ (config, transcripts, dumps), logs/. |
DT_DATA_DIR |
They are the same directory by default. Docker sets DT_DATA_DIR=/home/container
(the volume) while the code lives in /app. The environment file is
<data dir>/.env unless DT_ENV_FILE says otherwise.
An environment variable set to an empty string counts as unset, so a blank panel variable or a bare
KEY:in a compose file never shadows the value in.env.
Use docker-compose.yml. It brings up MySQL, a Temporal cluster with its own PostgreSQL, the Temporal Web UI and the bot.
curl -O https://raw.githubusercontent.com/Gelhaus-Solutions/discordtickets-revamped/main/docker-compose.yml
# read it, then set ENCRYPTION_KEY, DISCORD_TOKEN, DISCORD_SECRET, SUPER
docker compose up -d
docker compose logs -f botRead it in full before deploying: it ships with placeholder passwords and an insecure (plaintext) Temporal connection that are fine for local testing and not fine for production.
Generate the encryption key once and keep it:
openssl rand -hex 24Image tags:
| Tag | Contents |
|---|---|
:latest, :1, :1.4, :1.4.x |
Stable releases |
:main |
Built from every push to main |
Install from the release tarball. It ships the compiled Temporal layer and the dashboard build, so no build step is needed on the target machine.
# A user to run it as, and somewhere for its state
sudo useradd --system --home /var/lib/discord-tickets --create-home discord-tickets
sudo install -d -o discord-tickets -g discord-tickets -m 0750 /opt/discord-tickets
# The application
curl -fsSL -o /tmp/dt.tar.gz \
https://github.com/Gelhaus-Solutions/discordtickets-revamped/releases/latest/download/discordtickets-revamped.tar.gz
sudo -u discord-tickets tar -xzf /tmp/dt.tar.gz --strip-components=1 -C /opt/discord-tickets
cd /opt/discord-tickets
sudo -u discord-tickets npm ci --omit=dev --omit=optional
# The environment
sudo -u discord-tickets cp .env.example /var/lib/discord-tickets/.env
sudo -u discord-tickets chmod 600 /var/lib/discord-tickets/.env
sudoedit -u discord-tickets /var/lib/discord-tickets/.env # fill it inFirst run in the foreground, so you can watch the migrations:
sudo -u discord-tickets DT_DATA_DIR=/var/lib/discord-tickets sh scripts/start.shYou are looking for the [postinstall] block applying migrations, then
Listening at …. Stop it with Ctrl-C and install the service:
# /etc/systemd/system/discord-tickets.service
[Unit]
Description=Discord Tickets (revamped)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=discord-tickets
WorkingDirectory=/opt/discord-tickets
Environment=NODE_ENV=production
Environment=DT_DATA_DIR=/var/lib/discord-tickets
ExecStart=/usr/bin/env sh /opt/discord-tickets/scripts/start.sh
Restart=on-failure
RestartSec=10
KillSignal=SIGTERM
# The shutdown sequence drains the Temporal worker; give it room.
TimeoutStopSec=45
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/lib/discord-tickets /opt/discord-tickets
UMask=0077
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now discord-tickets
journalctl -u discord-tickets -f/opt/discord-tickets stays writable because the Prisma client is regenerated
there whenever the schema changes (a no-op on ordinary restarts).
sudo systemctl stop discord-tickets
# back up the database and /var/lib/discord-tickets first
curl -fsSL -o /tmp/dt.tar.gz \
https://github.com/Gelhaus-Solutions/discordtickets-revamped/releases/latest/download/discordtickets-revamped.tar.gz
sudo -u discord-tickets tar -xzf /tmp/dt.tar.gz --strip-components=1 -C /opt/discord-tickets
cd /opt/discord-tickets && sudo -u discord-tickets npm ci --omit=dev --omit=optional
sudo systemctl start discord-ticketsMigrations run automatically at startup and a failure stops the boot rather than letting the bot run against a half-migrated schema.
Only needed if you want to modify the code. The Temporal layer has to be compiled, which needs devDependencies:
git clone https://github.com/Gelhaus-Solutions/discordtickets-revamped.git
cd discordtickets-revamped
npm install --include=dev
npm run temporal.buildExporting
NODE_ENV=productionbeforenpm installmakes npm skip devDependencies — including TypeScript — and the Temporal build then cannot run. This is the single most common bare-metal install failure; the bot names it explicitly when it happens.
npm install also builds the dashboard, which the bot serves from
src/dashboard/build — that directory is generated, not committed. After
changing the dashboard, rebuild it with npm run dashboard.build. Set
DT_SKIP_DASHBOARD_BUILD=true to skip the build during install (the bot then
starts without a dashboard, and says so).
Import the egg for your panel:
- Pterodactyl: eggs/pterodactyl.json
- Pelican: eggs/pelican.json
Then create a server with it:
- Pick a Debian Node image (
ghcr.io/parkervcp/yolks:nodejs_22). Alpine images cannot run Temporal's native addon. - Allocate at least 1 GB of disk.
- Install the server. The install script downloads the release tarball,
installs production dependencies, and generates an encryption key into
/home/container/.env. - On the Startup tab, set at minimum:
DISCORD_TOKEN,DISCORD_SECRETDB_PROVIDER(mysqlorpostgresql) andDB_CONNECTION_URL— create the database through the panel's Databases tab, or point at an external oneTEMPORAL_ADDRESS, and theTEMPORAL_TLS_*variables unless the cluster is on a private network you controlSUPER— your own Discord user ID- Leave
ENCRYPTION_KEYempty to keep the key generated at install time.
- Start it. The console is ready when it prints
Listening at ….
HTTP_PORT and HTTP_HOST come from the allocation automatically, and
HTTP_EXTERNAL defaults to http://<server ip>:<port> — set it explicitly once
the bot is behind a domain, because it must match the OAuth redirect URI
configured in the Discord developer portal.
Reinstall Server. The install script replaces the code and leaves .env,
user/ and logs/ alone, so your encryption key, transcripts and config
survive. Pin a specific release by setting BOT_VERSION to a tag first.
Back up
/home/container/.envbefore any reinstall. Without that key, every encrypted ticket topic, close reason, feedback comment and archived message is unreadable — including in your database backups.
Temporal's addon ships prebuilt binaries for five platforms and only one is used. Removing the rest saves ~110 MB:
cd /home/container/node_modules/@temporalio/core-bridge/releases
ls # keep the one matching your platform, e.g. x86_64-unknown-linux-gnuThe S3 SDK is an optional dependency (~16 MB) and is only loaded if you set
STORAGE_DRIVER=s3. Installing with --omit=optional skips it; the panel
install script already does.
HTML transcripts are not stored in the database — it holds a reference like
local:transcripts/ticket-<id>.html and the bytes live wherever the configured
driver puts them.
The practical consequence: a database dump does not contain your transcripts. Back up the data directory alongside it.
Losing them is not fatal. Transcripts are rendered from the archived messages, which are in the database, so a missing or unreadable object is regenerated the next time someone opens it. Storage is a cache, not the system of record.
Nothing to configure. Transcripts are written to
<data dir>/user/transcripts/ticket-<id>.html, which is where they have always
been written.
For deployments where the data directory is not durable, or where transcripts have outgrown the disk. Set the driver and the bucket:
STORAGE_DRIVER=s3
S3_BUCKET=discord-tickets
S3_ENDPOINT=http://minio:9000 # omit for real AWS
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true # MinIO and most self-hosted gateways need this
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
Leave both credentials unset to use the AWS SDK's own credential chain, which
is how instance roles (IMDS, IRSA) work — prefer that where it is available.
The bucket details may also be set in user/config.yml under storage.s3, but
the environment wins, and credentials belong only in the environment.
The bucket is not created for you, and the bot does not stop if it is unreachable: it warns at startup and every read falls back to regenerating.
Testing against MinIO locally:
docker run -d --name minio -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
quay.io/minio/minio server /data --console-address ":9001"
mc alias set local http://127.0.0.1:9000 minioadmin minioadmin
mc mb local/discord-ticketsThere is a commented-out minio service in
docker-compose.yml for the same purpose.
Switching the driver does not move anything, and it does not need to: rows keep their own reference, so transcripts written before the switch keep being read from where they are. To move them anyway:
npm run transcripts -- --to s3 # dry run: says what it would do
npm run transcripts -- --to s3 -y # do it
npm run transcripts -- --to s3 -y --delete-source # ...and remove the originalsIt is a dry run unless you pass -y, it is idempotent — running it twice
changes nothing the second time — and it can be interrupted safely: each
transcript is copied and verified before its row is repointed, so a crash
leaves a stray object rather than a row pointing at nothing.
Run it with no --to to tidy the current driver in place: transcripts still
held in the database are written out, and references in the pre-4.x format are
rewritten. Add --prune-missing to clear references whose object has gone, so
those transcripts regenerate on next view instead of 404ing.
To delete stored transcripts belonging to tickets that no longer exist:
npm run transcripts -- --gc # dry run, lists what it would delete
npm run transcripts -- --gc -yThe collector only touches objects matching its own naming, skips anything
written in the last 24 hours, and refuses to run at all if the database
contains no tickets — a mistyped DB_CONNECTION_URL would otherwise look like
"nothing refers to any of these".
Three options, in the order most people should consider them.
1. Temporal Cloud. Simplest for panel and bare-metal users.
TEMPORAL_ADDRESS=<namespace>.<account>.tmprl.cloud
TEMPORAL_PORT=7233
TEMPORAL_NAMESPACE=<namespace>.<account>
TEMPORAL_TLS_ENABLED=true
TEMPORAL_TLS_CERT_PATH=/home/container/certs/client.pem
TEMPORAL_TLS_KEY_PATH=/home/container/certs/client.key
Upload the certificate and key with the panel's file manager (or scp them on
bare metal) and chmod 600 them. The bot checks at startup that both files
exist, instead of failing later inside the native addon.
2. Self-hosted, on your own machine. Copy the temporal,
temporal-postgresql and temporal-ui services out of
docker-compose.yml and run them alongside your bot,
then point TEMPORAL_ADDRESS at that host.
Plaintext gRPC on port 7233 has no authentication whatsoever. Anyone who can reach that port can read and modify workflow state, which contains ticket data.
TEMPORAL_TLS_ENABLED=falseis only acceptable when the port is bound to loopback or a private interface and firewalled. Otherwise terminate it with mTLS and set theTEMPORAL_TLS_*variables.
3. Running Temporal as a second panel server is not supported. It needs its own database plus schema tooling, and the egg does not provide it.
.env.example is the annotated reference and src/env.js is what actually validates them. The variables this fork adds or changes:
| Variable | Required | Default | Notes |
|---|---|---|---|
DB_PROVIDER |
yes | — | mysql or postgresql. |
TEMPORAL_ADDRESS |
yes | — | Host/IP of the Temporal frontend. |
TEMPORAL_PORT |
yes | — | Usually 7233. |
TEMPORAL_NAMESPACE |
no | default |
|
TEMPORAL_TASK_QUEUE |
no | discord-tickets |
|
TEMPORAL_DEPLOYMENT_NAME |
no | discord-tickets |
Worker Deployment name used for versioning. |
TEMPORAL_TLS_ENABLED |
no | true |
Set false only for a private/local cluster. |
TEMPORAL_TLS_CERT_PATH |
when TLS on | — | Client certificate (mTLS). Must exist. |
TEMPORAL_TLS_KEY_PATH |
when TLS on | — | Client private key. Must exist. |
TEMPORAL_TLS_CA_PATH |
no | — | Server root CA, if it is not publicly trusted. |
TEMPORAL_TLS_SERVER_NAME |
no | — | SNI override. |
TEMPORAL_WORKER_BUILD_ID |
no | build id from the release | Identifies the worker build. |
TEMPORAL_SET_CURRENT_ON_START |
no | true |
Promote this build to Current on startup. |
JWT_SECRET |
no | derived | Derived from ENCRYPTION_KEY via HKDF when unset. Set it to your old ENCRYPTION_KEY when upgrading from upstream to keep sessions valid. |
SUPER |
no | — | Comma-separated operator IDs. Also gates service API keys. |
STATS_URL |
no | — | Nothing is reported unless you set this. |
SENTRY_DSN |
no | — | Enables Sentry. Nothing is sent anywhere while this is unset. |
SENTRY_ENVIRONMENT |
no | NODE_ENV |
Labels events, e.g. production / staging. |
SENTRY_RELEASE |
no | discord-tickets@<version>+<build id> |
Must match uploaded source maps if you upload your own. |
SENTRY_SAMPLE_RATE |
no | 0.1 |
Fraction of requests and interactions traced, 0–1. |
SENTRY_PROFILING_RATE |
no | 1.0 |
Fraction of traced transactions profiled, 0–1. |
SENTRY_LOGGING |
no | false |
Forwards the bot's own log output to Sentry Logs. |
SENTRY_LOG_LEVEL |
no | info |
Minimum level forwarded. debug includes every database query. |
SENTRY_METRICS |
no | true |
Application metrics (tickets opened/closed, latencies). |
SENTRY_SEND_PII |
no | false |
Sends Discord user IDs and IP addresses. These identify your members, not you. |
PUBLIC_SENTRY_DSN |
no | — | Browser error reporting for the dashboard. Served to every visitor, so use a project separate from SENTRY_DSN. |
PUBLIC_SENTRY_TRACES_RATE |
no | 0 |
Browser performance tracing, 0–1. |
PUBLIC_SENTRY_REPLAY_SESSION_RATE |
no | 0 |
Session Replay sampling, 0–1. See the note below. |
PUBLIC_SENTRY_REPLAY_ERROR_RATE |
no | 0 |
Session Replay sampling for sessions that hit an error, 0–1. |
STORAGE_DRIVER |
no | local |
Where transcripts are kept: local or s3. See Transcript storage. |
S3_BUCKET |
when s3 |
— | Bucket transcripts are written to. |
S3_ENDPOINT |
no | AWS | Base URL of the gateway, e.g. http://minio:9000. Leave unset for real AWS. |
S3_REGION |
no | us-east-1 |
|
S3_PREFIX |
no | — | Key prefix within the bucket, e.g. production/. |
S3_FORCE_PATH_STYLE |
no | true |
Required by MinIO and most self-hosted gateways. |
S3_ACCESS_KEY_ID |
no | — | Leave unset to use the AWS SDK credential chain (instance roles). |
S3_SECRET_ACCESS_KEY |
no | — | |
S3_SESSION_TOKEN |
no | — | Only for temporary credentials. |
DT_DATA_DIR |
no | app directory | Where .env, user/ and logs/ live. |
DT_APP_DIR |
no | auto-detected | Where the code lives. |
DT_ENV_FILE |
no | <data dir>/.env |
|
DT_SKIP_MIGRATIONS |
no | — | true skips prisma migrate deploy at boot. Use when you migrate out of band or run several replicas — migrate deploy is not concurrency-safe. |
Sentry is entirely opt-in: with SENTRY_DSN unset, none of it runs.
If you do enable it, two settings decide whether other people's data leaves your
server. SENTRY_SEND_PII is false by default, so Discord user IDs and IP
addresses are not attached to events. And Session Replay is off by default,
because the dashboard renders ticket transcripts — recordings mask every text
node and block every image, but they still capture which pages your staff opened
and what they clicked. Turn both on deliberately, and tell your staff if you do.
PUBLIC_SENTRY_DSN is embedded in the pages the dashboard serves, so treat it as
public and point it at a different Sentry project from the bot's.
Everything else is documented upstream at https://discordtickets.app/self-hosting/configuration/#environment-variables.
npm run healthcheck # exit 0 when the HTTP server answersThe Docker image uses this for its HEALTHCHECK. GET /status returns 200 with
a body describing each subsystem; Temporal being unreachable reports
"degraded" rather than failing the check, deliberately — the bot still serves
Discord and the dashboard, and restarting it would not bring Temporal back.
Alert on the body, not the status code.
The Temporal layer (dist/temporal) is missing and TypeScript is not installed. You are running from a git checkout without devDependencies.
npm install --include=dev && npm run temporal.build. If you exported
NODE_ENV=production, unset it for the install.
This platform is not supported. Alpine/musl. Use a Debian or Ubuntu image.
The "DB_CONNECTION_URL" environment variable is required, but it is in my
.env. The error names the file the bot actually read. If that is not the
file you edited, set DT_DATA_DIR (or DT_ENV_FILE) to the right place.
The data directory … is not writable. The bot needs somewhere to put
user/ and logs/. Fix the permissions or point DT_DATA_DIR elsewhere.
Migrations fail with P3005. Your database has tables but no migration
history (an old prisma db push install, or a dump that excluded
_prisma_migrations). The bot baselines it automatically; if it cannot, the
error explains what to do.
Dashboard login loops. HTTP_EXTERNAL must exactly match the OAuth redirect
URI in the Discord developer portal, with no trailing slash.