Setup & Usage

Installation

This page walks through bringing IT Ticket System up from scratch: the development environment, environment variables, production deployment, the Coolify + Nginx Proxy Manager topology, database migrations, and fixes for the most common problems. For the reasoning behind the architecture see the Architecture page, and for the security checklist see the Security page.

Requirements

  • Docker and Docker Compose (v2, the docker compose command).
  • Nothing else is needed — Node.js, PostgreSQL, and Redis run inside containers.
  • openssl for generating secrets (ships by default on macOS/Linux).

Development environment

git clone https://github.com/mahmutyum/ticket-system.git
cd ticket-system
cp .env.example .env

Fill in the changeme_* values in .env (see the Environment variables section), then:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build

This command:

  • Brings up Postgres and Redis and waits for them to become healthy.
  • Applies the database schema (prisma migrate deploy).
  • Starts the backend with tsx watch (hot reload) and the frontend with vite.
  • Sets NODE_ENV to development: readable logs, verbose error detail.

The source code is bind-mounted — changes you make under backend/src or frontend/src are reflected without restarting the container.

Load the sample data (3 companies, locations, categories, staff, templates):

docker compose exec backend npx tsx prisma/seed.ts

To evaluate the tickets, tasks, reports, onsite support, and password vault screens with populated data, then run the synthetic detail seed:

docker compose exec backend npm run db:seed:demo

The detail seed is re-runnable and manages only the fictional records explicitly tagged DEMO. It contains no real system, personal, or password data, and refuses to run when NODE_ENV=production.

AddressWhat
http://localhost:1111Interface (public portal + staff panel)
http://localhost:1111/staff/loginStaff login
http://localhost:4000/docsAPI endpoint list (Swagger UI)
http://localhost:4000/health/liveProcess liveness check
http://localhost:4000/health/readyPostgreSQL + Redis readiness check

Seed credentials: admin@company.com / admin123 · manager@company.com / staff123 · it@company.com / staff123

The seed is for demo only. Do not run prisma/seed.ts against a production database.

To stop, press Ctrl+C. To also delete the data:

docker compose down -v   # -v also removes volumes: the database is reset

Environment variables

The single source of truth is .env.example. If you use Coolify, you manage these variables from the panel — no code change required.

Required — no default; the backend will not start if left empty

VariableHow to generate / what to enter
JWT_SECRETopenssl rand -base64 48 — at least 32 characters
JWT_REFRESH_SECRETopenssl rand -base64 48 — must be different from JWT_SECRET
CREDENTIALS_ENC_KEYopenssl rand -hex 32exactly 64 hex characters
DB_PASSWORDFree choice. Enter the same value in DATABASE_URL.
REDIS_PASSWORDFree choice. Enter the same value in REDIS_URL.
DATABASE_URLpostgresql://<DB_USER>:<DB_PASSWORD>@postgres:5432/<DB_NAME>
REDIS_URLredis://:<REDIS_PASSWORD>@redis:6379
APP_URLThe FQDN the system will be reached at. Separate multiple values with commas.
SMTP_HOST SMTP_USER SMTP_PASS SMTP_FROMGlobal SMTP. Zod requires these; a value must be provided even if you will not send email.

About CREDENTIALS_ENC_KEY: It encrypts the entries in the password vault (/staff/passwords) with AES-256-GCM. If you lose or change this key, every password in the vault becomes permanently undecryptable. Validation checks not only the length but also the hex character set at startup. Generate the value with openssl rand -hex 32.

Why is REDIS_PASSWORD in two places? It is used both when the redis container is started with --requirepass and inside REDIS_URL. If the two differ, the backend cannot connect to Redis. The same applies to DB_PASSWORD / DATABASE_URL.

APP_URL and CORS: It takes a comma-separated list. The first value is treated as canonical and is used in the tracking links inside emails; the entire list is added to the CORS whitelist. e.g. APP_URL=https://ticket.firma.com,https://destek.firma.com

Backend variables with defaults

VariableDefaultWhat it does
NODE_ENVdevelopmentproduction: JSON logs, 500 error details hidden
PORT4000Port the backend listens on inside the container
APP_NAMEIT Destek SistemiApplication name
ACCESS_TOKEN_EXPIRY15mAccess token lifetime
REFRESH_TOKEN_EXPIRY7dRefresh token lifetime
SMTP_PORT587
SMTP_SECUREfalsetrue means TLS (usually port 465)
MAX_FILE_SIZE26214400 (25 MB)Upper limit for file attachments
UPLOAD_DIR/app/uploadsIn-container upload directory (bound to a volume)
SMS_GATEWAY_URL SMS_GATEWAY_API_KEY SMS_SENDEROptional. If empty, no SMS is sent.

Variables used only by Docker Compose

VariableDefaultWhat it does
FRONTEND_PORT1111The single port exposed to the host. NPM forwards here.
DB_NAMEticketdb
DB_USERticket
BACKEND_PORT DB_PORT REDIS_PORT4000 5432 6379Exposed to the host only when docker-compose.local.yml is active
NGINX_HTTP_PORT NGINX_HTTPS_PORT80 443Only with --profile proxy
PGADMIN_PORT PGADMIN_EMAIL PGADMIN_PASSWORD5050Only with --profile tools

Production

cp .env.example .env      # fill in the values
./scripts/check-production-env.sh .env
docker compose up -d --build

What happens:

  • The backend image is built; on startup prisma migrate deploy runs, then node dist/server.js.
  • Only frontend is exposed to the host (FRONTEND_PORT). backend, postgres, and redis are only on the internal app-networknot exposed to the host.
  • The uploads and pgdata named volumes keep data persistent.

For production, .env must have:

  • NODE_ENV=production
  • APP_URL set to your real FQDN(s)
  • All changeme_* values replaced with real, randomly generated values

If you need the internal nginx proxy (when there is no NPM/Coolify):

docker compose --profile proxy up -d --build

This profile uses nginx/conf.d/default.conf: HTTP→HTTPS redirect, TLS 1.2/1.3, rate limiting, and an IP whitelist restricted to RFC1918 private networks (external requests get a 403). Adjust server_name and the VPN subnets for your own environment. nginx/Dockerfile generates a self-signed certificate at build time — replace it with your own certificate in a real deployment.

Coolify + Nginx Proxy Manager

Internet/VPN → NPM (SSL + FQDN) → frontend:1111 ─┬─ /          → SPA
                                                  ├─ /api/*     → backend:4000
                                                  └─ /uploads/* → backend:4000
  1. Add the repo in Coolify — docker-compose.yml is detected automatically.
  2. Enter all .env variables from the Coolify environment panel.
  3. Create a proxy host in NPM:
    • Domain Names: ticket.firma.com (multiple can be entered)
    • Scheme: http · Forward Hostname/IP: the Coolify host IP · Forward Port: FRONTEND_PORT (1111)
    • Websockets Support: ONrequired for SSE (live updates)
    • Block Common Exploits: ON
    • SSL: Let's Encrypt or your own certificate · Force SSL: ON · HTTP/2: ON
  4. Write all of the FQDNs you defined in NPM into APP_URL, separated by commas — otherwise CORS will reject the requests.

The backend only trusts the number of hops specified by TRUST_PROXY. In the NPM/Coolify + frontend nginx topology the typical value is 2; verify your topology against the checklist on the Security page.

Database migrations

The schema lives in backend/prisma/schema.prisma, and versioned migrations are kept under backend/prisma/migrations/.

  • Automatic on startup: both the dev and production compose files run prisma migrate deploy. It is idempotent — it skips already-applied migrations and runs safely on every restart.

  • When you change the schema, generate a new migration:

    docker compose exec backend npx prisma migrate dev --name aciklayici_bir_ad

    Review the generated SQL and include it in the commit.

Do not use prisma db push. It skips the migration history and can silently drop columns/tables with --accept-data-loss. This project used to rely on that approach; it has now moved to versioned migrations.

Baselining an existing database

If you have an already running database created with prisma db push, migrate deploy will say:

Error: P3005 The database schema is not empty.

Because Prisma sees the tables but finds no record of any applied migration. A one-time baseline is required.

1. First, verify the schema actually matches. If the output is empty, your database is identical to 0_init:

docker compose exec backend npx prisma migrate diff \
  --from-url "$DATABASE_URL" \
  --to-schema-datamodel prisma/schema.prisma \
  --script

2. If the output is empty, mark 0_init as applied (this runs no SQL, it only records it):

docker compose exec backend npx prisma migrate resolve --applied 0_init

3. If the output is not empty, your database has drifted from the schema. The SQL above is the difference — do not apply it automatically. Review it, take a backup if needed, then apply it by hand or turn it into a migration. When in doubt, try it on a copy first.

Before baselining, take a database backup:

docker compose exec postgres pg_dump -U ticket ticketdb > backup.sql

Connecting with local tools

To connect with tools like DBeaver or RedisInsight, use the override that exposes Postgres/Redis only to 127.0.0.1:

docker compose -f docker-compose.yml -f docker-compose.local.yml up -d
ServiceAddress
Postgres127.0.0.1:5432
Redis127.0.0.1:6379
Backend127.0.0.1:4000

If you prefer PgAdmin:

docker compose --profile tools up -d pgadmin   # http://localhost:5050

Do not use docker-compose.local.yml in production — it exposes the database to the host network.

Troubleshooting

The backend shuts down as soon as it starts, with Invalid environment variables in the log Zod validation failed. The log states which variable is at fault. The most common cause: CREDENTIALS_ENC_KEY is missing or not 64 characters, or JWT_SECRET is shorter than 32 characters.

CREDENTIALS_ENC_KEY must be 64 hex characters Generate it with openssl rand -hex 32. Make sure no quotes or spaces remain.

Invalid key length in the password vault / passwords cannot be decrypted CREDENTIALS_ENC_KEY is 64 characters but not valid hex (validation only checks the length), or the records were encrypted with a different key. If the old key is lost, those records cannot be recovered.

P3005 The database schema is not empty See the Baselining an existing database section.

The backend cannot connect to Redis The password in REDIS_PASSWORD differs from the one inside REDIS_URL. They must match. Make the same check for DB_PASSWORD / DATABASE_URL.

CORS error in the browser APP_URL does not include the FQDN you are accessing the site from. Add it separated by a comma and restart the backend.

Live updates (SSE) do not arrive in the panel Websockets Support: ON must be set on the NPM proxy host. If there is another proxy in between, it must disable response buffering.

Frontend loads but the API returns 502 The backend cannot pass its healthcheck. Check with docker compose logs backend — usually a migration or env error.

File upload returns 413 The upload exceeds MAX_FILE_SIZE (default 25 MB) or the nginx client_max_body_size (frontend/nginx.conf, 25M). Increase both together.