CHROOT.DEV BLOG2026-10-08CHROOT.DEV BLOG

An OpenBSD web server in one ssh command

Sign up with an invite, boot a VM that serves a page, stop it, start it, remove it.

One ssh command creates an OpenBSD VM, runs a script on its first boot and leaves httpd answering at https://web.chroot.run. From my connection in Brazil it took 0.9 s.

This post walks through the whole life of that VM on the 14-day trial: sign up, create it, look at the page, stop and start it, remove it. Every command after the sign-up was run against chroot.dev while writing this post; the outputs are from those runs, minus the login token and my other VMs.

The whole post in one take, from new to rm

During the beta, accounts are created by invitation: ask at invite@chroot.dev and you get a one-time code by email.

Before the first connection, check the host key. chroot.dev and chroot.run share one:

$ ssh-keyscan -t ed25519 chroot.dev 2>/dev/null | ssh-keygen -lf -
256 SHA256:y0rdGMRXmIFbxKPP6Qhmcx6Npr/rH8PdfYEgv+zehoo chroot.dev (ED25519)

Then connect with the key you want on the account and type the code:

$ ssh -t -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 chroot.dev
invite: code
email: alice@example.org (from the invite)
user: alice

The -o IdentitiesOnly=yes -i part matters if your agent holds more than one key. ssh offers them in order and the first one accepted wins, so name the key you mean. After this, plain ssh chroot.dev works.

The trial lasts 14 days and gives you 1 VM, 1 cpu, 512 MiB of memory, 5 GiB of disk and 10 GiB of traffic. ssh chroot.dev whoami shows the limits and what you use.

Put the setup in a file. It runs once, as the user puffy, on the first boot, and puffy has doas(1) without a password:

$ cat setup.sh
echo 'server "web" { listen on * port 80 }' |
    doas tee /etc/httpd.conf >/dev/null
echo 'hi g33ks =]' |
    doas tee /var/www/htdocs/index.html >/dev/null
doas rcctl enable httpd
doas rcctl start httpd

The same file is at https://chroot.dev/blog/quickstart/setup.sh. Create the VM with it:

$ ssh chroot.dev new -n web -w -s /dev/stdin < setup.sh
web 10.201.0.6
ssh in 758 ms

-n web names the VM; the name is also its hostname and its HTTPS name. -w waits until ssh on the VM answers and prints how long that took. -s /dev/stdin reads the first-boot script from the redirect.

Port 80 is where the HTTPS proxy sends requests by default, so there is nothing else to configure. httpd(8) could listen elsewhere, and ssh chroot.dev share port web 8080 would point the proxy there.

-w waits for ssh, not for the script. This one was done before my first request, but a script that runs pkg_add(1) takes longer, so poll the page instead of assuming it is up.

$ ssh vm+web@chroot.run uname -a
OpenBSD web 8.0 MICROVM#10 amd64

vm+NAME@chroot.run is the whole addressing scheme. Your ssh talks to chroot.run, which reaches the VM over the internal network, so you never see or accept a per-VM host key. Leave the command out for a shell.

https://web.chroot.run has a valid certificate from the start, but only you get through. A browser has no ssh key, so it logs in with a one-time link:

$ ssh chroot.dev browser
https://chroot.dev/__fcm/auth?t=...

Open the link and press Log in: from then on the proxy knows that browser on every VM you can reach. curl has no session, so it gets the redirect to the login:

$ curl -s -o /dev/null -w '%{http_code}\n' https://web.chroot.run
303

To let everyone see the page:

$ ssh chroot.dev share set-public web
$ curl https://web.chroot.run
hi g33ks =]

share add web bob lets one other user in instead, and share set-private web goes back. website(7) has the rest, including the header that tells your application who is logged in.

$ ssh chroot.dev stop web
$ ssh chroot.dev ls
NAME                 STATE    IP              CPUS    MEM  DISK IMAGE
web                  stopped  10.201.0.6         1    128     0 base
$ curl https://web.chroot.run
<!doctype html>
<meta charset=utf-8>
<title>chroot.dev</title>
<pre>web: VM stopped</pre>

stop is a clean shutdown and took 5 s. The proxy answers 503 with the reason while the VM is down.

$ ssh chroot.dev start web
$ curl https://web.chroot.run
hi g33ks =]

start returned in 0.2 s, and httpd answered under a second later; a request in between gets 502 with web: port 80 not responding. The disk kept the httpd.conf and rcctl enable brought httpd back. A stopped VM still counts against the trial's 1 VM and 5 GiB, so remove the ones you are done with.

$ ssh chroot.dev rm web
$ curl https://web.chroot.run
<!doctype html>
<meta charset=utf-8>
<title>chroot.dev</title>
<pre>web: does not exist (ref 1f296e21)</pre>

The proxy answers 404 for a name that does not exist. rm destroys the VM and its disk. There is no undo and there are no backups yet, so keep a copy of anything you care about.

  • cp(1) copies a running VM, memory included: the copy comes up with httpd already running.
  • api(7) runs the same commands over HTTPS with a token, for scripts and CI.
  • billing(1) has the plans for after the trial, from US$ 5 a month.
  • intro(1) is the start of the manual; curl chroot.dev/man/intro gives it as text.

Invites: invite@chroot.dev. Questions: contact@chroot.dev.

Murilo Ijanc

chroot.devOctober 8, 2026CHROOT.DEV BLOG