Skip to content

Create a VM

A VM is one demo. It has a name, a build recipe, and — once it has been built — a saved machine that visitors resume.

Every VM lives in a project, and its full name is org/project:vm. That name is what share links and embeds use.

Create the VM

Open your project and press Create a VM. Three fields:

FieldNotes
NameLowercase letters, digits and hyphens. This is the :vm part of the reference.
Visibilitypublic — anyone with the link can run it. private — only your own site, using an embed key. See Sharing.
DescriptionOptional, for your own dashboard.

Import existing VM

If you already created a VM in another project or account and would like to move it into the current one, you can use Import VM from JSON option, from the project's menu. One JSON file carries the whole definition: the recipe, the watches and the guide. Export JSON in a VM's menu writes that file.

The build recipe

The recipe is how the machine is made: a base image plus the shell commands that install your software on it. It is the same idea as a Dockerfile — a written-down description of a machine.

Base image

BaseWhat is in it
Alpine (minimal)Small Alpine Linux with networking and apk. The right default for terminal apps, single binaries and servers.
Alpine with Docker (optimized)The same, plus a working Docker daemon, for demos that docker pull your published image.

Pick the Docker one only if the demo really runs docker. It is a bigger image and it boots slower for your visitors.

Base image script

The shell commands, up to 64 KB. Three rules:

  • They run as root, in order, in one shell. A cd on one line still applies to the next.
  • Every command must finish. A script that ends by starting an app which keeps running (mc, top, a foreground server) never exits, so the build waits for the timeout and saves nothing. Use VM startup below to start the app you'd like to demo, and keep the script for preparing the VM before it launches.
  • What it prints is not what visitors see. The script runs while the VM is being built; a visitor opens a restored machine with a fresh terminal, so nothing printed here is on their screen, and a cd here does not move the shell they get. Both of those belong in After VM load below.
  • The architecture is arm64 (aarch64). Fetch arm64 binaries and images.

The build always has internet, so apk add, curl, wget and docker pull all work here.

See a few example below.

sh
# install your app from the Alpine package index
apk add mc
sh
# or fetch your own arm64 linux binary and put it on PATH
apk add curl
curl -fLo /usr/local/bin/yourapp https://example.com/yourapp-linux-arm64
chmod +x /usr/local/bin/yourapp

Go tips and tricks

Many terminal tools are Go binaries, and two Go runtime settings make them faster on an emulated CPU. Set them with a small wrapper in the base image script, so they apply no matter how the tool is started:

sh
# rename the real binary, put a wrapper in its place
mv /usr/local/bin/yourapp /usr/local/bin/yourapp-real
cat > /usr/local/bin/yourapp <<'WRAP'
#!/bin/sh
export GOGC=off GOMEMLIMIT=400MiB
exec /usr/local/bin/yourapp-real "$@"
WRAP
chmod +x /usr/local/bin/yourapp
  • GOGC=off turns the garbage collector off. For a command that runs and exits this is the right call: the collector's cost grows with the size of the binary, and on a large tool it can be more than half of what each command spends. Commands on the dolt demo got 3× faster from this wrapper.
  • GOMEMLIMIT=400MiB keeps a limit anyway. A command that allocates a lot still collects garbage near the limit instead of running out of memory. 400 MiB is a good number for the default 1 GB VM.

Wrap command-line tools only. A Go server that keeps running (the app on your VM startup line) needs its collector: give it GOGC=400 instead, which collects a quarter as often as the default and still keeps memory flat.

One more thing worth a check: some Go tools fork a background process after every command, for telemetry or update checks. On one core that process competes with the next command your visitor types. Run the tool once, wait for the prompt, then run ps. If something is still running, look for the tool's own off switch and add it to the wrapper (for dolt it is DOLT_DISABLE_EVENT_FLUSH=1).

VM startup

Optional, and one question: what does a visitor land in? Pick one of the three answers — a demo has exactly one.

A shell prompt. The box under it, After VM load, takes shell commands (up to 4 KB) that run once the VM has finished loading — in the build, and again for every visitor who opens the demo. Their output is the first thing on the visitor's screen.

sh
cd /root/project
cat /etc/motd

This is the only place that can decide the directory the demo opens in and the message on its first screen. The base image script cannot: it runs in a shell of its own while the VM is being built, and a visitor's terminal starts empty.

Because it runs for every visitor, keep it to the commands that set the scene. Anything that installs, downloads or starts a background process belongs in the base image script, which runs once, at build time.

An app. One line, up to 512 characters. It runs after the script and is left running when the machine is captured, so a visitor opens straight into the app rather than at a prompt.

Setup goes in front of it on the same line — cd /root/work && mc. There is no after-load script for this answer: the VM resumes with the app already running, and there is no shell there to run one.

A web page. The demo opens on the web UI of a server running inside the VM — your dashboard, your admin panel, your app — instead of on a terminal. It has a section of its own below.

Open a web page

Fill in Web page address and the demo shows that page instead of a terminal. The address is the one inside the VM:

http://localhost:7700/

Only http://localhost:<port>/… (or http://127.0.0.1:<port>/…) works, and only plain http. The page comes from the server running in the VM, in your visitor's own browser — not from the internet, so there is nothing to reach over https and no other host to name. Leave the port out and it means 80. A path and a query are kept; anything after a # is not.

Bind 0.0.0.0, not 127.0.0.1

The request reaches your server from outside the VM, so a server listening only on loopback refuses it and the demo never opens. Start it on all addresses — meilisearch --http-addr 0.0.0.0:7700, directus start --host 0.0.0.0. The address you type above stays http://localhost:7700/ either way: that is what the VM itself calls it.

Start the server in whichever of the three places fits it:

  • the base image script, if the package installs a service that comes up on its own;

  • Start command — the usual answer. It is captured running, so a visitor resumes straight into a server that is already up;

  • Run on every load — only for a server that does not come back after the VM is resumed. Visitors then wait for it to start, so the demo opens slower.

    On a web demo these commands start by stopping whatever is still listening on your port, so the fresh copy can bind it. That means Run on every load on a web demo has to start the server — it cannot only set up the shell, or nothing will be listening when the visitor arrives.

What a visitor sees. The app's page fills the whole embed, and two small buttons sit in its top-right corner:

buttonwhat it does
▤ TERMINALslides the terminal in — under the page, or beside it on a wide frame. Press again to hide it
↗ OPENopens the same page in a tab of its own. Shown where the opened tab can still reach the VM — on an embed in your own page, not on our gallery or in the studio

The terminal is still there, still real, and still runs your After VM load commands if you set any. It just is not the first thing on screen.

While the server is starting, the visitor sees the loading panel with one extra line, starting the app. If it never answers, the terminal opens instead with a line saying which port stayed quiet — the logs, which is what somebody needs at that point, rather than a blank box.

Embedding a web demo is the same one line as any other demo; see Embedding.

vCPUs

How many cores the VM has, 1 to 4. The saved machine keeps the count, so every visitor gets it.

Leave it at 1 unless you have a reason. More cores help software that genuinely runs work in parallel, and cost your visitors more of their own machine. They do not make a docker run faster — that work is close to serial, and measures slower on 4 cores than on 1.

Enable internet access

Off by default. This is about the served demo, not the build — a build always has internet.

Turn it on when visitors need to download a file, call an API or clone a repository from inside the demo. It is part of the paid plan.

Traffic is metered per visitor address, 100 MB per 24 hours. Loading the demo itself does not count — only what the machine downloads while it runs. When a visitor runs out, the demo keeps running without internet and a warning appears.

Build settings

SettingDefaultRangeWhat it does
Wait before snapshotting0 s0–600 sHow long to let the started app run before capturing. Raise it for an app that draws a screen and then keeps loading, so the demo (and its screenshot) does not open on a half-drawn UI.
Build timeout300 s1–3600 sHow long the whole build may take. Raise it if the script installs a lot. The build is emulated, so it is slower than the same commands on your laptop.

Press Save recipe when you are done.

Build it

Press ▶ Build on the VM page:

  1. A base image boots in your browser tab.
  2. Your script is typed into it and runs as root.
  3. If the script exits 0, the start command runs and is left up.
  4. The machine — memory, disk, running processes and all — is saved, along with a screenshot of the terminal.

Keep the tab open until it finishes. The build runs on your machine.

A failed build is harmless

If any command exits non-zero, or the build reaches the timeout, nothing is saved and the demo that is already live keeps serving.

Try it

The VM page shows the screenshot taken at the end of the build. Press ▶ Launch on it to boot the saved machine — the same machine a visitor resumes. It is the fastest way to see whether the demo opens where you meant it to.

Live mode, for figuring out the commands

The studio also has Live mode (testing): boot a base image (or run a VM's recipe from scratch) and type into the terminal yourself.

Use it as playground to find out which packages you need and which commands work, then paste those commands into the recipe and build from that. A live-mode boot is always 1 core, whatever the recipe says.

Managing a VM

The VM's menu holds:

  • Edit — rename, change visibility, edit the description.
  • Export JSON — the whole definition in one file.
  • Delete VM — removes the VM, its recipe, its watches and its guide.

Renaming changes the reference

The name is the VM's identity. Rename acme/web:hello to acme/web:demo and every share link and embed naming the old one stops resolving. The app warns you before the save.

When a build does not work

What you seeUsual cause
The build hangs and then times outA command in the script never exits. Move the app that keeps running into VM startup → An app.
The demo opens at a blank prompt in the wrong directoryThe script's cd and its output belong to the build, not to the visitor. Put them in VM startup → After VM load.
"start command exited"The app died at startup. Run it in live mode and read the error.
apk cannot find a packageThe package does not exist for Alpine arm64, or the name is different. Check in live mode.
The binary will not runIt is not an arm64 Linux build.
The demo opens on a half-drawn screenRaise Wait before snapshotting.
The build runs out of timeRaise Build timeout. Emulated installs are slower than they are on your machine.

Next