Host Benmore on your own server
Run your app with the open-source framework, your own domain, and a server you control. You do not need a Benmore platform account or hosting subscription.
This walkthrough uses a Linux server with systemd, a persistent local disk, and Caddy as the HTTPS reverse proxy. Commands use Bash and a sudo-capable operator account. Replace app.example.com with your domain. The result is a production service: internet → Caddy on ports 80/443 → Benmore on 127.0.0.1:8080 → the app's SQLite database.
What you run and operate
The framework includes the app runtime, embedded TSX compiler, SQLite, authentication, generated CRUD APIs, access rules, flows, hooks, jobs, and real-time features. You provide the machine, DNS, TLS, monitoring, backups, and external service credentials. Email, OAuth, object storage, and TURN/SFU services need their own configuration when used.
The public framework does not include the commercial fleet engine, hosted dashboard, account billing, MCP hosting service, or cloud deployment commands. benmore deploy, push, promote, skill, and hosted api commands belong to the separate cloud CLI. Both binaries are called benmore; check version --json and use explicit paths if you install both.
Use one process and one local writable app directory for this setup. Do not put SQLite on a shared network filesystem or point multiple machines at this directory. Separate apps need separate directories, users, ports, and secrets. A container deployment needs persistent storage for the entire app state and a coordinated backup strategy too.
1. Install the framework
On Debian or Ubuntu, install the tools used below. Other systemd distributions can use their equivalent packages. Prebuilt releases do not require Go or Node.
sudo apt-get update
sudo apt-get install -y ca-certificates curl tar sqlite3 openssl
The following Bash block selects the server architecture, downloads the pinned framework release, and requires its published SHA-256 checksum to match before installing. Review newer releases before changing the version.
set -euo pipefail
release_version=2.7.226
case "$(uname -m)" in
x86_64) release_arch=amd64 ;;
aarch64|arm64) release_arch=arm64 ;;
*) echo "Unsupported architecture" >&2; exit 1 ;;
esac
release_dir=$(mktemp -d)
cd "$release_dir"
release_asset="benmore_${release_version}_linux_${release_arch}.tar.gz"
release_base="https://github.com/Benmore-Studio/benmore/releases/download/v${release_version}"
curl -fL --retry 3 -o "$release_asset" "$release_base/$release_asset"
curl -fL --retry 3 -o checksums.txt "$release_base/checksums.txt"
awk -v asset="$release_asset" '$2 == asset {print; found++} END {if (found != 1) exit 1}' checksums.txt > selected.sha256
sha256sum -c selected.sha256
tar -xzf "$release_asset" benmore
sudo install -D -m 0755 benmore "/opt/benmore-framework/$release_version/benmore"
sudo ln -s "/opt/benmore-framework/$release_version" /opt/benmore-framework/current
/opt/benmore-framework/current/benmore version --json
Expect version 2.7.226 and edition framework. The initial symlink command intentionally fails if current already exists; use the upgrade procedure for an existing installation. Source and all platform downloads are on the framework release page. To build from source, follow the release's Go/toolchain requirements and include -tags sqlite_fts5 for full-text search.
2. Create the app and its secrets
Create a dedicated service account and scaffold a fresh app. For an existing app, stage its source in this directory instead, then follow the migration notes below.
sudo useradd --system --user-group --home-dir /srv/benmore --shell /usr/sbin/nologin benmore-app
sudo install -d -m 0750 -o benmore-app -g benmore-app /srv/benmore
sudo -u benmore-app /opt/benmore-framework/current/benmore new /srv/benmore/myapp
sudo install -d -m 0700 /etc/benmore
App source is Prisma, HTML/TSX, and YAML. Edit /srv/benmore/myapp/app.yaml and merge these settings into its existing maps, keeping the generated authentication and access rules. Do not duplicate top-level YAML keys.
seo:
url: "https://app.example.com"
backup:
interval: "24h"
keep: 7
Create two different random secrets once. BENMORE_SERVER_SECRET must be in the process environment before the binary starts; putting it only in the app's env.yaml is insufficient. ENCRYPTION_KEY is the app's persistent encryption key. Never replace it just to fix a startup problem.
sudo bash <<'BASH'
set -euo pipefail
umask 077
test ! -e /etc/benmore/myapp.env
test ! -e /srv/benmore/myapp/env.yaml
printf 'BENMORE_SERVER_SECRET=%s\n' "$(openssl rand -hex 32)" > /etc/benmore/myapp.env
printf 'ENCRYPTION_KEY: "%s"\n' "$(openssl rand -hex 32)" > /srv/benmore/myapp/env.yaml
chown benmore-app:benmore-app /srv/benmore/myapp/env.yaml
BASH
This block refuses to overwrite existing secrets. For an imported app, preserve the encryption key paired with its database. Back up both secret files privately; keep them out of Git, browser code, and logs. With this standalone layout, app values load from env.yaml, and process environment values can override them. A present .benmore/env changes that precedence: inspect it before migrating a hosted app instead of copying it blindly.
Configure integrations in env.yaml as quoted string values. SMTP uses SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, and EMAIL_FROM (or SMTP_FROM). Test delivery before requiring email OTP or verification. Google sign-in uses your own GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, with https://app.example.com/auth/google/callback registered at the provider. Other providers need their own configuration.
3. Run it with systemd
Save this as /etc/systemd/system/benmore-myapp.service. The framework writes migrations, generated types, uploads, and backup state inside the app directory, so that directory must stay writable.
[Unit]
Description=Benmore myapp
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=benmore-app
Group=benmore-app
WorkingDirectory=/srv/benmore/myapp
EnvironmentFile=/etc/benmore/myapp.env
Environment=BENMORE_BIND_LOOPBACK=1
Environment=BENMORE_TRUST_PROXY=1
ExecStart=/opt/benmore-framework/current/benmore serve /srv/benmore/myapp --port 8080
Restart=on-failure
RestartSec=5
TimeoutStopSec=120
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/srv/benmore/myapp
[Install]
WantedBy=multi-user.target
sudo systemd-analyze verify /etc/systemd/system/benmore-myapp.service
sudo systemctl daemon-reload
sudo systemctl enable --now benmore-myapp
sudo systemctl status benmore-myapp --no-pager
sudo journalctl -u benmore-myapp -n 80 --no-pager
curl -fsS http://127.0.0.1:8080/health
serve loads the app and applies pending migrations. A started process is not proof initialization finished; wait for a healthy response and inspect startup errors. BENMORE_BIND_LOOPBACK=1 keeps port 8080 off the public interfaces. In 2.7.226 it changes only the listener; later framework releases also treat it as a hosted-platform marker, so read the upgrade notes below before upgrading. Proxy trust is appropriate only with the restricted listener and sanitized ingress below. See the systemd service reference for service lifecycle settings.
4. Put HTTPS in front
Point the domain's A record at the server; publish an AAAA record only if IPv6 reaches it too. Allow inbound TCP 80 and 443 in the host firewall and provider firewall. Keep the app port private and retain your SSH access.
Install Caddy using its official package instructions, then add this site block to /etc/caddy/Caddyfile. Preserve any other sites already configured.
app.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up -CF-Connecting-IP
header_up -X-Real-IP
header_up -X-Benmore-App
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -fsS https://app.example.com/health
Caddy obtains and renews certificates when DNS and network access meet its automatic HTTPS requirements. Its reverse proxy supports WebSocket upgrades. This example assumes Caddy is the internet-facing proxy; if you add a CDN or another proxy, explicitly configure that trusted hop and verify client IP attribution. Do not forward arbitrary client-supplied identity headers.
5. Verify the deployment
- Check
/healthover the public HTTPS domain for the expected version and healthy status. Confirmss -ltnshows the app listening on127.0.0.1:8080. - Open the app, sign up with a test account, sign out, and sign back in. Test email delivery and OAuth if enabled.
- Create and read a record through the app. With a second test account, verify private records and uploads remain inaccessible.
- Restart the service and verify records persist. Exercise uploads, jobs, and real-time behavior the app actually uses.
- Monitor disk space, process restarts, logs, public HTTPS health, certificate renewal, and backup age. A 200 response alone does not prove a user journey works.
Read bundled app guidance with /opt/benmore-framework/current/benmore docs build. The scaffold's AGENTS.md is shared by Codex and other agents; CLAUDE.md imports it. Hosted skills and cloud commands do not deploy to this server. Authenticated API discovery is available at /api/_openapi on the running app.
6. Back up and restore
Scheduled SQLite snapshots live in .benmore/backups/. They protect database contents, not the entire deployment, and a backup on the same disk cannot survive loss of that disk. Keep encrypted off-machine copies of source, data.db, uploads, env.yaml, required .benmore state, the service environment file, and the exact runtime version. Back up external object storage separately.
For a simple consistent full backup, schedule a maintenance window, pause deployments and key rotation, then stop the only writer. Run the following on the server. It verifies the stopped database before archiving the full app and its process secrets; check each result before continuing.
sudo bash <<'BASH'
set -euo pipefail
umask 077
systemctl stop benmore-myapp
test "$(systemctl show benmore-myapp -p ActiveState --value)" = inactive
test "$(systemctl show benmore-myapp -p MainPID --value)" = 0
test "$(sqlite3 -readonly /srv/benmore/myapp/data.db 'PRAGMA integrity_check;')" = ok
backup_dir="/var/backups/benmore/$(date -u +%Y%m%dT%H%M%SZ)"
install -d -m 0700 "$backup_dir"
/opt/benmore-framework/current/benmore version --json > "$backup_dir/runtime.json"
tar -C / -czf "$backup_dir/app-and-secrets.tar.gz" srv/benmore/myapp etc/benmore/myapp.env
cd "$backup_dir"
sha256sum app-and-secrets.tar.gz > SHA256SUMS
sha256sum -c SHA256SUMS
systemctl start benmore-myapp
printf 'Backup: %s\n' "$backup_dir"
BASH
If any step fails, the script stops; inspect the error and service state before resuming traffic. Verify public health after restart. Encrypt and transfer the archive and checksum to your backup destination, with retention and access controls. The archive contains credentials and customer data.
For database-only snapshots without stopping the service, use SQLite's online backup API through sqlite3 .backup, or the framework's scheduled snapshots. Never copy only a live data.db: committed transactions can still be in its WAL. An online database snapshot does not atomically capture uploads or encryption-key rotation.
Restore procedure
- Choose a restore point and account for writes after it: restoring loses those newer changes. Rehearse on an isolated machine first, with outbound integrations and scheduled work disabled or replaced with test credentials.
- Verify the archive checksum. Extract into a private staging directory and run
PRAGMA integrity_check;against the staged database; requireok. Confirm the saved encryption key and source match that database. - During maintenance, stop the service and verify it is inactive with no remaining writer. Move the entire current app directory and process environment file to a private recovery location. Preserve its database, WAL, and SHM together.
- Install the staged directory and secret file at their original paths. Restore app ownership to
benmore-app:benmore-app, restrict secret files to mode0600, and keep the process environment file owned by root. Do not overlay a backup database onto the old WAL/SHM. - Use the saved compatible runtime version, start the service, and verify health, sign-in, representative records, decrypted fields, and uploads. Startup can run migrations; retain the untouched staged backup until verification succeeds.
The hosted benmore restore command does not operate this standalone service. Never run benmore test against live or restored production data: it resets the target database. Use a disposable source fixture for app tests.
7. Ship updates and upgrade
Keep source in your own version control. Validate changes in an isolated staging service with a separate directory, port, database, and test credentials. Copy only reviewed source files during deployment; preserve data.db*, env.yaml, uploads/, and .benmore/. Avoid a blanket directory sync with deletion.
- Take and verify a complete backup. Record the current source revision, runtime version, and secret backup location.
- Stage the next source and any new framework binary. Verify release checksums. Keep the prior versioned binary.
- Stop the service, install reviewed source, and switch
/opt/benmore-framework/currentto the chosen version when upgrading the runtime. Keep directory ownership and secret permissions intact. - Restart, watch initialization logs, then verify public health and the changed user journey. Large migrations can take time; investigate progress before repeatedly restarting.
Malformed core configuration is rejected, but startup validation warnings are not a complete test suite. A source revert or binary downgrade cannot undo database migrations or external effects. If old code is incompatible with migrated data, use a tested forward fix or the coordinated restore procedure, accepting the restore point's data loss.
Before upgrading past 2.7.226
The hosted runtime is ahead of the public framework release. When a newer framework release is published, read its release notes; these changes in the current source affect this setup:
- From 2.7.237,
BENMORE_BIND_LOOPBACK=1also marks a hosted-platform app process: SMTP is never used andS3_ENDPOINTmust resolve to a public address. From 2.7.242, replace that line withBENMORE_LISTEN=127.0.0.1, which keeps port 8080 on loopback without the hosted-platform behavior, and setBENMORE_TRUST_PROXY=127.0.0.1so only the local Caddy is trusted for forwarded client addresses. Upgrade straight to 2.7.242 or later rather than to 2.7.237-2.7.241. - From 2.7.237, password reset, verification and email-change links are sent only when
seo.url(orBENMORE_PUBLIC_URL) names the public origin; keep theseo.urlsetting above. - From 2.7.237, session IDs are stored as hashes, so rolling back past that release signs everyone out. From 2.7.239, the HTTPS session cookie is
__Host-benmore_session, so the upgrade signs everyone in again once.
Move an existing hosted app
A source pull alone does not move a running app. Inventory its schema, environment values, encryption key, uploads or object storage, OAuth callbacks, email sender, webhooks, cron jobs, domains, and platform-managed integrations. Replace managed services with your own credentials and endpoints.
Arrange a verified database snapshot paired with its encryption key and required files; never seed ciphertext under a newly generated key. Rehearse the import and permission checks on a separate server. At cutover, pause writes, take a final consistent snapshot, install it, change DNS and callback/webhook destinations, and verify before resuming writes. Keep the old instance from processing jobs or writes concurrently. Platform account credentials and hosting settings are separate from the app's users and data.
Troubleshooting
| Symptom | What to check |
|---|---|
| Unknown hosted command | Check the edition. Framework commands manage the local service; cloud commands target the hosted platform. |
| 502 from Caddy | Read the service logs, verify initialization and the configured port, and request loopback /health. |
| Certificate cannot be issued | Check A/AAAA records, ports 80/443, conflicting listeners, and Caddy logs. |
| Secrets or encrypted data stop working | Restore the matching encryption key and inspect environment precedence. Do not generate a replacement key over existing ciphertext. |
| CSRF or signed URLs break after restart | Verify a stable 32+ character BENMORE_SERVER_SECRET is in the process environment before startup. |
| OTP/reset email never arrives | Test SMTP credentials, sender authorization, and delivery logs before enabling email-dependent authentication. |
| Database locked or disk full | Check free disk/inodes, backup retention, and unexpected writers. Preserve the database bundle before recovery. |