mybasev1.0.21

Hosting

Run your sites (Next.js, static, any Docker image) as containers on your mybase server, at <site>.<your domain> or your own custom domains.

Sites run as containers on the same machine as mybase. You build the image on your own computer, mybase deploy pushes it to the project's container registry, and mybase starts it, waits until it answers, then switches traffic to it. If the new container crashes or never answers, the old one keeps serving.

mybase server hosting                     # once, on the server
mybase hosting enable shop                # once per project
mybase hosting init --template nextjs --project shop --site web
mybase deploy                             # → https://web.example.com

Set up the server

Run mybase server hosting on the machine that runs mybase. It turns on three containers (Compose profile hosting):

  • registry: stores your images (CNCF distribution, registry:3; see Container registry). Internal only: pushes go through the mybase API, and Docker pulls from 127.0.0.1:5000.
  • caddy: routes <site>.<domain> to each site's live container. Its config comes from mybase.
  • hosting-worker: starts and stops site containers. It's the only part with the Docker socket; the API never gets it.

It asks for the base domain (MYBASE_HOSTING_DOMAIN, e.g. example.com). With a Cloudflare Tunnel (mybase server tunnel), it adds one wildcard route, *.example.com → caddy, plus a proxied wildcard DNS record, using a Cloudflare API token it doesn't save. It also makes Caddy the tunnel's catch-all, so custom domains need only a DNS record. Explicit hostnames like mybase.example.com keep working: they're matched before the wildcard.

Use the zone itself as the base domain. Cloudflare's free certificate covers one level (*.example.com); *.apps.example.com needs their Advanced Certificate Manager.

Without a tunnel, Caddy answers plain HTTP on port 80 (MYBASE_HOSTING_PORT). Sites are reachable at http://<site>.localhost on the server, or at <site>.<domain> if you point that name at the machine yourself.

SettingDefaultWhat it does
MYBASE_HOSTING_DOMAINnoneSites live at <site>.<domain>
MYBASE_HOSTING_MEMORY512mMemory cap per site container
MYBASE_HOSTING_PORT80Host port Caddy listens on
MYBASE_TUNNEL_IDfrom the tunnel tokenCustom domains' CNAMEs point at <id>.cfargotunnel.com (written by mybase server setup/hosting)
MYBASE_DNS_TOKENnoneCloudflare token (DNS edit + zone read) mybase uses to add custom domains' records. --dns-token saves it

mybase hosting enable also turns on the container registry for the project: site images live in <project>/<site>. The registry can't be turned off while hosting is on.

Sites

A project can have any number of sites. Each has a name (one DNS label, unique across the whole server, since they share the domain), a port the container listens on (default 3000, also passed as $PORT), and a health path (default /).

mybase hosting create shop web --port 3000 --health /api/health
mybase hosting sites shop
mybase hosting set shop web --port 8080
mybase hosting delete shop web

Names like www, mybase, s3, db, and registry are reserved, as are the first labels of mybase's own public hostnames.

Custom domains

Besides <site>.<domain>, a site can answer at any hostnames you give it (shop.com, www.shop.com, blog.other.dev), each used by one site only. The domain's zone must be in the same Cloudflare account as the tunnel: Cloudflare routes a proxied CNAME to <tunnel id>.cfargotunnel.com only within that account, and the tunnel hands every name without its own rule to Caddy.

Once, on the server (also after updating from a version without custom domains):

mybase server hosting --dns-token <token>   # or answer the prompt

The token is a Cloudflare API token with Zone › DNS › Edit and Zone › Zone › Read on the zones you'll use (MYBASE_DNS_TOKEN, shared with public Postgres). Without it, mybase still routes the domain and tells you which record to add.

mybase hosting domain add shop web shop.com --primary
mybase hosting domain add shop web www.shop.com
mybase hosting domains shop web
mybase hosting domain remove shop web www.shop.com

Adding a domain creates a proxied CNAME shop.com → <tunnel id>.cfargotunnel.com (comment mybase hosting web) and adds the name to the site's Caddy route. Apex names work (Cloudflare flattens the CNAME). If the name already has an A, AAAA, or CNAME record, the add fails with what's there; --replace swaps it (MX and TXT records are left alone). Removing a domain deletes only the record mybase made, and only if it still points at the tunnel. Deleting the site removes all of them.

StatusMeans
activeThe CNAME points at the tunnel; HTTPS works once Cloudflare has a certificate (seconds to minutes)
manualmybase can't edit DNS (no MYBASE_DNS_TOKEN, or no tunnel); the row says what to add
errorThe zone isn't on the token, or Cloudflare refused; fix it, then mybase hosting domain verify

Primary domain: --primary (or mybase hosting domain primary) makes it the site's URL in the CLI and dashboard, and MYBASE_SITE_URL in the container after the next redeploy (Better Auth's baseURL, links in emails). Without one, <site>.<domain> is the URL.

Cloudflare's free certificate covers a zone and one level below it (shop.com, www.shop.com); a.b.shop.com needs Advanced Certificate Manager, and mybase says so when you add one. A single label under the base domain (x.example.com) is a site's own address, so it can't be a custom domain.

In mybase.config.ts, domains: ["shop.com", "www.shop.com"] on a site makes mybase deploy add any that are missing. It never removes domains; it lists the ones only the server has.

Without a tunnel, Caddy routes the name too; point its DNS at the machine yourself (plain HTTP on MYBASE_HOSTING_PORT).

Deploying

mybase deploy reads mybase.config.ts (or --project and --site):

import { defineConfig } from "@mybase-dev/sdk/config";

export default defineConfig({
  project: "shop",
  hosting: {
    sites: { web: { port: 3000, healthPath: "/", context: ".", dockerfile: "Dockerfile", domains: ["shop.com"] } },
  },
});

It then:

  1. builds the Dockerfile with docker build --platform linux/<server arch> (an arm64 Pi needs buildx/QEMU on an amd64 laptop),
  2. saves the image and pushes its layers in 32 MB chunks (Cloudflare caps requests at 100 MB; plain docker push sends a layer in one request), skipping layers the registry already has,
  3. starts a deploy and waits: the new container must answer its health path (any status below 500) within 90 seconds,
  4. switches Caddy to it, and stops the old container 15 seconds later.

A deploy that fails keeps its error and the container's last 30 lines of output (mybase hosting deploys shop web). The previous deploy stays live. The site is created on first deploy if it doesn't exist.

The CLI needs an API key with hosting:write for the project (it covers pushing to the registry too). mybase deploy --no-wait returns once the deploy is queued.

Pushing with docker

You can also push with docker and deploy the digest:

docker login mybase.example.com -u mybase      # password: an mb_… API key with registry:write
docker push mybase.example.com/shop/web:v1     # repository = <project>/<site>
curl -X POST https://mybase.example.com/projects/shop/hosting/sites/web/deploys \
  -H "Authorization: Bearer $MYBASE_API_KEY" -H "Content-Type: application/json" \
  -d '{"digest":"sha256:…"}'

Over plain HTTP (a LAN address), add it to Docker's insecure-registries first. Large layers fail through the tunnel; use mybase deploy there. Other repositories in the project (shop/tools, …) are ordinary registry images that hosting leaves alone.

Templates

mybase hosting init --template <t> writes a Dockerfile and .dockerignore (and mybase.config.ts with --project and --site). Existing files are left alone unless --force.

TemplateForNotes
nextjsNext.jsNeeds output: "standalone" in next.config. Multi-stage, runs node server.js as a non-root user
staticVite, Astro, plain HTMLnpm run build into dist/ (OUT_DIR build arg), served by Caddy with SPA fallback
nodeBun or Node serversbun run start; your server must listen on $PORT

Every template installs with the lockfile it finds (npm, pnpm, yarn, or bun).

Env vars and secrets

Each site has its own env vars, and optionally a secrets environment whose values are added at deploy time. Precedence: secrets env, then the site's vars, then mybase's own (PORT, HOSTNAME, MYBASE_SITE, MYBASE_SITE_URL, MYBASE_DEPLOY_ID).

mybase hosting env set shop web NEXT_PUBLIC_API=https://api.example.com
mybase hosting set shop web --secrets-env prod
mybase hosting redeploy shop web          # env changes apply on the next deploy

For the project's own database, cache, storage, and API, link the keys instead of writing connection strings: a linked key resolves to the project network's names inside the site, and to a reachable address for mybase run on your laptop (see Secrets).

mybase secrets link -p shop -e prod --database --cache --storage --api
mybase hosting redeploy shop web

Site vars are encrypted with MYBASE_SECRETS_KEY when it's set. Keep real secrets in a secrets environment anyway: values there have revisions and env-scoped keys. Next.js inlines NEXT_PUBLIC_* at build time, so those must be set while building, not here.

Rollbacks, logs, retention

mybase hosting rollback shop web            # the previous live image
mybase hosting rollback shop web 1a2b3c4d   # a given deploy
mybase hosting logs shop web                # follows; --no-follow, --tail 500, --deploy <id>

In the dashboard, a site's Logs tab streams the same output live: it follows the live deploy across redeploys, or shows any earlier deploy whose container is still around (the Logs button on each deploy row). Filter by text or by errors/warnings, toggle timestamps and wrapping, and download what's loaded.

Before a new container takes traffic, every env value that's a URL with a host (DATABASE_URL, REDIS_URL, S3_ENDPOINT, …) is tried from the project network. One that doesn't answer becomes a warning on the deploy, shown by mybase deploy, mybase hosting deploys, the dashboard, and at the top of mybase hosting logs:

DATABASE_URL → db.example.com:5432 unreachable from site network (timed out).
This project's Postgres is reachable here as postgres:5432. Run:
  mybase secrets link -p shop -e prod --database
  mybase hosting redeploy shop web

The deploy still goes on. To fail it instead, set strictEnvCheck: true on the site in mybase.config.ts (or mybase hosting set shop web --strict-env-check on). Only hosts and ports are checked and shown, never values.

A rollback is a new deploy of an older image, with the current env vars. The last 5 deploys per site keep their images; older images are deleted, and the registry is garbage-collected daily when nobody's pushing. Container logs are capped at two 5 MB files per container.

Networking and isolation

Each project gets its own Docker network (mybase-p-<project>). Sites reach mybase's services on it by name: postgres:5432, valkey:6379, garage:3900, and mybase:4000 (the API). You don't write these by hand: keys linked with mybase secrets link --database|--cache|--storage|--api resolve to them inside sites (TLS off, since the certificate is for the public name), and to other addresses elsewhere. Sites in other projects, the workers, and SearXNG aren't on it. Compose drops these extra networks whenever it recreates a container (mybase server update, a changed .env), so the hosting worker re-attaches mybase's services as soon as they start, at its own start, every 5 minutes, and before each deploy. mybase database access checks the hosting route live. Docker's default address pools allow about 30 networks, which caps the number of projects with hosting on.

Containers restart with Docker (unless-stopped), get Docker's default capabilities minus a few web servers don't need, and no-new-privileges. They aren't sandboxed like sandboxes: only deploy images you trust.

Run exactly one API instance: it watches deploys in memory.

Security

  • Sites are ordinary containers on the Docker host (not gVisor): deploy only code you trust. A hosting:write key can run code on the project's network, so guard it like the database password.
  • Serve sites from a different registrable domain than the dashboard (custom domains do this per site). Env names that steer the hosting worker (DOCKER_*, LD_*, PATH, HOME) are refused.
  • MYBASE_DNS_TOKEN can change DNS in the zones it covers: give it DNS edit + zone read on only those zones. Removing a domain deletes only the CNAME mybase made.

More: Security best practices.

Agent notes

  • Deploy with mybase deploy from the app's directory; never ask for the server's Docker access.
  • Sites must listen on $PORT and bind 0.0.0.0. HOSTNAME=0.0.0.0 is set for Next.js.
  • A failed deploy's error includes the container's last output: read it with the hosting_deploys MCP tool or mybase hosting deploys.
  • After changing env vars or the linked secrets env, redeploy (hosting_redeploy).
  • For mybase services, link the keys in the site's secrets env (mybase secrets link -p <p> -e <env> --database); they resolve to postgres:5432 and friends inside the site. Don't hand-write public or internal URLs.
  • A deploy warning like DATABASE_URL → … unreachable from site network carries the fix command; run it, then redeploy.
  • To serve a site at the user's own domain, use mybase hosting domain add <p> <site> <host> (MCP hosting_domain_add). If it reports an existing record, ask the user before retrying with --replace. After --primary, redeploy so MYBASE_SITE_URL changes.

On this page