Skip to the article
Aniq-UI

TicketoryDeploy Ticketory

Deploy Ticketory

Put the website, the dashboard and the API online: Docker Compose on one server, one app per host from its Dockerfile, or plain Node.js.

For the Full Stack package

Three ways to deploy

WayGood forWhat you need
Docker Compose on one serverA VPS you control, all three apps togetherThe Full Stack package, Docker on the server, a reverse proxy for HTTPS
One app per host, from its DockerfileRailway, Render, Fly.io and other hosts that build from a DockerfileEach app's folder; a volume or MySQL and a bucket for the API
Plain Node.jsAny Node hostNode.js 22 or newer and Yarn 4 on the host

Whichever way you choose, the website and the dashboard compile the API's address into their JavaScript when they are built, so build them with the public addresses, never the local ones.

Docker Compose on one server

Copy the package folder to the server and put a .env beside docker-compose.yml with the three public addresses. The API's allowed origins, the addresses payment pages send buyers back to, the ticket download links, the picture addresses and the frontends' API address are all built from them. REVALIDATE_SECRET replaces the local default the website and the API share to refresh event pages at once.

.env beside docker-compose.yml
SITE_URL=https://www.your-domain.com
ADMIN_URL=https://dashboard.your-domain.com
API_URL=https://api.your-domain.com
MEDIA_HOSTNAME=api.your-domain.com
REVALIDATE_SECRET=a-long-random-string

MEDIA_HOSTNAME is optional, one host or several comma separated (add your bucket's host when uploads go to one): pictures on those https hosts are resized by Next.js, and any other picture loads as it is. Then build and start in the background:

Terminal
docker compose up --build -d
  • Point each domain at the server through a reverse proxy that serves HTTPS (Caddy, Nginx or Traefik), forwarding to ports 3030, 3031 and 8000.
  • Behind that proxy, set TRUST_PROXY=1 in back-end/.env, so the sign-in, contact form and door scan limits count each visitor rather than the proxy. The API container reads back-end/.env when it exists, which is also where the payment, mail, AI and storage keys go.
  • Without a JWT_SECRET of your own, the API generates one on its first start and keeps it in the ticketory-data volume. The database, the uploads and the CSV exports live in that volume too, so back it up.
  • The first start on a new volume seeds the sample organisers unless SEED_DEMO_DATA=false is in the .env. For your own events, start without them and with your own Owner, as in Going live with your own workspace below.

The API from its Dockerfile

Point the host at the API's folder (back-end/ in the Full Stack). The image listens on PORT (8000 unless the host sets another), answers GET /api/health once its database answers, and keeps SQLite, the uploads and the CSV exports in /data.

  • Mount a persistent volume at /data. Without one, every deploy starts on an empty database.
  • Set CORS_ORIGIN to the website's and the dashboard's addresses (comma separated), FRONTEND_URL to the dashboard's, STOREFRONT_URL to the website's, PUBLIC_API_URL to the API's public address and PUBLIC_MEDIA_URL to that address followed by /media.
  • The image's default is SEED_DEMO_DATA=false: a new volume gets the tables only, and the API's first start then creates your workspace and its Owner from OWNER_EMAIL and OWNER_PASSWORD (see below). Set SEED_DEMO_DATA=true instead for the sample organisers. An existing database is never seeded again.
  • Set your own JWT_SECRET, or let the container generate one and keep it in /data/.jwt-secret.
  • Set TRUST_PROXY=1: a hosting platform puts a proxy in front of the container.
  • On MySQL (DB_TYPE=mysql and the five DB_* variables), the container does not create the tables. Run node dist/database/seeder.js (sample data) or node dist/database/sync-schema.js (empty tables) once with the same environment.
  • Uploads on the volume survive a redeploy; to keep them off the server, set the five R2_* variables for a bucket.
  • Set REVALIDATE_SECRET to the same long random value on the API and the website, and STOREFRONT_INTERNAL_URL when the API reaches the website at another address than STOREFRONT_URL.

The website and the dashboard from their Dockerfiles

Point the host at the app's folder and give it the NEXT_PUBLIC_* values as build arguments, not runtime variables: they are compiled in. Each image serves Next.js's standalone server on PORT (3030 for the website, 3031 for the dashboard, unless the host sets another).

Build argumentAppValue
NEXT_PUBLIC_API_BASE_URLBothThe API's public address followed by /api
NEXT_PUBLIC_SITE_URLBothThe website's public address: canonical URLs and the sitemap on the website, the "View website" link and the event previews in the dashboard
NEXT_PUBLIC_DASHBOARD_URLBothThe dashboard's public address: where the website's "Sell tickets" sends organisers, and the dashboard's own sharing picture
NEXT_PUBLIC_MEDIA_HOSTNAMEBothOptional. The https hosts serving the pictures (the API's domain, your bucket's, or both), comma separated
NEXT_PUBLIC_WEBSOCKET_BASE_URLDashboardThe API's public address without /api, for live notifications

The website's server also fetches from the API while it renders event, venue and organiser pages and the sitemap. With a public API address that works from the server too, nothing more is needed; API_INTERNAL_URL at run time overrides it when the server must use another address. REVALIDATE_SECRET is a run-time variable of the website too, never a build argument.

Plain Node.js

Build each app once on the host, then start it. Set the frontends' .env to the public addresses before yarn build.

AppBuildStart
APIyarn build, then once yarn seed:prod (sample data) or yarn db:sync:prod (empty tables)yarn start:prod
Public websiteyarn buildyarn start (port 3030)
Organiser dashboardyarn buildyarn start (port 3031)

The API binds 0.0.0.0 and reads PORT. Keep the SQLite file and MEDIA_UPLOAD_DIR on storage that survives a deploy, or use MySQL and a bucket.

Going live with your own workspace

The sample organisers and their sign-ins are for trying Ticketory. For your own events, start on a new database without them: on a database with no staff account and no workspace, the API's first start creates the base data (the 29 permissions, the four roles, the site settings and the three event categories), one empty workspace and its Owner.

  1. Set the owner variables

    In back-end/.env, or in your host's configuration for the API. With Docker Compose, put them in the .env beside docker-compose.yml together with SEED_DEMO_DATA=false: Compose reads that file first and then back-end/.env, whose value wins where both set one.

    back-end/.env
    OWNER_EMAIL=you@your-domain.com
    OWNER_PASSWORD=a-long-password-of-your-own
    OWNER_NAME=Your Name
    WORKSPACE_NAME=Your organisation

    Expected result: OWNER_NAME and WORKSPACE_NAME are optional (Owner and Ticketory when unset), and both change later, on your profile and under Settings, General. The password needs at least 10 characters, and with NODE_ENV=production the example value from .env.example and the sample accounts' password are refused.

  2. Start without the sample data, on a new database

    Without Docker, in the API's folder: yarn build, then yarn db:sync:prod (not yarn seed), then yarn start:prod. With Docker Compose, keep SEED_DEMO_DATA=false in the .env beside docker-compose.yml and run docker compose up --build -d on a new volume (docker compose down -v deletes an existing one, with all its data). An API container alone starts without -e SEED_DEMO_DATA=true, with the variables passed by --env-file .env or -e.

    Expected result: The API's log says Clean start: created the workspace "Your organisation" and its Owner, with the email.

    Having trouble?
  3. Sign in and set up the workspace

    Sign in to the dashboard with OWNER_EMAIL and OWNER_PASSWORD. The workspace is empty: add a venue, then your first event. Its currency is USD until you change it under Settings, General, which you can do until the first ticket is paid. Then invite your team from the Team page, each with the role they need.

  • It happens once. As soon as any staff account exists, the owner variables are ignored, so a restart or a second copy of the API creates nothing more, and you may remove OWNER_PASSWORD from the configuration afterwards. The password is never printed.
  • yarn seed and a database filled with the sample data never read them: such a database already has staff.
  • The dashboard has no sign-up page. Every other staff account is added with Invite member on the Team page, with a role and a password you share yourself (nothing is emailed).

Starting from the sample data instead

A database filled with the sample data keeps its four organisers and their accounts. If you start from it, make it yours before it is reachable from the internet:

  1. Make the seeded Owner yours: change its name and email under Settings, Profile, and its password under Settings, Security. It is the one account that belongs to every sample workspace, and no screen adds an existing member to another workspace, so keep it rather than replacing it with a new account.
  2. Remove the seeded members you do not need from the Team page of each workspace, or change their passwords. Remove takes a member out of the workspace you are working in; the account is deleted with its last workspace. You cannot remove yourself, and each workspace keeps one Owner.
  3. Set the workspace's name, support email, currency, booking fee, time zone and the team's notifications under Settings, General and Notifications. The currency locks once the first paid order is taken.
  4. Archive or cancel the sample events and replace the sample venues with your own, with their seat layouts. There is no screen or route to create another workspace, so rename one under Settings, General for your own business.
  5. Attendees create their own accounts on the website, or buy as guests. The seeded attendee is sample data: delete it with the rest.

Updating an install you already run

With NODE_ENV=production the API never changes the tables itself. After an update that adds a column or a table, synchronise the schema once on the existing database, before you start the new version. This one adds the locale column on orders (the language of each order's emails) and the event_reminders table.

Without Docker, in the API's folder:

Terminal
yarn build
yarn db:sync:prod

It writes no rows and keeps your data. With Docker the container leaves an existing database alone, so after docker compose up --build -d run the same script once in the running API container, then restart it (for an API container alone, docker exec -u node <container> node dist/database/sync-schema.js, then docker restart <container>):

Terminal
docker compose exec -u node api node dist/database/sync-schema.js
docker compose restart api

With NODE_ENV=development, as with yarn dev, the API updates the tables by itself on start.

Going live

Before Ticketory is reachable from the internet:

  1. In the API's environment, NODE_ENV=production and a long random JWT_SECRET of your own (Docker generates one when none is set). In production the API refuses to start without CORS_ORIGIN, and says which value is missing.
  2. CORS_ORIGIN at the website and the dashboard, FRONTEND_URL at the dashboard, STOREFRONT_URL at the website, PUBLIC_API_URL at the API and PUBLIC_MEDIA_URL at the API followed by /media.
  3. TICKET_SECRET set once, before the first sale: it signs the QR codes, and changing it later voids every ticket already sold.
  4. Your live Stripe or PayPal keys, and the webhooks registered with each provider at /api/webhooks/stripe and /api/webhooks/paypal on the API's address. With a live key set, Test payment disappears from checkout.
  5. Tax set up for your country: Ticketory charges none by itself.
  6. RESEND_API_KEY and MAIL_FROM, or attendees receive no ticket emails, sign-up codes or password resets.
  7. The database, the uploads and the CSV exports on persistent storage, with a backup.
  8. The host's health check at /api/health, and TRUST_PROXY=1 behind a proxy.
  9. Both frontends built with the public NEXT_PUBLIC_* values, and the same REVALIDATE_SECRET on the API and the website.
  10. On the website, your brand in src/config/brand.config.ts, and the privacy, terms and cancellation texts in messages/legal rewritten for your business and your country.
  11. Your own workspace from a clean start, or the seeded passwords changed and the seeded accounts you do not need removed.
  12. DEMO_MODE=false on the API and NEXT_PUBLIC_DEMO_MODE=false in both frontends, as shipped.

Not for live sales data

yarn setup:fresh builds, drops every table and seeds, every time it runs, and yarn db:reset drops every table. They suit a brand new environment, never a start or build command.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.