The Visual Website Builder
You Actually Own. React + Next.js, low-code, self-hosted. Your data, your infrastructure, every feature included.
© ChaiBuilder. All rights reserved.

Self-managing a Node server gives you full control and a fixed cost: a single VPS runs the
whole site - builder, CMS, and rendering - with the ISR cache on local disk. The starter
ships a production Dockerfile, and a plain pnpm build && pnpm start works anywhere
Node 20+ runs.
On the server (or in CI, shipping the build output to the server):
pnpm install
pnpm run build
pnpm run start # serves on port 3000
Set the environment variables from
Environment & License in the process
environment (systemd Environment=, a .env loaded by your process manager, or your
platform's secret store).
Keep the process alive with a process manager:
# pm2
pm2 start pnpm --name chaibuilder -- start
# or a systemd unit with ExecStart=pnpm run start, Restart=always
Single instance, local SQLite is fine. A long-running server is the one target where a local SQLite file works in production - it persists on disk. Move to Turso or Postgres before scaling to multiple instances.
The starter includes a multi-stage Dockerfile based on Next.js standalone output - it
builds the app and produces a small runtime image that starts node server.js as a
non-root user on port 3000.
One requirement: standalone output must be enabled in next.config.ts:
const nextConfig: NextConfig = {
output: 'standalone',
// …rest of your config
}
Then:
docker build -t my-site .
docker run -d -p 3000:3000 --env-file .env.production my-site
Notes on the shipped image:
node:22-alpine with libc6-compat installed (needed by sharp)..next inside the container. It is a cache, not data - losing it
on redeploy just means pages rebuild on first visit.A docker-compose.yml is included for local development with a containerized database;
for production compose setups, add your app service plus Postgres and point DATABASE_URL
at the database service name.
Put the app behind a reverse proxy that terminates TLS:
server {
listen 443 ssl;
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Host and X-Forwarded-Proto matters - the app derives the site origin from
the request, and licensing validates against your domain.client_max_body_size (nginx defaults to 1 MB) so media uploads are not rejected
by the proxy before reaching the app.A standard zero-fuss update flow:
pnpm build in a release directory).For zero-downtime, run the new container next to the old one and switch the proxy upstream once it is healthy.
output: 'standalone' set when using the DockerfileHost and X-Forwarded-Proto, TLS configured