Self-hosting OpenBase
This guide takes a fresh VPS to a running OpenBase with HTTPS, backups (including offsite copies), upgrades and recovery. Every setting is described in the README.
1. Choosing a server
Section titled “1. Choosing a server”OpenBase is one process that uses SQLite, so a small VPS is enough.
| Team size | Suggested VPS | Notes |
|---|---|---|
| 1-10 people, up to ~20k records per table | 1 vCPU, 1 GB RAM, 20 GB disk | The $4-6/month tier at most providers. |
| 10-25 people, up to ~50k records per table | 1-2 vCPU, 2 GB RAM | Leaves headroom for Caddy and large imports. |
These are suggestions, not tested configurations. What has been measured (on a fast laptop, not a VPS; see Resource usage): the server idles at about 16-20 MB and stayed at or under about 105 MB with a 50,000-record, 11-field table, and the Docker image is about 184 MB on disk. The systemd unit caps memory at 400 MB.
Disk usage is roughly the database (a 50,000-record test table with 11 fields
produced a 5.7 MB backup archive; usually tens of MB) plus your attachments plus
your backups. Each backup archive holds the whole database and every
attachment, so with OPENBASE_BACKUP_KEEP=7 you need room for about 8x
(database + attachments). The server does not check free disk space: keep an eye
on it, especially if you store many large files.
Use any 64-bit Linux with Docker. The published image supports linux/amd64
(ordinary VPS) and linux/arm64 (Ampere, Graviton, Raspberry Pi 4/5 with a 64-bit OS); Docker
picks the right one. Both were built and run through the container smoke test
(scripts/smoke-docker.sh); the memory and size measurements in the README were taken on arm64
only. Open ports 80 and 443 in your firewall; OpenBase itself is never exposed directly.
2. Docker Compose with Caddy (recommended)
Section titled “2. Docker Compose with Caddy (recommended)”Caddy terminates HTTPS and gets and renews a Let’s Encrypt certificate automatically. OpenBase listens only on the internal Docker network.
-
Point DNS for your domain (for example
base.example.com) at the server: anArecord, plusAAAAif you use IPv6. Wait untildig +short base.example.comreturns the server’s IP. -
Install Docker Engine and the Compose plugin (https://docs.docker.com/engine/install/).
-
Get the Compose files and configure them. The clone is only for
deploy/(the Compose file, the Caddyfile and.env.example); nothing is built from it:Terminal window git clone https://github.com/lukewilson2002/openbase openbasecd openbase/deploycp .env.example .envnano .env # set OPENBASE_DOMAIN, TZ and optionally SMTPThe repository’s
mainbranch is the development branch. To get the Compose files of a specific release instead,git checkout vX.Y.Z(or download the source archive of that release); the image you run is chosen byOPENBASE_VERSIONin.env, not by the checkout. -
Download the image and start:
Terminal window docker compose up -ddocker compose ps # openbase should become "healthy"docker compose logs -f # Ctrl-C to stop following -
Open
https://base.example.comand continue with first run.
What runs where:
openbaseuses the imageghcr.io/lukewilson2002/openbase:${OPENBASE_VERSION}(defaultlatest), built by the release workflow fromdeploy/Dockerfile: the web client is built with Node 24, the server withrust:1-bookworm(the client embedded), and the final image isdebian:bookworm-slimwith only the binary, CA certificates and time zone data. Every image has provenance and SBOM attestations. It runs as the unprivileged useropenbase(uid 10001), keeps its data in theopenbase-datavolume (mounted at/data), and has a built-in health check (openbase healthcheck, every 30 s).caddyuses the officialcaddy:2image configured bydeploy/Caddyfile. Certificates are stored in thecaddy-datavolume. It forwards Server-Sent Events without buffering, so live updates work.OPENBASE_TRUST_PROXY=trueandOPENBASE_PUBLIC_URL=https://<domain>are set for you indocker-compose.yml. Nothing but Caddy can reach theopenbasecontainer (its port is not published).- Everything in
deploy/.envis passed to theopenbasecontainer, so anyOPENBASE_*option from the README works there.
The Compose file, the image build (arm64 and amd64), first-run signup, data surviving
docker compose restart / a re-created container, openbase backup and the
restore procedure below were all run end to end. A real Let’s Encrypt
certificate could not be: it needs a public domain, so that step is untested here.
Trying the stack locally (no domain, no certificate)
Section titled “Trying the stack locally (no domain, no certificate)”Caddy cannot get a public certificate for localhost, so deploy/docker-compose.local.yml
overrides three things: the site address becomes http://localhost (Caddy then serves
plain HTTP and skips certificates), the published port becomes 127.0.0.1:8088, and
OPENBASE_PUBLIC_URL becomes http://localhost:8088.
cd openbase/deploycp .env.example .envdocker compose -f docker-compose.yml -f docker-compose.local.yml up -dopen http://localhost:8088 # first account becomes the administratordocker compose -f docker-compose.yml -f docker-compose.local.yml down -v # removes the volumes tooUse this only on your own machine: there is no TLS. docker compose config accepts
every combination of the override files with the shipped .env.example.
Building the image yourself
Section titled “Building the image yourself”docker-compose.build.yml replaces the published image with one built from your checkout
(tagged openbase:local, never pushed). Use it for unreleased changes, for contributing, or on an
architecture other than amd64 and arm64:
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build# and for a trial on your own machine without a domain:docker compose -f docker-compose.yml -f docker-compose.build.yml -f docker-compose.local.yml up -d --buildRun CLI commands inside the container like this:
docker compose exec openbase openbase --help3. Alternative: the binary with systemd
Section titled “3. Alternative: the binary with systemd”-
Get the binary. Either download a release (Linux, amd64 or arm64; needs glibc 2.36 or newer, so Debian 12, Ubuntu 22.04 or later):
Terminal window V=0.1.0 # the release you want, without the "v"A=amd64 # or arm64curl -fsSLO https://github.com/lukewilson2002/openbase/releases/download/v$V/openbase-$V-linux-$A.tar.gzcurl -fsSLO https://github.com/lukewilson2002/openbase/releases/download/v$V/SHA256SUMSsha256sum --check --ignore-missing SHA256SUMStar xzf openbase-$V-linux-$A.tar.gz # openbase-$V-linux-$A/openbase and openbase.service(On a public repository you can also verify where it was built:
gh attestation verify openbase-$V-linux-$A.tar.gz --repo lukewilson2002/openbase.) Or build it yourself (needs Rust and Node 24):Terminal window (cd web && npm ci --ignore-scripts && npm run build)cargo build --release --locked -p openbase -
Install the binary, create a user and install the unit (use
openbase-$V-linux-$A/openbaseandopenbase-$V-linux-$A/openbase.servicefrom the download, or the paths below from a checkout):Terminal window sudo install -m 0755 target/release/openbase /usr/local/bin/openbasesudo useradd --system --home-dir /var/lib/openbase --shell /usr/sbin/nologin openbasesudo install -m 0644 deploy/openbase.service /etc/systemd/system/openbase.servicesudo install -d -m 0750 -o root -g openbase /etc/openbasesudo tee /etc/openbase/openbase.env >/dev/null <<'EOF'OPENBASE_PUBLIC_URL=https://base.example.comOPENBASE_TRUST_PROXY=trueTZ=Europe/BerlinEOFsudo chmod 0640 /etc/openbase/openbase.envsudo chgrp openbase /etc/openbase/openbase.envsudo systemctl daemon-reloadsudo systemctl enable --now openbasesystemctl status openbaseThe unit stores data in
/var/lib/openbaseand listens on127.0.0.1:8080. It is hardened withProtectSystem=strict,NoNewPrivileges,PrivateTmp, a system-call filter andMemoryMax=400M. The unit file passessystemd-analyze verify(checked in a Debian 12 container); it has not been started under a real systemd host, so if a hardening option does not suit your distribution, loosen it and tell us. -
Put a TLS reverse proxy in front. With Caddy installed from your distribution,
/etc/caddy/Caddyfileonly needs:base.example.com {reverse_proxy 127.0.0.1:8080}
Run CLI commands as the service user so file ownership stays correct:
sudo -u openbase env OPENBASE_DATA_DIR=/var/lib/openbase openbase --help4. First run: create the administrator
Section titled “4. First run: create the administrator”The first account created through the sign-up page becomes the
administrator. Do this right after the first start, before you share the
URL. Afterwards, new accounts need an invite unless
OPENBASE_OPEN_SIGNUP=true.
You can also create an administrator from the command line (for example for automated installs). The password is read from an environment variable so it does not end up in your shell history or the process list:
read -rs OPENBASE_ADMIN_PASSWORD && export OPENBASE_ADMIN_PASSWORDdocker compose exec -e OPENBASE_ADMIN_PASSWORD openbase \ openbase create-admin --email you@example.com --name "Your Name"unset OPENBASE_ADMIN_PASSWORDThen give the admin account a second factor under Account → Security: a passkey (Touch ID, Windows Hello, a phone or a security key; it also signs in without a password) or an authenticator app (TOTP).
Pick the final domain before enabling passkeys. A passkey belongs to the host
of OPENBASE_PUBLIC_URL (base.example.com). If that host ever changes, every
passkey stops working and people have to sign in with their password and add
new ones. Passkeys made while you try OpenBase on http://localhost:8080 do not
carry over to your real domain either. Passkeys also need an address with a
domain name (not an IP address) and a browser on a secure origin (https://, or
localhost); with an IP address as the public URL the Account page says they are
off. See security.md.
5. Email (SMTP)
Section titled “5. Email (SMTP)”Email is optional. Without it, invites and password-reset links are shown to the admin to pass on by hand. There are two ways to turn it on, and you can mix them.
In the app (no restart). Sign in as an administrator, open
Admin → Email, fill in host, port, connection security (starttls for
port 587, tls for 465, none only for a relay on the same machine or
network), username, password and the sender address, and press Save
changes. It takes effect immediately. Press Send test email (to your
own address, or another one you type) to check it: if delivery fails, the
page shows the real error from the mail server or the connection (“Connection
refused”, “authentication failed”, …), so you can fix it before an invitation
goes missing. The settings are stored in the database, so they are in your
backups (the password included: protect backups as you protect the database).
The password is write-only: the page shows “Saved” with Replace and
Remove, and leaving the field empty keeps the saved one.
In the environment. Set these in deploy/.env (or
/etc/openbase/openbase.env) and restart:
OPENBASE_SMTP_HOST=smtp.example.comOPENBASE_SMTP_PORT=587OPENBASE_SMTP_SECURITY=starttls # or tls for port 465OPENBASE_SMTP_USER=openbase@example.comOPENBASE_SMTP_PASSWORD=app-passwordOPENBASE_SMTP_FROM=OpenBase <openbase@example.com>Leave the OPENBASE_SMTP_* lines out entirely (or commented out) to leave
the setting to the page. Don’t set them to empty values. A variable that is
set always wins, field by field: the page shows that field as read-only
(“set by environment”), and a change attempted through the API is refused with
the name of the variable. So you can, for example, keep the host and the
password in the environment (out of the database) and still let an
administrator change the sender address on the page. Remove the variable and
restart to manage that field on the page again. The limits on how many
invitation and automation emails go out per hour
(OPENBASE_EMAIL_PER_USER_HOUR, OPENBASE_EMAIL_PER_HOUR) are shown on the
page but can only be changed in the environment. Any transactional email
provider works (Postmark, Mailgun, Amazon SES, Fastmail, a Google
Workspace app password, …). Many VPS providers block outgoing port 25,
so use 587 or 465. Configure SPF/DKIM for the sender domain, or invites
will end up in spam.
6. Backups
Section titled “6. Backups”A backup archive (.tar.gz) holds:
openbase.db, a consistent snapshot of the database taken with SQLiteVACUUM INTOwhile the server keeps running;attachments/, every uploaded file and thumbnail;manifest.json, with the OpenBase version, the schema version and the creation time.
Backups are written to <data dir>/backups/ under a temporary name and
renamed when complete, so a crash never leaves a half-written archive that
looks valid.
Automatic daily backups
Section titled “Automatic daily backups”Every day at OPENBASE_BACKUP_HOUR (default 3, server local time, so set
TZ) the server writes openbase-auto-YYYYmmdd-HHMMSS.tar.gz (the
timestamp in the name is UTC). It keeps the newest OPENBASE_BACKUP_KEEP
(default 7) automatic backups and deletes older ones. Manual backups and
pre-migration snapshots are never deleted automatically. If the server was
down at the scheduled hour, the backup runs as soon as it is back up that
day. Set OPENBASE_BACKUP_HOUR=-1 to turn automatic backups off.
Manual backups
Section titled “Manual backups”-
In the web app: Admin → Backups → Create backup now. You can also download any backup there.
-
From the command line:
Terminal window docker compose exec openbase openbase backup# or to a specific file:docker compose exec openbase openbase backup --out /data/backups/before-cleanup.tar.gzManual backups are named
openbase-YYYYmmdd-HHMMSS.tar.gz. Delete old ones yourself when you no longer need them. Archives written with--outunder any other name work forrestorebut are not listed (or downloadable) on the admin Backups page, which only shows OpenBase’s own file names.
Offsite copies (strongly recommended)
Section titled “Offsite copies (strongly recommended)”Backups on the same disk do not protect you from losing the server, the disk or the account. Copy them somewhere else every day. With Docker, the backups directory on the host is the volume’s data path:
docker volume inspect deploy_openbase-data --format '{{ .Mountpoint }}'# usually /var/lib/docker/volumes/deploy_openbase-data/_dataUsing rclone (any S3-compatible storage, Backblaze B2,
Google Drive, another server over SFTP, …), add a root crontab entry
(sudo crontab -e) that runs an hour after the backup:
# m h dom mon dow command30 4 * * * rclone copy --max-age 48h /var/lib/docker/volumes/deploy_openbase-data/_data/backups b2:my-openbase-backups >> /var/log/openbase-offsite.log 2>&1Or with rsync over SSH to another machine:
30 4 * * * rsync -a /var/lib/docker/volumes/deploy_openbase-data/_data/backups/ backup@backup-host:/srv/openbase-backups/ >> /var/log/openbase-offsite.log 2>&1For systemd installs the directory is /var/lib/openbase/backups.
The archives are not encrypted, and they contain every record, every
attachment and the user table (password hashes). Use encrypted storage
(an rclone crypt remote, or a provider with server-side encryption) and
restrict who can read it. Run a test restore now and then (see below).
7. Restoring a backup
Section titled “7. Restoring a backup”A restore replaces the database with the one in the archive and adds the
archive’s attachments. Files already on disk are kept, since attachment
files are named by their content hash. The current database is never
deleted: it is moved aside to openbase.db.pre-restore-<timestamp> (with its
-wal file, if it had one).
The server must be stopped. openbase restore refuses to run while
openbase.db-wal is non-empty, which is how it detects a running (or
crashed) server.
With Docker Compose:
cd openbase/deploydocker compose stop openbase
# Restore an archive that is already in the backups directory ...docker compose run --rm --no-deps openbase restore /data/backups/openbase-auto-20261012-010000.tar.gz --force
# ... or one from the host (for example downloaded from offsite storage):docker compose run --rm --no-deps -v "$PWD/openbase-20261012-010000.tar.gz:/restore.tar.gz:ro" \ openbase restore /restore.tar.gz --force
docker compose start openbasedocker compose logs -f openbaseWith systemd:
sudo systemctl stop openbasesudo -u openbase env OPENBASE_DATA_DIR=/var/lib/openbase \ openbase restore /path/to/openbase-20261012-010000.tar.gz --forcesudo systemctl start openbaseWhat happens, step by step:
- Refuse if the WAL file is non-empty (server still running). If the
server crashed instead, start it once and stop it cleanly so SQLite
checkpoints the WAL, then retry.
--forceskips this check: use it only if you are sure no OpenBase process is running. - Refuse to replace an existing database unless
--forceis given. - Extract the archive into a temporary directory inside the data
directory. Archives containing absolute paths,
..components, links or device files are rejected outright. - Check that
manifest.jsonexists, that its schema version is not newer than this OpenBase supports (upgrade OpenBase first if it is), and thatopenbase.dbpasses SQLite’squick_check. - Move the current
openbase.db(and its WAL) aside toopenbase.db.pre-restore-<timestamp>, move the restored database in, and merge the attachments.
If the archive came from an older OpenBase, the database is migrated on the
next start (with a pre-migration snapshot, see below). Once everything
looks right, you can delete the openbase.db.pre-restore-* files.
To restore onto a new server, install OpenBase there (sections 2 or 3)
but do not create an account. Then run the restore (without --force if
the data directory is still empty) and start the server.
8. Upgrading
Section titled “8. Upgrading”Releases are published on the releases page
and listed in CHANGELOG.md. A push to main never changes what you run: the
image is built and the tags move only when a release is published.
-
Read the release notes.
-
Take a manual backup and make sure a recent offsite copy exists:
Terminal window docker compose exec openbase openbase backup -
Update and restart:
Terminal window cd openbase/deploydocker compose pulldocker compose up -ddocker compose ps # healthy again?docker compose exec openbase openbase --versiondocker compose pullfetches what yourOPENBASE_VERSIONnow points to (see below);up -dre-creates only the containers whose image changed. Built from source instead?git pull, thendocker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build. With systemd: replace/usr/local/bin/openbasewith the new release binary and runsudo systemctl restart openbase.
Which version do I follow?
Section titled “Which version do I follow?”OPENBASE_VERSION in deploy/.env (default latest) selects the image tag:
| Value | You get | Choose it when |
|---|---|---|
1.4.2 |
exactly that release, forever | you want to decide every update, or you are rolling back |
1.4 |
patch releases of 1.4 (1.4.3, …) |
you want bug and security fixes only |
1 |
every 1.x.y release (no breaking changes by the project’s versioning rules) |
you want new features without surprises (once 1.0 exists) |
latest |
every stable release, including a new major version | you read the notes before each pull |
Pre-releases (1.5.0-rc.1) have only their exact tag; no moving tag ever points at one. Until
1.0, a minor version (0.2.0) may break things: there is no 0 tag, so follow latest or pin a
minor such as 0.1.
Automatic updates (optional)
Section titled “Automatic updates (optional)”Off by default. If you want them, add docker-compose.autoupdate.yml, which runs
Watchtower once a day (04:30, after the 03:00 backup):
docker compose -f docker-compose.yml -f docker-compose.autoupdate.yml up -dIt updates only containers labelled com.centurylinklabs.watchtower.enable=true, which is the
openbase container and nothing else, and only to what your OPENBASE_VERSION tag points at.
So the safe setup is a major or minor tag (1, or 0.1 while OpenBase is 0.x), not latest.
Understand the trade-offs: updates then happen while nobody reads the release notes (a schema
change still gets its pre-migration snapshot, and OpenBase refuses to start if it cannot write
it); and Watchtower needs the Docker socket, which is root-equivalent on that machine. The
alternative without it is a cron job: 30 4 * * * cd /path/to/openbase/deploy && docker compose pull openbase && docker compose up -d.
Schema changes and snapshots
Section titled “Schema changes and snapshots”When the new version needs to change the database schema, OpenBase first
writes a pre-migration snapshot,
backups/pre-migration-v<old version>-<timestamp>.db, and then migrates.
If the snapshot cannot be written (for example, the disk is full), it does
not migrate and refuses to start. These snapshots are plain SQLite
databases and are never deleted automatically. Remove old ones once you
are happy with the upgrade.
openbase migrate (also run on every start) is a no-op on a database that is
already current: it takes no snapshot and writes nothing. That was checked with
the release binary; a snapshot taken because migrations were pending is covered
by unit tests only, since schema version 1 is the only version so far.
Rolling back
Section titled “Rolling back”A newer version never goes backwards on its own: OpenBase refuses to open a database written by a newer version (“schema version N is newer than this binary supports”). If an update that did not change the schema misbehaves, pin the previous version and you are done. If it did change the schema, you need the data from before the update too:
-
Stop following the new version: set
OPENBASE_VERSIONindeploy/.envto the previous release (for exampleOPENBASE_VERSION=0.1.0; the releases page lists them). -
Stop the server and restore the data from before the update: your pre-update backup archive (the normal way, section 7):
Terminal window docker compose stop openbasedocker compose run --rm --no-deps openbase restore /data/backups/openbase-20261012-030000.tar.gz --forcedocker compose runstarts the image your.envnow names, so do step 1 first. The pre-migration snapshot the new version wrote is the alternative if you have no archive: it is a plain SQLite file, so with the server stopped deleteopenbase.db-walandopenbase.db-shmand copy the snapshot overopenbase.db, then start the previous version. -
docker compose pull && docker compose up -d, and checkopenbase --version.
Anything written after the backup was taken is lost by a restore; that is the price of a schema change, which is why the backup comes before the update. systemd: reinstall the previous release binary instead of step 1, then restore the same way.
9. Account recovery
Section titled “9. Account recovery”Forgotten password. Users can reset their own password with “Forgot password” when SMTP is configured. Otherwise an administrator can create a reset link under Admin → Users → Create password reset link and pass it on (this works only in the web app, signed in: an API token or AI agent can never create a reset link). From the command line (this also signs the user out everywhere):
read -rs OPENBASE_NEW_PASSWORD && export OPENBASE_NEW_PASSWORDdocker compose exec -e OPENBASE_NEW_PASSWORD openbase \ openbase reset-password --email user@example.comunset OPENBASE_NEW_PASSWORDLost two-factor device. Turn off 2FA for the user from the command line. This also signs them out everywhere. They can log in with their password and set up 2FA again:
docker compose exec openbase openbase disable-2fa --email user@example.comConnect Claude or ChatGPT. They sign in through OAuth, which OpenBase serves
itself, but they connect from their providers’ servers: OPENBASE_PUBLIC_URL must be your
public https:// address (with the bundled Caddy it already is) and the server must be
reachable from the internet. Over plain http:// on a public address OpenBase does not
offer OAuth at all. Check with
curl https://<domain>/.well-known/oauth-protected-resource/mcp. People approve each app
on an approval page after signing in; you can see and revoke everyone’s connections under
Admin → Connected apps. Apps register themselves unless you set
OPENBASE_OAUTH_REGISTRATION=false (apps that publish a metadata document can still ask a
signed-in person for approval); openbase oauth-client add --name N --redirect-uri URI
adds one by hand. Details: mcp.md.
Lost passkey device. The person signs in with their password (a passkey never
replaces it) and removes the passkey under Account → Security. If they cannot,
an administrator removes all of their passkeys under Admin → Users → Remove
passkeys; this does not sign anyone out. A password reset or reset-password
keeps passkeys.
The only administrator is locked out. Use reset-password and/or
disable-2fa above, or create another administrator with create-admin.
10. Troubleshooting
Section titled “10. Troubleshooting”| Symptom | What to check |
|---|---|
Browser shows a certificate error or Caddy logs challenge failed |
DNS must point at this server, and ports 80 and 443 must be reachable from the internet. Check with docker compose logs caddy. |
openbase container keeps restarting |
docker compose logs openbase. The most common cause is a database from a newer OpenBase (“schema version N is newer than this binary supports”): upgrade (set OPENBASE_VERSION back to the newer version), or restore an older backup. |
unhealthy in docker compose ps |
docker compose exec openbase openbase healthcheck prints the reason. |
| Login works but you are logged out immediately | OPENBASE_PUBLIC_URL must match the URL in the browser exactly, including https://. |
| “Add a passkey” is greyed out, or a passkey is refused | Passkeys work only at the exact address in OPENBASE_PUBLIC_URL, whose host must be a domain name (not an IP address), over https:// or on localhost. After changing the host, old passkeys no longer work: sign in with the password and add new ones. |
| Requests rejected with “missing CSRF header” | A custom script is using a session cookie. Use a personal access token (Authorization: Bearer ...) instead. |
| All users appear to come from one IP (rate limits hit) | Set OPENBASE_TRUST_PROXY=true when running behind a reverse proxy. Never enable it when OpenBase is reachable directly, and only behind a proxy that sets or appends the client address in X-Forwarded-For (OpenBase uses the last entry); see docs/security.md. |
| Uploads fail with 413 | Raise OPENBASE_MAX_UPLOAD_MB. If you use a proxy other than Caddy, raise its body size limit too (nginx: client_max_body_size). |
| Live updates do not appear | A proxy is buffering Server-Sent Events. For nginx set proxy_buffering off; for /api/bases/. |
restore says the server looks like it is still running |
Stop the server. If it crashed, start and stop it cleanly once, then retry. |
| Disk full | Delete old manual backups and pre-migration-*.db / openbase.db.pre-restore-* files from the data directory, or lower OPENBASE_BACKUP_KEEP. |
| Claude or ChatGPT cannot connect or sign in | They connect from the internet: OPENBASE_PUBLIC_URL must be the public https:// address, with a valid certificate, and /.well-known/oauth-protected-resource/mcp must answer JSON from outside. Put a proxy rule in front of /mcp, /oauth/ and /.well-known/ if you restrict paths. |
| Emails are not delivered | Check the logs for SMTP errors, make sure outgoing port 587/465 is not blocked, and check SPF/DKIM. |
Logs go to stdout (docker compose logs, or journalctl -u openbase for
systemd). Set OPENBASE_LOG=debug temporarily for more detail, and
OPENBASE_LOG_FORMAT=json if you ship logs to a collector.