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
| Way | Good for | What you need |
|---|---|---|
| Docker Compose on one server | A VPS you control, all three apps together | The Full Stack package, Docker on the server, a reverse proxy for HTTPS |
| One app per host, from its Dockerfile | Railway, Render, Fly.io and other hosts that build from a Dockerfile | Each app's folder; a volume or MySQL and a bucket for the API |
| Plain Node.js | Any Node host | Node.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.
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-stringMEDIA_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:
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,3031and8000. - Behind that proxy, set
TRUST_PROXY=1inback-end/.env, so the sign-in, contact form and door scan limits count each visitor rather than the proxy. The API container readsback-end/.envwhen it exists, which is also where the payment, mail, AI and storage keys go. - Without a
JWT_SECRETof your own, the API generates one on its first start and keeps it in theticketory-datavolume. 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=falseis 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_ORIGINto the website's and the dashboard's addresses (comma separated),FRONTEND_URLto the dashboard's,STOREFRONT_URLto the website's,PUBLIC_API_URLto the API's public address andPUBLIC_MEDIA_URLto 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 fromOWNER_EMAILandOWNER_PASSWORD(see below). SetSEED_DEMO_DATA=trueinstead 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=mysqland the fiveDB_*variables), the container does not create the tables. Runnode dist/database/seeder.js(sample data) ornode 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_SECRETto the same long random value on the API and the website, andSTOREFRONT_INTERNAL_URLwhen the API reaches the website at another address thanSTOREFRONT_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 argument | App | Value |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | Both | The API's public address followed by /api |
NEXT_PUBLIC_SITE_URL | Both | The 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_URL | Both | The dashboard's public address: where the website's "Sell tickets" sends organisers, and the dashboard's own sharing picture |
NEXT_PUBLIC_MEDIA_HOSTNAME | Both | Optional. The https hosts serving the pictures (the API's domain, your bucket's, or both), comma separated |
NEXT_PUBLIC_WEBSOCKET_BASE_URL | Dashboard | The 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.
| App | Build | Start |
|---|---|---|
| API | yarn build, then once yarn seed:prod (sample data) or yarn db:sync:prod (empty tables) | yarn start:prod |
| Public website | yarn build | yarn start (port 3030) |
| Organiser dashboard | yarn build | yarn 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.
Set the owner variables
In
back-end/.env, or in your host's configuration for the API. With Docker Compose, put them in the.envbesidedocker-compose.ymltogether withSEED_DEMO_DATA=false: Compose reads that file first and thenback-end/.env, whose value wins where both set one.back-end/.envOWNER_EMAIL=you@your-domain.com OWNER_PASSWORD=a-long-password-of-your-own OWNER_NAME=Your Name WORKSPACE_NAME=Your organisationExpected result:
OWNER_NAMEandWORKSPACE_NAMEare optional (OwnerandTicketorywhen unset), and both change later, on your profile and under Settings, General. The password needs at least 10 characters, and withNODE_ENV=productionthe example value from.env.exampleand the sample accounts' password are refused.Start without the sample data, on a new database
Without Docker, in the API's folder:
yarn build, thenyarn db:sync:prod(notyarn seed), thenyarn start:prod. With Docker Compose, keepSEED_DEMO_DATA=falsein the.envbesidedocker-compose.ymland rundocker compose up --build -don a new volume (docker compose down -vdeletes 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 .envor-e.Expected result: The API's log says
Having trouble?Clean start: created the workspace "Your organisation" and its Owner, with the email.Sign in and set up the workspace
Sign in to the dashboard with
OWNER_EMAILandOWNER_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_PASSWORDfrom the configuration afterwards. The password is never printed. yarn seedand 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:
- 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.
- 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.
- 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.
- 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.
- 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:
yarn build
yarn db:sync:prodIt 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>):
docker compose exec -u node api node dist/database/sync-schema.js
docker compose restart apiWith 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:
- In the API's environment,
NODE_ENV=productionand a long randomJWT_SECRETof your own (Docker generates one when none is set). In production the API refuses to start withoutCORS_ORIGIN, and says which value is missing. CORS_ORIGINat the website and the dashboard,FRONTEND_URLat the dashboard,STOREFRONT_URLat the website,PUBLIC_API_URLat the API andPUBLIC_MEDIA_URLat the API followed by/media.TICKET_SECRETset once, before the first sale: it signs the QR codes, and changing it later voids every ticket already sold.- Your live Stripe or PayPal keys, and the webhooks registered with each provider at
/api/webhooks/stripeand/api/webhooks/paypalon the API's address. With a live key set, Test payment disappears from checkout. - Tax set up for your country: Ticketory charges none by itself.
RESEND_API_KEYandMAIL_FROM, or attendees receive no ticket emails, sign-up codes or password resets.- The database, the uploads and the CSV exports on persistent storage, with a backup.
- The host's health check at
/api/health, andTRUST_PROXY=1behind a proxy. - Both frontends built with the public
NEXT_PUBLIC_*values, and the sameREVALIDATE_SECRETon the API and the website. - On the website, your brand in
src/config/brand.config.ts, and the privacy, terms and cancellation texts inmessages/legalrewritten for your business and your country. - Your own workspace from a clean start, or the seeded passwords changed and the seeded accounts you do not need removed.
DEMO_MODE=falseon the API andNEXT_PUBLIC_DEMO_MODE=falsein 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.