Serve HTTPS by default with a local CA

The installer generates a CA and a 397-day certificate into
$COBBLE_HOME/certs (in the app image, with the host's addresses), mounts
the certificate, key and CA into the container and serves TLS from Bun on
the published port. --no-https opts out; --https-cert/--https-key use a
real certificate, --https-host adds names and --https-regen replaces the
CA. The installer and the image health check use curl -k when TLS is on.

The CA is served at /ca.crt and as an iOS/macOS profile at
/ca.mobileconfig for one-tap install. Development picks up certs/
automatically, `bun run dev:phone` generates and starts, and a plain-HTTP
listener on PORT + 1 redirects to HTTPS. Port sharing is off, so a stale
dev server fails instead of silently answering on the same port.
This commit is contained in:
2026-10-20 10:20:01 +01:00
parent 535adb6e16
commit e859bab294
26 changed files with 1582 additions and 40 deletions
+88 -15
View File
@@ -26,10 +26,13 @@ missing):
curl -fsSL https://url.moduletrace.com/cobble | sudo bash
```
Open the address it prints, usually `http://<your-ip>:7227`, and create the
super admin account. The first signup also creates the coordinator and the
main computer, which pulls the sandbox image in the background. Give it a
few minutes.
Open the address it prints, usually `https://<your-ip>:7227`. The
certificate is signed by a local CA the installer made, so the browser warns
until you trust it: open `https://<your-ip>:7227/ca.crt` on the device and
install it (see [HTTPS](#https)), then reload and create the super admin
account. The first signup also creates the coordinator and the main
computer, which pulls the sandbox image in the background. Give it a few
minutes.
Then open **Settings > Models**, add an OpenRouter key (or another provider)
and wait for the model list to sync. This is what the bots think with.
@@ -43,13 +46,47 @@ and wait for the model list to sync. This is what the bots think with.
- Docker Desktop on macOS works for development and small installs, but
Linux is the supported host.
- Port 7227, or another port with `--port`.
- HTTPS in front of the app before you expose it to the internet. Web Push
and installable PWA features need it. A Cloudflare Tunnel or Caddy in
front of `127.0.0.1:7227` is enough.
- HTTPS is on by default, with a certificate for this machine. Exposing the
app publicly? Use `--https-cert` and `--https-key` with a real
certificate, or `--no-https` behind a proxy or tunnel.
The installer binds to `0.0.0.0` so the app is reachable from your phone on
the same network. Use `--bind 127.0.0.1` when a reverse proxy is the only
thing that should reach it.
thing that should reach it (with `--no-https`).
## HTTPS
The installer serves HTTPS by default. It generates a local CA and a
certificate for this machine into `$COBBLE_HOME/certs` and mounts them into
the container read only. The CA is kept across updates; the certificate is
re-issued when the machine's addresses change or it is within 30 days of
expiring, and a rerun of the updater does both.
Trust the CA once on each device. Until it is trusted the browser warns, and
the installed app, its service worker and Web Push stay off:
- **Download:** open `https://<this machine>:7227/ca.crt` on the device, or
copy `$COBBLE_HOME/certs/ca.crt`.
- **iPhone or iPad:** open `https://<this machine>:7227/ca.mobileconfig`
instead. iOS offers the profile in one tap; install it, then turn it on
under Settings > General > About > Certificate Trust Settings.
- **Android:** Settings > Security > Encryption & credentials > Install a
certificate > CA certificate.
- **macOS:** add it to the login keychain and set it to Always Trust.
- **Linux:** copy it into `/usr/local/share/ca-certificates/` and run
`sudo update-ca-certificates`.
Options for the certificate:
- A VPN name or address the installer did not detect: `--https-host NAME`,
repeatable.
- Your own certificate (a public CA, an internal CA, `tailscale cert`):
`--https-cert FILE --https-key FILE`. Rerun the installer when it is
renewed.
- Replace the local CA, for example after trusting the wrong one:
`--https-regen`. Every device must trust the new CA.
- Plain HTTP with a proxy or tunnel in front: `--no-https`.
- Turn HTTPS back on after `--no-https`: `--https`.
## Update and uninstall
@@ -80,6 +117,12 @@ curl -fsSL https://url.moduletrace.com/cobble | sudo bash -s -- --uninstall --pu
| `--image IMAGE` | Container image, default `ghcr.io/moduletrace/cobble` |
| `--version TAG` | Image tag, default `latest` |
| `--extra-network NET` | Join another Docker network, for a tunnel or reverse proxy |
| `--no-https` | Serve plain HTTP, for a proxy or tunnel in front |
| `--https` | Serve HTTPS again after `--no-https` |
| `--https-host VALUE` | Names or IPs the certificate covers, repeatable |
| `--https-cert FILE` | Use your own certificate instead of the local CA |
| `--https-key FILE` | Its private key, given with `--https-cert` |
| `--https-regen` | Replace the local CA and the certificate |
| `--force` | Recreate the container even when it is current |
| `--no-pull` | Use an image that is already on the machine |
| `--skip-backup` | Update without a database backup |
@@ -96,6 +139,7 @@ Everything that must survive an update lives under the install directory:
.env # generated once, mode 600
install.conf # installer state
backups/ # pre-update database snapshots
certs/ # the local CA and the certificate (HTTPS)
data/
db/cobble
db/.backups/
@@ -103,13 +147,15 @@ Everything that must survive an update lives under the install directory:
computers/<id>/ # each computer's home directory
```
`.env` holds the port and the encryption keys. The keys encrypt sessions and
settings, so keep a copy and do not rotate them without re-entering provider
keys and other secrets. Provider keys, MCP servers, tools and app settings
are managed in the app, not in `.env`.
`.env` holds the port, the encryption keys and the `TLS_*` certificate
paths. The keys encrypt sessions and settings, so keep a copy and do not
rotate them without re-entering provider keys and other secrets. Provider
keys, MCP servers, tools and app settings are managed in the app, not in
`.env`.
Edit `.env` and run `docker restart cobble` to apply changes. The installer
only appends missing values and keeps what is already there.
only appends missing values and keeps what is already there, apart from the
port and the `TLS_*` entries it manages.
The app container is started with the Docker socket mounted and
`MAIN_SERVER=1`, so it can create computer containers and run cron, the
@@ -137,16 +183,42 @@ bun run db:schema
bun run dev
```
Open `http://localhost:7227`. With no users, every page goes to
`/auth/signup` to create the super admin. For a headless setup:
Open `http://localhost:7227`, or `https://localhost:7227` once `certs/`
exists. With no users, every page goes to `/auth/signup` to create the super
admin. For a headless setup:
```bash
SEED_SUPER_ADMIN_PASSWORD=... bun run seed
```
The development server reads the same `TLS_*` variables as the container, so
it can serve HTTPS too. That is how to try the app from a phone over a VPN,
where the secure context (installed app, Web Push, voice) needs it:
```bash
bun run dev:phone -- --host cobble-laptop.tailnet-name.ts.net
```
That generates `certs/` and starts the dev server with it, with no `.env`
edits. Afterwards a plain `bun run dev` reuses the certificate whenever
`certs/server.crt` and `certs/server.key` exist (production never does).
`bun run gen:certs` on its own regenerates them if you need to add a name.
Open `https://<the name you passed>:7227` on the phone and install the CA
from `https://<the name you passed>:7227/ca.mobileconfig` first. On the
laptop, trust the same `certs/ca.crt` once, so `https://localhost:7227`
stops warning.
With `certs/` present the dev server also listens one port above (`7228`)
for plain HTTP and redirects to the HTTPS page, so a stray
`http://localhost:7228/...` link still lands. The HTTPS port itself cannot
answer plain HTTP, so `http://localhost:7227` still fails; use
`DEV_HTTP_PORT` to move the redirect.
| Command | Purpose |
| -------------------- | ------------------------------------ |
| `bun run dev` | Development server |
| `bun run dev:phone` | Dev server over HTTPS for a phone |
| `bun run start` | Production server on the host |
| `bun run typecheck` | Type-check |
| `bun run test` | Run the test suite |
@@ -154,6 +226,7 @@ SEED_SUPER_ADMIN_PASSWORD=... bun run seed
| `bun run seed:bots` | Add a sample crew |
| `bun run db:schema` | Sync the schema and types |
| `bun run gen:favicon` | Regenerate favicons and PWA assets |
| `bun run gen:certs` | Generate a local HTTPS certificate |
The app is Bun, [`@moduletrace/bunext`](https://github.com/Moduletrace/bunext),
SQLite via [`@moduletrace/bun-sqlite`](https://github.com/Moduletrace/bun-sqlite),