<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>chroot.dev blog</title><link href="https://chroot.dev/blog/"/><link rel="self" href="https://chroot.dev/blog/feed.xml"/><id>https://chroot.dev/blog/</id><updated>2026-10-08T00:00:00Z</updated><author><name>chroot.dev</name></author>
<entry><title>An OpenBSD web server in one ssh command</title><link href="https://chroot.dev/blog/quickstart/"/><id>https://chroot.dev/blog/quickstart/</id><published>2026-10-08T00:00:00Z</published><updated>2026-10-08T00:00:00Z</updated><author><name>Murilo Ijanc</name></author><summary>Sign up with an invite, boot a VM that serves a page, stop it, start it, remove it.</summary><content type="html">&lt;section class="Sh"&gt;&lt;h1 class="Sh"&gt;An OpenBSD web server in one ssh command&lt;/h1&gt;&lt;p class="Pp lede"&gt;Sign up with an invite, boot a VM that serves a page, stop it, start it, remove it.&lt;/p&gt;&lt;p class="Pp"&gt;One ssh command creates an OpenBSD VM, runs a script on its first boot and leaves httpd answering at &lt;code class="Li"&gt;https://web.chroot.run&lt;/code&gt;. From my connection in Brazil it took 0.9 s.&lt;/p&gt;
&lt;p class="Pp"&gt;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.&lt;/p&gt;
&lt;figure&gt;&lt;video controls preload="none" playsinline poster="https://chroot.dev/blog/quickstart/quickstart.jpg" width="1364" height="1072" aria-label="A terminal running the commands of this post, from new to rm"&gt;&lt;source src="https://chroot.dev/blog/quickstart/quickstart.mp4" type="video/mp4"&gt;&lt;a href="https://chroot.dev/blog/quickstart/quickstart.mp4"&gt;A terminal running the commands of this post, from new to rm&lt;/a&gt;&lt;/video&gt;&lt;figcaption&gt;The whole post in one take, from new to rm&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="sign-up-with-the-invite-code"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#sign-up-with-the-invite-code"&gt;SIGN UP WITH THE INVITE CODE&lt;/a&gt;&lt;/h2&gt;
&lt;p class="Pp"&gt;During the beta, accounts are created by invitation: ask at invite@chroot.dev and you get a one-time code by email.&lt;/p&gt;
&lt;p class="Pp"&gt;Before the first connection, check the host key. chroot.dev and chroot.run share one:&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ &lt;span class="nw"&gt;ssh-keyscan&lt;/span&gt; &lt;span class="nw"&gt;-t&lt;/span&gt; ed25519 chroot.dev &lt;span class="nw"&gt;2&amp;gt;/dev/null&lt;/span&gt; | &lt;span class="nw"&gt;ssh-keygen&lt;/span&gt; &lt;span class="nw"&gt;-lf&lt;/span&gt; &lt;span class="nw"&gt;-&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;256 &lt;span class="nw"&gt;SHA256:y0rdGMRXmIFbxKPP6Qhmcx6Npr/rH8PdfYEgv+zehoo&lt;/span&gt; chroot.dev (ED25519)
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;Then connect with the key you want on the account and type the code:&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh &lt;span class="nw"&gt;-t&lt;/span&gt; &lt;span class="nw"&gt;-o&lt;/span&gt; IdentitiesOnly=yes &lt;span class="nw"&gt;-i&lt;/span&gt; &lt;span class="nw"&gt;~/.ssh/id_ed25519&lt;/span&gt; chroot.dev
&lt;/span&gt;&lt;span class="l"&gt;invite: code
&lt;/span&gt;&lt;span class="l"&gt;email: alice@example.org (from the invite)
&lt;/span&gt;&lt;span class="l"&gt;user: alice
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;The &lt;code class="Li"&gt;-o IdentitiesOnly=yes -i&lt;/code&gt; 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 &lt;code class="Li"&gt;ssh chroot.dev&lt;/code&gt; works.&lt;/p&gt;
&lt;p class="Pp"&gt;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. &lt;code class="Li"&gt;ssh chroot.dev whoami&lt;/code&gt; shows the limits and what you use.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="create-a-vm-that-serves-a-page-on-its-first-boot"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#create-a-vm-that-serves-a-page-on-its-first-boot"&gt;CREATE A VM THAT SERVES A PAGE ON ITS FIRST BOOT&lt;/a&gt;&lt;/h2&gt;
&lt;p class="Pp"&gt;Put the setup in a file. It runs once, as the user puffy, on the first boot, and puffy has &lt;a class="Xr" href="https://man.openbsd.org/doas.1"&gt;&lt;b&gt;doas&lt;/b&gt;(1)&lt;/a&gt; without a password:&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ cat setup.sh
&lt;/span&gt;&lt;span class="l"&gt;echo 'server "web" { listen on * port 80 }' |
&lt;/span&gt;&lt;span class="l"&gt;    doas tee &lt;span class="nw"&gt;/etc/httpd.conf&lt;/span&gt; &lt;span class="nw"&gt;&amp;gt;/dev/null&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;echo 'hi g33ks =]' |
&lt;/span&gt;&lt;span class="l"&gt;    doas tee &lt;span class="nw"&gt;/var/www/htdocs/index.html&lt;/span&gt; &lt;span class="nw"&gt;&amp;gt;/dev/null&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;doas rcctl enable httpd
&lt;/span&gt;&lt;span class="l"&gt;doas rcctl start httpd
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;The same file is at &lt;code class="Li"&gt;https://chroot.dev/blog/quickstart/setup.sh&lt;/code&gt;. Create the VM with it:&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh chroot.dev new &lt;span class="nw"&gt;-n&lt;/span&gt; web &lt;span class="nw"&gt;-w&lt;/span&gt; &lt;span class="nw"&gt;-s&lt;/span&gt; &lt;span class="nw"&gt;/dev/stdin&lt;/span&gt; &amp;lt; setup.sh
&lt;/span&gt;&lt;span class="l"&gt;web 10.201.0.6
&lt;/span&gt;&lt;span class="l"&gt;ssh in 758 ms
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;&lt;code class="Li"&gt;-n web&lt;/code&gt; names the VM; the name is also its hostname and its HTTPS name. &lt;code class="Li"&gt;-w&lt;/code&gt; waits until ssh on the VM answers and prints how long that took. &lt;code class="Li"&gt;-s /dev/stdin&lt;/code&gt; reads the first-boot script from the redirect.&lt;/p&gt;
&lt;p class="Pp"&gt;Port 80 is where the HTTPS proxy sends requests by default, so there is nothing else to configure. &lt;a class="Xr" href="https://man.openbsd.org/httpd.8"&gt;&lt;b&gt;httpd&lt;/b&gt;(8)&lt;/a&gt; could listen elsewhere, and &lt;code class="Li"&gt;ssh chroot.dev share port web 8080&lt;/code&gt; would point the proxy there.&lt;/p&gt;
&lt;p class="Pp"&gt;&lt;code class="Li"&gt;-w&lt;/code&gt; waits for ssh, not for the script. This one was done before my first request, but a script that runs &lt;a class="Xr" href="https://man.openbsd.org/pkg_add.1"&gt;&lt;b&gt;pkg_add&lt;/b&gt;(1)&lt;/a&gt; takes longer, so poll the page instead of assuming it is up.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="log-in"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#log-in"&gt;LOG IN&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh vm+web@chroot.run uname &lt;span class="nw"&gt;-a&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;OpenBSD web 8.0 MICROVM#10 amd64
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;&lt;code class="Li"&gt;vm+NAME@chroot.run&lt;/code&gt; 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.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="the-page-is-private-until-you-share-it"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#the-page-is-private-until-you-share-it"&gt;THE PAGE IS PRIVATE UNTIL YOU SHARE IT&lt;/a&gt;&lt;/h2&gt;
&lt;p class="Pp"&gt;&lt;code class="Li"&gt;https://web.chroot.run&lt;/code&gt; 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:&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh chroot.dev browser
&lt;/span&gt;&lt;span class="l"&gt;&lt;span class="nw"&gt;https://chroot.dev/__fcm/auth?t=...&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;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:&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ curl &lt;span class="nw"&gt;-s&lt;/span&gt; &lt;span class="nw"&gt;-o&lt;/span&gt; &lt;span class="nw"&gt;/dev/null&lt;/span&gt; &lt;span class="nw"&gt;-w&lt;/span&gt; '%{http_code}\n' &lt;span class="nw"&gt;https://web.chroot.run&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;303
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;To let everyone see the page:&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh chroot.dev share &lt;span class="nw"&gt;set-public&lt;/span&gt; web
&lt;/span&gt;&lt;span class="l"&gt;$ curl &lt;span class="nw"&gt;https://web.chroot.run&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;hi g33ks =]
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;&lt;code class="Li"&gt;share add web bob&lt;/code&gt; lets one other user in instead, and &lt;code class="Li"&gt;share set-private web&lt;/code&gt; goes back. &lt;a class="Xr" href="https://chroot.dev/man/website/"&gt;&lt;b&gt;website&lt;/b&gt;(7)&lt;/a&gt; has the rest, including the header that tells your application who is logged in.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="stop-and-start-the-disk-stays"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#stop-and-start-the-disk-stays"&gt;STOP AND START: THE DISK STAYS&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh chroot.dev stop web
&lt;/span&gt;&lt;span class="l"&gt;$ ssh chroot.dev ls
&lt;/span&gt;&lt;span class="l"&gt;NAME                 STATE    IP              CPUS    MEM  DISK IMAGE
&lt;/span&gt;&lt;span class="l"&gt;web                  stopped  10.201.0.6         1    128     0 base
&lt;/span&gt;&lt;span class="l"&gt;$ curl &lt;span class="nw"&gt;https://web.chroot.run&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;&amp;lt;!doctype html&amp;gt;
&lt;/span&gt;&lt;span class="l"&gt;&amp;lt;meta &lt;span class="nw"&gt;charset=utf-8&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;&lt;span class="nw"&gt;&amp;lt;title&amp;gt;chroot.dev&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;&amp;lt;pre&amp;gt;web: VM &lt;span class="nw"&gt;stopped&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;stop is a clean shutdown and took 5 s. The proxy answers 503 with the reason while the VM is down.&lt;/p&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh chroot.dev start web
&lt;/span&gt;&lt;span class="l"&gt;$ curl &lt;span class="nw"&gt;https://web.chroot.run&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;hi g33ks =]
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;start returned in 0.2 s, and httpd answered under a second later; a request in between gets 502 with &lt;code class="Li"&gt;web: port 80 not responding&lt;/code&gt;. The disk kept the httpd.conf and &lt;code class="Li"&gt;rcctl enable&lt;/code&gt; 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.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="remove-it"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#remove-it"&gt;REMOVE IT&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;span class="l"&gt;$ ssh chroot.dev rm web
&lt;/span&gt;&lt;span class="l"&gt;$ curl &lt;span class="nw"&gt;https://web.chroot.run&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;&amp;lt;!doctype html&amp;gt;
&lt;/span&gt;&lt;span class="l"&gt;&amp;lt;meta &lt;span class="nw"&gt;charset=utf-8&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;&lt;span class="nw"&gt;&amp;lt;title&amp;gt;chroot.dev&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class="l"&gt;&amp;lt;pre&amp;gt;web: does not exist (ref &lt;span class="nw"&gt;1f296e21)&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/pre&gt;
&lt;p class="Pp"&gt;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.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="where-to-go-from-here"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#where-to-go-from-here"&gt;WHERE TO GO FROM HERE&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;&lt;li&gt;&lt;a class="Xr" href="https://chroot.dev/man/cp/"&gt;&lt;b&gt;cp&lt;/b&gt;(1)&lt;/a&gt; copies a running VM, memory included: the copy comes up with httpd already running.&lt;/li&gt;&lt;li&gt;&lt;a class="Xr" href="https://chroot.dev/man/api/"&gt;&lt;b&gt;api&lt;/b&gt;(7)&lt;/a&gt; runs the same commands over HTTPS with a token, for scripts and CI.&lt;/li&gt;&lt;li&gt;&lt;a class="Xr" href="https://chroot.dev/man/billing/"&gt;&lt;b&gt;billing&lt;/b&gt;(1)&lt;/a&gt; has the plans for after the trial, from US$ 5 a month.&lt;/li&gt;&lt;li&gt;&lt;a class="Xr" href="https://chroot.dev/man/intro/"&gt;&lt;b&gt;intro&lt;/b&gt;(1)&lt;/a&gt; is the start of the manual; &lt;code class="Li"&gt;curl chroot.dev/man/intro&lt;/code&gt; gives it as text.&lt;/li&gt;&lt;/ul&gt;
&lt;p class="Pp"&gt;Invites: invite@chroot.dev. Questions: contact@chroot.dev.&lt;/p&gt;&lt;/section&gt;
&lt;section class="Sh"&gt;&lt;h2 class="Sh" id="authors"&gt;&lt;a class="permalink" href="https://chroot.dev/blog/quickstart/#authors"&gt;AUTHORS&lt;/a&gt;&lt;/h2&gt;&lt;p class="Pp"&gt;Murilo Ijanc&lt;/p&gt;&lt;/section&gt;</content></entry>
</feed>
