Run Your Second Firecracker microVM: REST API Way
Why Use the Firecracker API?
In the previous lesson, we booted a microVM by handing Firecracker a single JSON file with --config-file and explicitly disabling the API with --no-api. That mode is great for getting started: you point at a kernel and a rootfs, and Firecracker starts a VM in one go. But once the guest is running, Firecracker stops listening to you. The only "control" you have over the live VM is sending the termination signal to its host process.
The API mode is the other end of the spectrum. You launch firecracker first, without any configuration; it opens a Unix domain socket and waits for HTTP requests. You then control every step of the lifecycle - from configuring the boot source to gracefully shutting the guest down - by sending small JSON payloads to that socket. It's the same VMM, but exposed as a tiny REST service instead of a single CLI invocation.
This is the path most real-world systems take. AWS Lambda, Fly.io, E2B, and iximiuz Labs all speak to Firecracker over its API.
Regardless of whether you use curl or consume the API via the official Go SDK, the mental model you build here is exactly the one you'll need when you start using Firecracker in production.
What you can do over the API that you can't do via the config file
The Firecracker REST API is small - a couple of dozen endpoints - but it covers the entire lifecycle of a microVM. A few things you can do with it that simply have no equivalent in the --no-api mode:
- Inspect the live machine.
GET /returns the current state (Running,Paused, ...) and instance metadata, andGET /vm/configdumps the full effective configuration of the running guest. - Pause and resume execution.
PATCH /vmflips the VM betweenPausedandResumed- vCPUs stop scheduling, but the guest's memory is kept intact. This is how snapshotting works under the hood, and how platforms like Fly.io implement "stopped but warm" instances. - Snapshot and restore.
PUT /snapshot/createwrites the current memory and machine state to disk, andPUT /snapshot/loadboots a fresh microVM straight from that snapshot - skipping kernel boot entirely. That's what enables sub-100 ms cold starts in production microVM platforms. - Hot-update devices. Once the guest is running,
PATCH /drives/{id}can swap a backing file (handy for CI sandboxes that hand fresh rootfses to short-lived jobs), andPATCH /network-interfaces/{id}can re-tune rate limiters on the fly. - Graceful shutdown.
PUT /actionswithSendCtrlAltDelasks the guest to power off cleanly through ACPI - much friendlier than killing the host process. - Hot(un)plug memory and tune ballooning.
PATCH /hotplug/memoryresizes a virtio-mem region the guest sees as live RAM, andPATCH /balloontells the in-guest balloon driver to give back or take memory. Both let the host change a guest's working set without rebooting it. - Read out runtime telemetry.
GET /balloon/statisticsexposes the guest's memory accounting, andPUT /metricsandPUT /loggerwire up structured logs and metrics streams.
We're keeping things minimal again. No networking, no SSH, no extra features - just the kernel, a rootfs, and a Firecracker process listening on the API socket. Every networking-related step we'll skip here gets its own dedicated lesson later in the course.
Start Firecracker in API Mode
Before we can send any API requests, we need a Firecracker process listening on a socket. Unlike the config-file mode, launching firecracker does NOT start the VM - it just opens the socket and waits for instructions.
Everything we need from the previous lesson - the firecracker binary, the kernel, the rootfs - has already been prepared for you under ~/vm/:
ls -lh ~/vm
-rw-r--r-- 1 laborant laborant 1.0G rootfs.ext4
-rwxr-xr-x 1 laborant laborant ... vmlinux.bin
If you'd like to refresh how these artifacts were produced, jump back to the previous lesson - the steps are exactly the same as before.
Now let's launch Firecracker in API mode. Keep this terminal open - the firecracker process will run in the foreground and the guest's serial console will eventually attach to it:
sudo firecracker --api-sock ~/vm/firecracker.sock
You should see no output - that's intended. Firecracker is now sitting on the Unix socket waiting for the first PUT request. Compared to the config-file mode, nothing about the VM has been decided yet: no kernel, no drives, no vCPU count.
In a separate terminal, you can confirm the socket is there:
ls -lh ~/vm/firecracker.sock
srwxr-xr-x 1 root root 0 ... /home/laborant/vm/firecracker.sock
The leading s in the file mode means it's a Unix domain socket. We'll be talking to it for the rest of the lesson with curl --unix-socket.
A quick sanity check - the API responds even before the VM is configured:
curl --silent --unix-socket ~/vm/firecracker.sock \
http://localhost/ | jq
{
"app_name": "Firecracker",
"id": "anonymous-instance",
"state": "Not started",
"vmm_version": "1.15.1"
}
The state: "Not started" confirms we're pre-boot. Let's change that next.
Configure the microVM Over the API
Now that Firecracker is listening, we'll send it the same three pieces of information that lived in the JSON config file last lesson - boot source, root drive, and machine config - except this time each one becomes a separate PUT request to the corresponding endpoint.
All commands in this unit go in Terminal 2, while Firecracker keeps running in Terminal 1.
To save typing, define a small helper that wraps curl with the right flags:
SOCK=/home/laborant/vm/firecracker.sock
fc_api() {
local method="$1" path="$2" data="$3"
sudo curl --silent --show-error --fail \
--unix-socket "${SOCK}" \
-X "${method}" "http://localhost${path}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d "${data}"
}
The http://localhost part is just a placeholder - HTTP requires a host header but the actual transport here is the Unix socket, not TCP. The path after localhost is what selects the API endpoint. We use sudo because the socket was created by the sudo firecracker process from Terminal 1.
1. Set the boot source
The /boot-source endpoint tells Firecracker which kernel to load and which command-line arguments to pass to it. The kernel command-line is the exact same string we wrote into boot_args in the JSON config last lesson:
fc_api PUT /boot-source '{
"kernel_image_path": "/home/laborant/vm/vmlinux.bin",
"boot_args": "console=ttyS0 reboot=k panic=1"
}'
2. Attach the root drive
Next, PUT /drives/{drive_id} adds a block device backed by the rootfs ext4 file. The drive_id is just a label we choose - we'll call it rootfs:
fc_api PUT /drives/rootfs '{
"drive_id": "rootfs",
"path_on_host": "/home/laborant/vm/rootfs.ext4",
"is_root_device": true
}'
You can attach more than one drive - just PUT /drives/<another-id> with a different path_on_host. Only one drive is allowed to have is_root_device: true, though.
3. Set the machine resources
The /machine-config endpoint defines the vCPU count and memory size - the same machine-config block we had in the JSON file:
fc_api PUT /machine-config '{
"vcpu_count": 2,
"mem_size_mib": 1024
}'
At this point, Firecracker has all the pieces it needs to boot, but the VM is still not running - we haven't told it to start yet. You can verify that yourself:
curl --silent --unix-socket "${SOCK}" http://localhost/ | jq -r '.state'
Not started
In the next unit, we flip that switch.
Boot the microVM
With the boot source, root drive, and machine config in place, the only thing left to do is to fire the start action. Firecracker exposes lifecycle actions through a single endpoint - PUT /actions - and uses the action_type field to discriminate between them. The relevant ones for now are InstanceStart (boot the VM) and SendCtrlAltDel (graceful shutdown).
From Terminal 2, send the start action:
fc_api PUT /actions '{"action_type":"InstanceStart"}'
The request returns immediately with a 204 No Content - and almost at the same instant, Terminal 1 starts streaming kernel boot messages as the guest comes up. The serial console is attached to Firecracker's stdout, exactly like in the config-file mode.
After a brief flurry of messages, you should see the OpenRC banner and an Alpine login prompt in Terminal 1:
Welcome to Alpine Linux 3.23
Kernel 6.18.21 on x86_64 (/dev/ttyS0)
secondvm login:
Log in as root with password root and take a look around:
uname -a
cat /etc/alpine-release
cat /etc/hostname
free -m
nproc
ls /
ps aux
The output should look essentially identical to what you saw in the previous lesson - same kernel, same rootfs, same shape. The only thing that changed is how we got here.
We deliberately did not configure a network interface, so the guest has only the loopback device. We'll cover networking via the API in the upcoming networking lessons - and once you've seen this lesson, those PUT requests will feel familiar.
Keep both terminals open - in the next units we'll start poking the running VM through the API while it's alive.
Inspect the Live microVM
With the VM running and Firecracker still listening on the socket, we can ask it questions about itself. From Terminal 2 (Terminal 1 is still attached to the guest console):
Instance info
GET / returns a small "header" object that summarizes the microVM's lifecycle state:
curl --silent --unix-socket "${SOCK}" http://localhost/ | jq
{
"app_name": "Firecracker",
"id": "anonymous-instance",
"state": "Running",
"vmm_version": "1.15.1"
}
The state field is the most useful one in practice. It's how a control plane checks whether a microVM is Running, Paused, or hasn't been started yet. We'll come back to this field in the next unit.
VMM version
If you only need the Firecracker version, /version is a focused endpoint:
curl --silent --unix-socket "${SOCK}" http://localhost/version | jq
{
"firecracker_version": "1.15.1"
}
Effective machine configuration
GET /machine-config returns whatever Firecracker actually accepted for the machine shape - including any defaults that were filled in for fields you didn't explicitly set:
curl --silent --unix-socket "${SOCK}" http://localhost/machine-config | jq
{
"vcpu_count": 2,
"mem_size_mib": 1024,
"smt": false,
"track_dirty_pages": false,
"huge_pages": "None"
}
Full effective configuration
GET /vm/config is the heavy-duty cousin - it returns the entire effective configuration as a single JSON document, including all sections (boot-source, drives, machine-config, network interfaces, etc.):
curl --silent --unix-socket "${SOCK}" http://localhost/vm/config | jq
{
"boot-source": {
"kernel_image_path": "/home/laborant/vm/vmlinux.bin",
"boot_args": "console=ttyS0 reboot=k panic=1",
"initrd_path": null
},
"drives": [
{
"drive_id": "rootfs",
"path_on_host": "/home/laborant/vm/rootfs.ext4",
"is_root_device": true,
"is_read_only": false,
...
}
],
"machine-config": {
"vcpu_count": 2,
"mem_size_mib": 1024,
...
},
"network-interfaces": [],
...
}
This is what you'd dump from a control plane to record exactly how a particular guest was provisioned - useful for debugging "wait, what was attached to that VM?" tickets, and for building snapshot-equivalent configurations when restoring a guest on another host.
Try filtering for individual sections with jq:
curl --silent --unix-socket "${SOCK}" http://localhost/vm/config | jq '."boot-source"'
curl --silent --unix-socket "${SOCK}" http://localhost/vm/config | jq '.drives'
These read-only endpoints are the closest Firecracker comes to a "show me everything you know" command. Internalize them - in the Go SDK lesson and the production-style examples later in the course, this is how you'll reconcile what should be true about a microVM with what actually is.
Pause and Resume the microVM
A microVM is a process from the host's point of view, but it's also a guest with vCPUs, memory, and devices that the VMM can stop and re-arm at will. The API mode lets us freeze a running guest in place and then thaw it later - all without touching the underlying firecracker host process.
This is what PATCH /vm is for. It takes a single field, state, with two valid values:
Paused- stop scheduling vCPUs, keep memory and device state intact.Resumed- resume vCPU scheduling.
Pause the guest
From Terminal 2, freeze the VM:
fc_api PATCH /vm '{"state":"Paused"}'
The request returns immediately. Switch over to Terminal 1 - the serial console is still attached, but now it's completely unresponsive. Hit Enter a few times: nothing happens. The guest's vCPUs aren't being scheduled, so no command, kernel timer, or output thread can make progress.
Confirm the state through the API:
curl --silent --unix-socket "${SOCK}" http://localhost/ | jq -r '.state'
Paused
A paused microVM still owns its memory on the host. From the host's perspective, the firecracker process is still alive and its RSS hasn't shrunk. What changed is that no vCPU thread is being scheduled - so the guest sees time as if it has stopped.
This is the exact primitive that snapshotting builds on: pause first, then PUT /snapshot/create to dump the frozen state to disk. We'll see that pattern in a later lesson.
Resume the guest
Now flip it back:
fc_api PATCH /vm '{"state":"Resumed"}'
Switch back to Terminal 1 - the prompt is alive again, and any keys you type now reach the guest. Time inside the guest also resumes from where it left off; from the guest's perspective, no time elapsed during the pause:
date
Verify through the API too:
curl --silent --unix-socket "${SOCK}" http://localhost/ | jq -r '.state'
Running
This pause/resume primitive is small, but it's the foundation under most microVM-based platforms. Sub-second cold starts, "warm pools" of suspended VMs, snapshots, live migration - they all start with a PATCH /vm.
Gracefully Shut the microVM Down
In the previous lesson we shut the microVM down by typing reboot from inside the guest. That works - but it requires getting into the guest first. With the API mode, we can ask the guest to power off from the outside, without ever touching the console.
The mechanism is PUT /actions with action_type: "SendCtrlAltDel". Despite the name, it doesn't literally send a key sequence - on x86 microVMs, Firecracker raises an ACPI shutdown event, and the guest's init system reacts to it the same way it would react to a power button press on a real machine.
Send the action from Terminal 2:
fc_api PUT /actions '{"action_type":"SendCtrlAltDel"}'
Switch to Terminal 1: you should see OpenRC's shutdown sequence run through (stopping local services, unmounting /, syncing disks), followed by the kernel's reboot poweroff message. Because we set reboot=k panic=1 and Firecracker doesn't actually support reboot, the host process exits cleanly:
* Stopping local ... [ ok ]
* Unmounting loop devices
* Unmounting filesystems
...
[ 42.130991] reboot: Restarting system
[ 42.131488] reboot: machine restart
The Firecracker process in Terminal 1 has now exited, and the API socket is gone. Confirm from Terminal 2:
ls /home/laborant/vm/firecracker.sock 2>&1
ls: cannot access '/home/laborant/vm/firecracker.sock': No such file or directory
pgrep -fa firecracker || echo "no firecracker process"
no firecracker process
Why this matters in practice. Sending SIGKILL (or just unplugging the host) would also stop the guest, but it would leave the rootfs in whatever state it happened to be in - dirty filesystem journals, half-written log files, broken application state. SendCtrlAltDel lets the guest's init system run its shutdown hooks, so files are flushed, daemons get a chance to deregister, and the rootfs ext4 image is cleanly unmounted.
For workloads that care about durability - databases, queues, anything stateful - always prefer the graceful path.
There's also a "harder" option: PUT /actions doesn't have a force-shutdown action, so if the guest hangs and doesn't react to SendCtrlAltDel, you fall back to terminating the host firecracker process (SIGTERM or SIGKILL). That's the equivalent of pulling the power - useful, but not your default.
Summary
We booted the same microVM as in the previous lesson - same kernel, same rootfs, same shape - but instead of a single static config file, we drove every step through Firecracker's REST API on a Unix socket.
To recap the lifecycle:
- Started Firecracker in server mode -
firecracker --api-sock <socket>opens the Unix socket and waits, with no VM configured yet. - Configured the boot source, root drive, and machine shape - three
PUTrequests (/boot-source,/drives/{id},/machine-config) carrying the same JSON shape we had in the static config file. - Started the VM -
PUT /actionswithInstanceStart. The guest came up on Terminal 1's serial console. - Inspected the live machine -
GET /,GET /machine-config,GET /vm/config,GET /version. None of these have an equivalent in the config-file mode. - Paused and resumed the guest -
PATCH /vmwithPaused/Resumed. The vCPUs stop scheduling but memory stays put - the foundation for snapshots, warm pools, and live migration. - Shut the guest down gracefully -
PUT /actionswithSendCtrlAltDel. The guest's init system runs its shutdown hooks; the hostfirecrackerprocess exits cleanly when the guest reboots.
In the next lesson, we'll boot the same VM one more time - but instead of curl, we'll go through the official Go SDK. The wire-level interactions are identical to the ones you just sent by hand; the SDK simply replaces JSON strings with typed Go structs and adds a few conveniences on top.
- Previous lesson
- Run Your First Firecracker microVM
- Next lesson
- Run Your Third Firecracker microVM: Go SDK Way