Deploy the agency
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 dashboard's address, the photo addresses and the frontends' API address are all built from them.
SITE_URL=https://www.your-agency.com
ADMIN_URL=https://admin.your-agency.com
API_URL=https://api.your-agency.com
MEDIA_HOSTNAME=api.your-agency.comMEDIA_HOSTNAME is optional, one host or several comma separated (add your bucket's host when uploads go to one): photos on those https hosts are resized by Next.js, and any other photo 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 viewing request limits count each visitor rather than the proxy. The API container readsback-end/.envwhen it exists, which is also where the optional keys go. - Without a
JWT_SECRETof your own, the API generates one on its first start and keeps it in thevelra-datavolume. The database and the uploads live in that volume too, so back it up. - The first start seeds the demo agency unless
SEED_DEMO_DATA=falseis in the.env. Keep it for the first start: the seed is what creates the accounts.
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 and the uploads 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, andPUBLIC_MEDIA_URLto the API's public address followed by/media. - Set
SEED_DEMO_DATA=truefor the first start on a new volume: the image's default isfalse, which creates the tables with no accounts. 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(demo agency) 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.
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 in the dashboard |
NEXT_PUBLIC_MEDIA_HOSTNAME | Both | Optional. The https hosts serving the photos (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 property 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.
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 (demo agency) or yarn db:sync:prod (empty tables) | yarn start:prod |
| Public website | yarn build | yarn start (port 3030) |
| Agency 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.
Your first accounts
There is no sign-up page: the two dashboard accounts come from the seed. Start the first time with the demo agency, sign in, then make it yours:
- Change both passwords under Settings, Security, or create your own Super Admin in Admin management and delete the seeded accounts.
- Archive or delete the sample properties, and delete the sample neighborhoods you do not need.
- Close the sample inquiries and cancel the sample viewings: they have no delete.
- Set the agency's name, tagline, description and contact email under Settings, Agency, and the homepage's featured homes under Settings, Homepage.
Going live
Before the agency 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,PUBLIC_MEDIA_URLat the API followed by/media.- The database and the uploads 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. - On the website, your brand in
src/config/brand.config.ts, and the privacy policy inmessages/privacyrewritten for your agency and your country. - The seeded passwords changed, or the seeded accounts deleted.
DEMO_MODE=falseon the API andNEXT_PUBLIC_DEMO_MODE=falsein both frontends, as shipped.
Not for an agency with real 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.