A quick way to get going with testing BareMetal-Firecracker, BareMetal-AppPort, and uploading your program to BareMetal Cloud.
BareMetal is an exokernel written in x86-64 Assembly that expects a payload program. Your C program is compiled into an .app and is combined with the BareMetal kernel to produce a single bootable .elf unikernel. It can then be run directly in Firecracker microVM with as little as 4MiB of RAM. This repo wires together the pieces needed to go from a C file on your laptop to a running instance, either locally in a VM for fast iteration or on BareMetal Cloud for real deployment.
Log into BareMetal Cloud, open API keys, and create a new API key.
Enter the following commands, pasting the new key when ./bmcloud login asks for it:
git clone https://github.com/ReturnInfinity/BareMetal-App
cd BareMetal-App
./setup.sh
./bmcloud login
cp BareMetal-AppPort/hello.c .
./1-build.sh hello.c
./2-run.sh
./3-upload.sh
When prompted to upload to cloud hit Y. Your program should be running in BareMetal Cloud now.
./1-build.sh writes the name of the .app it built to .prog_app in the repo root, which ./3-upload.sh reads back so it knows which file to upload — this is what lets the two scripts be run separately, one after the other.
Confirm it:
./bmcloud vm list
The following commands must be installed before running ./setup.sh: git, curl, unzip, tar, gcc, nasm, make, patch, jq, mkfs.ext2 (from e2fsprogs). The script will check for these.
To run VMs locally you'll also need Firecracker installed and accessible, screen, and your user must be in the kvm group:
sudo usermod -aG kvm $YOURUSERNAME
Log out and back in for the group change to take effect.
git clone https://github.com/ReturnInfinity/BareMetal-App
cd BareMetal-App
./setup.sh
This runs a "pre-flight" check to verify the prerequisites above are installed (including mkfs.ext2, from e2fsprogs), then clones BareMetal-AppPort and BareMetal-Firecracker alongside this repo, copies the bundled libraries in files/ (lwIP, mbedTLS, musl) into the app-port build directory, builds everything, and downloads the bmcloud CLI into the repo root. It also creates a 512M disk.img, formatted as a plain EXT2 filesystem (mkfs.ext2 -b 4096 - the 4096-byte block size matters, see setup.sh's comment) for the VM to boot against; BareMetal-AppPort/port/ext4_shim.c mounts it through lwext4. A CA bundle (files/cacert.pem) is installed onto that image too, so https:// requests (tls_shim.c, curltest.c) verify the server's certificate rather than trusting it blindly; the same bundle is also compiled directly into every app binary as a fallback, so verification still works even with no disk attached at all.
Re-running ./setup.sh starts from a clean slate — it calls ./clean.sh first, which removes the cloned repos, baremetal.elf, disk.img, and bmcloud.
Any standard C program is a valid starting point. For example:
echo -e '#include <stdio.h>\n\nint main(void) {\n printf("Hello, World!\\n");\n return 0;\n}' > hello.c
./1-build.sh hello.c
You can also use multiple C files:
./1-build.sh testjson.c cjson/cjson.c
This will:
- Build your program into a BareMetal
.appviaBareMetal-AppPort/build-app.sh. - Link it into a bootable kernel image via
BareMetal-Firecracker/build.sh, producingbaremetal.elfin the repo root.
./1-build.sh script.sql [database.db|-]
As with .py and .lua files, nothing is compiled: sqlite.app (SQLite's own command-line shell, built by setup.sh) runs the script as /sqlite/main.sql on disk.img, against the database /sqlite/main.db, and prints each statement followed by its results. The database persists between runs. Give a local database file as the second argument to replace it with a copy, or - to start over with an empty one. Arguments to ./2-run.sh become shell options, e.g. ./2-run.sh -box or ./2-run.sh -header -csv. See examples/sqlite/hello.
./1-build.sh lighttpd [lighttpd.conf] [docroot-dir]
There's nothing to compile here: lighttpd.app (the lighttpd web server, see BareMetal-AppPort/LIGHTTPD.md) is already built by setup.sh. This copies a config to /lighttpd/lighttpd.conf on disk.img and the contents of a site directory to /lighttpd/www, replacing any previous site, then builds baremetal.elf around lighttpd.app. With no arguments it uses the sample config and site in BareMetal-AppPort/port/lighttpd_port/; start from that lighttpd.conf when writing your own. lighttpd listens on port 80 and keeps running until the VM is stopped. Under QEMU without tap0, forward a host port to it (see below):
HOSTFWD=18080:80 ./2-run-qemu.sh
curl http://127.0.0.1:18080/
./2-run.sh
Boot baremetal.elf locally under Firecracker and print its console output.
./2-run-qemu.sh
Boot the same baremetal.elf in a QEMU microvm instead, with disk.img and a network device attached and the serial console in your terminal. QEMU exits when the app finishes; press Ctrl-A then X to quit early. Needs qemu-system-x86_64 and access to /dev/kvm. The network uses tap0 if it exists, otherwise QEMU's user-mode networking (outbound only, no host setup needed). There is no memory hot-plug under QEMU, so the VM gets MEMSIZE MiB up front (default 256; e.g. MEMSIZE=1024 ./2-run-qemu.sh). Arguments are passed to the app as with ./2-run.sh. To reach a server in the guest over user-mode networking, set HOSTFWD to space-separated host:guest TCP port pairs; e.g. HOSTFWD=18080:80 makes guest port 80 reachable at 127.0.0.1:18080 (with tap0, connect to the guest's own address instead).
Uploading requires an API key. Create one on the API keys page of the BareMetal Cloud portal, then save it once with:
./bmcloud login
Upload baremetal.elf to BareMetal Cloud as a kernel image and launch it as a VM:
./3-upload.sh
The image and the VM are both named after your program (e.g. hello). See Deploying to BareMetal Cloud for managing it afterwards.
Local VM testing needs a tap0 device. Create one with:
./BareMetal-Firecracker/scripts/mkbr0.sh
This sets up a br0 bridge with tap0 attached in promiscuous mode. On a wired connection the host NIC is enslaved to the bridge for full L2 visibility to the VM; on Wi-Fi (which can't be bridged in station mode) it falls back to NAT so the guest still has outbound connectivity. If tap0 isn't present, ./2-run.sh warns and starts the VM without network access, and ./2-run-qemu.sh falls back to QEMU's user-mode networking.
./baremetal.sh controls the Firecracker VM directly:
| Command | Description |
|---|---|
start |
Configure and start the VM (kernel, disk, network, boot) |
status |
Check if the VM is currently running |
send <text> |
Send a line of text to the VM serial console |
output [--full] |
Print new console output since the last check (--full for the entire log) |
stop |
Gracefully shut down the VM (Ctrl+Alt+Del) |
attach |
Attach to the interactive screen session for the console |
help |
Show usage and current configuration |
./2-run.sh calls start, attach (when screen is installed), and output --full for you; use these directly when iterating without rebuilding, or attach to interact with the running program.
./3-upload.sh handles a single upload-and-launch flow. For everything else, use ./bmcloud, the BareMetal Cloud command-line client (setup.sh downloads it from https://baremetal.returninfinity.com/cli/bmcloud; ./3-upload.sh fetches it if it's missing). It needs bash 4+, curl, and jq.
Authenticate with an API key from the portal's API keys page, either saved once or set per shell:
./bmcloud login # prompts for the key and saves it to ~/.config/bmcloud/credentials
export BMC_API_KEY=bmc_... # or provide it per shell/session
Then:
./bmcloud [--json] [--url URL] <command> [args...]
VMs and images can be referred to by id or by a unique name.
| Command | Description |
|---|---|
login / logout |
Save / remove the API key |
whoami |
Account, plan, balance, and limits |
vm list |
List your VMs |
vm show <vm> |
Status, hostname, memory, disk, forwards, and domains of a VM |
vm create <name> --kernel <image> [--ram MiB] [--hotplug MiB] [--disk ext2|raw|none|image] [--disk-size MiB] [--disk-image <image>] [--args "app args"] [--no-start] [--no-https] |
Create (and by default start) a VM |
vm start|stop|suspend|resume <vm> |
Lifecycle actions (applied asynchronously) |
vm wait <vm> <status> [--timeout SECONDS] |
Wait until a VM reaches a status, e.g. running |
vm delete <vm> [--yes] |
Delete a VM and its disk |
vm args <vm> "<app args>" |
Change app arguments (VM must be stopped) |
vm memory <vm> <ram-mib> [hotplug-mib] |
Change memory (VM must be stopped) |
vm console <vm> [--bytes N] [--follow] |
Print the VM's console output |
vm usage <vm> |
Hourly resource usage and cost |
vm forwards <vm> |
List port forwards |
vm forward-add <vm> <http|tcp|udp> <guest-port> [public-port] |
Forward a public port (or the HTTPS hostname) to the VM |
vm forward-rm <vm> <forward-id> |
Remove a port forward |
vm domains <vm> |
Custom domains and the DNS records they need |
vm domain-add <vm> <hostname> |
Add a custom domain |
vm domain-check <vm> <domain-id> |
Retry the DNS check and certificate now |
vm domain-rm <vm> <domain-id> |
Remove a custom domain |
image list |
List your images |
image upload <file> [--name NAME] [--kind auto|kernel|disk] |
Upload a built .elf (kernel) or a disk image |
image delete <image> [--yes] |
Delete an image |
billing |
Balance, spend, and recent transactions |
billing usage |
Hourly usage records |
billing topup <amount> |
Fund the account (opens Stripe Checkout) |
Pass --json before a command to print the raw JSON API response instead (for scripting). Run ./bmcloud help for the full usage text.
./clean.sh
Removes the cloned BareMetal-AppPort and BareMetal-Firecracker repos, examples, baremetal.elf, disk.img, .prog_app, and bmcloud. Your saved bmcloud login credentials are kept. setup.sh runs this automatically before rebuilding.
