Skip to the article
Aniq-UI

HollinaDeploy the booking business

Deploy the booking business

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, then connect your payment processor.

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 website's address a payment processor returns the guest to, the photograph addresses and the frontends' API address are all built from them.

.env beside docker-compose.yml
SITE_URL=https://www.your-company.com
ADMIN_URL=https://admin.your-company.com
API_URL=https://api.your-company.com
MEDIA_HOSTNAME=api.your-company.com

MEDIA_HOSTNAME is optional, one host or several comma separated (add your bucket's host when uploads go to one): photographs on those https hosts are resized by Next.js, and any other photograph 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, booking, question and review limits count each visitor rather than the proxy. The API container reads back-end/.env when it exists, which is also where the payment, AI, R2 and MCP keys go.
  • Without a JWT_SECRET of your own, the API generates one on its first start and keeps it in the hollina-data volume. The database and the uploads live in that volume too, so back it up.
  • The first start seeds the demo company 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, SITE_URL to the website'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 company) 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, "Back to website" and "View stay" in the dashboard
NEXT_PUBLIC_MEDIA_HOSTNAMEBothOptional. The https hosts serving the photographs (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 page titles, the first render of the home and stay 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 company) or yarn db:sync:prod (empty tables)yarn start:prod
Stays websiteyarn buildyarn start (port 3030)
Booking 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.

Take real payments

A fresh install settles card and PayPal bookings with the shipped simulator, which takes no money, until a processor is configured. Configure the ones your guests will pay with: with Stripe alone the website offers card, with PayPal alone PayPal, with both both, and pay on arrival beside them while it is on. In the API's environment:

  • Stripe: STRIPE_SECRET_KEY, and a webhook endpoint at https://<your API>/api/webhooks/payments/stripe for checkout.session.completed, payment_intent.payment_failed, charge.refunded and charge.dispute.created, whose signing secret goes in STRIPE_WEBHOOK_SECRET. Until it is set, every Stripe webhook is refused and a card booking never settles.
  • PayPal: PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_ENV=live, and a webhook at https://<your API>/api/webhooks/payments/paypal for PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.DENIED, PAYMENT.CAPTURE.REFUNDED and PAYMENT.CAPTURE.REVERSED, whose id goes in PAYPAL_WEBHOOK_ID.
  • SITE_URL at the website's public address: the processor sends the guest back to their own booking page there.
  • Pay on arrival is a switch in the dashboard's Payments tab, on by default.

Your first accounts

There is no sign-up page: the two dashboard accounts come from the seed. Start the first time with the demo company, 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. Return the demo stays to draft so the website no longer offers them. The dashboard has no delete for a stay, and the API deletes one only while none of its bookings holds nights, which a completed booking always does.
  3. Set the company's name, tagline, description, contact email, phone and address line under Settings, Company; the hero stay and the featured stays under Settings, Homepage; pay on arrival and the cancellation policy under Settings, Payments.
  4. Rename or replace the demo destinations under Settings, Destinations. A destination that still has stays cannot be deleted.

Going live

Before the booking business 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, SITE_URL at the website, PUBLIC_MEDIA_URL at the API followed by /media.
  3. Stripe, PayPal or both configured with their webhooks. Until one is, the simulator confirms every card and PayPal booking without taking any money, and the API's start-up log says Payment simulator active.
  4. The database and the uploads on persistent storage, with a backup.
  5. The host's health check at /api/health, and TRUST_PROXY=1 behind a proxy.
  6. Both frontends built with the public NEXT_PUBLIC_* values.
  7. On the website, your brand in src/config/brand.config.ts, and the privacy policy in messages/privacy rewritten for your company and your country.
  8. The seeded passwords changed, or the seeded accounts deleted.
  9. DEMO_MODE=false on the API and NEXT_PUBLIC_DEMO_MODE=false in both frontends, as shipped.

Not for a company with real data

yarn setup:fresh builds, drops every table and seeds, every time it runs, and yarn db:reset drops every table. Running yarn seed again writes the shipped stays, bookings and company settings back to their shipped values, though it keeps the rows you created. 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.