Skip to the article
Aniq-UI

VelraDeploy the agency

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

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 dashboard's address, the photo addresses and the frontends' API address are all built from them.

.env beside docker-compose.yml
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.com

MEDIA_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:

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 viewing request limits count each visitor rather than the proxy. The API container reads back-end/.env when it exists, which is also where the optional keys go.
  • Without a JWT_SECRET of your own, the API generates one on its first start and keeps it in the velra-data volume. 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=false is 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_ORIGIN to the website's and the dashboard's addresses (comma separated), FRONTEND_URL to the dashboard's, and PUBLIC_MEDIA_URL to the API's public address followed by /media.
  • Set SEED_DEMO_DATA=true for the first start on a new volume: the image's default is false, 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=mysql and the five DB_* variables), the container does not create the tables. Run node dist/database/seeder.js (demo agency) 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.

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 in the dashboard
NEXT_PUBLIC_MEDIA_HOSTNAMEBothOptional. The https hosts serving the photos (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 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.

AppBuildStart
APIyarn build, then once yarn seed:prod (demo agency) or yarn db:sync:prod (empty tables)yarn start:prod
Public websiteyarn buildyarn start (port 3030)
Agency 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.

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:

  1. Change both passwords under Settings, Security, or create your own Super Admin in Admin management and delete the seeded accounts.
  2. Archive or delete the sample properties, and delete the sample neighborhoods you do not need.
  3. Close the sample inquiries and cancel the sample viewings: they have no delete.
  4. 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:

  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, PUBLIC_MEDIA_URL at the API followed by /media.
  3. The database and the uploads on persistent storage, with a backup.
  4. The host's health check at /api/health, and TRUST_PROXY=1 behind a proxy.
  5. Both frontends built with the public NEXT_PUBLIC_* values.
  6. On the website, your brand in src/config/brand.config.ts, and the privacy policy in messages/privacy rewritten for your agency and your country.
  7. The seeded passwords changed, or the seeded accounts deleted.
  8. DEMO_MODE=false on the API and NEXT_PUBLIC_DEMO_MODE=false in 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.

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.