Freestyle sandboxes are microVMs with a full Linux kernel, so the Docker Engine runs inside one natively — overlayfs storage, cgroup v2, and bridge networking all work, no vfs fallback. This guide bakes Docker and the Compose plugin into a snapshot, then boots VMs that run containers and docker compose stacks instantly.
Install the SDK
pnpm add freestylebun add freestylenpm install freestyleyarn add freestyle Set your API key before calling the API:
export FREESTYLE_API_KEY="your-api-key"
Build a Snapshot with Docker Installed
Create a VM, install Docker with the official convenience script — it pulls the Engine and the docker compose plugin — then start the daemon under systemd and wait until it answers. A Freestyle snapshot is a full memory and disk capture, so it preserves the running daemon, not just the installed files; VMs booted from this snapshot come up with Docker already active.
import { freestyle } from "freestyle";
const { vm: builder } = await freestyle.vms.create();
// get.docker.com installs the Engine + the Compose v2 plugin. It's a big install,
// so give it room.
await builder.exec({
command: "curl -fsSL https://get.docker.com | sh",
timeoutMs: 600_000,
});
// systemd is PID 1 in the VM, so let it supervise dockerd. Wait until the daemon
// answers before snapshotting.
await builder.exec("systemctl enable --now docker");
for (let i = 0; i < 15; i++) {
const r = await builder.exec("docker info >/dev/null 2>&1 && echo READY || echo WAIT");
if (r.stdout?.includes("READY")) break;
await builder.exec("sleep 2");
}
// Confirm the engine picked the native overlayfs driver (not vfs).
const info = await builder.exec("docker info --format '{{.Driver}} / cgroup {{.CgroupVersion}}'");
console.log(info.stdout?.trim()); // overlayfs / cgroup 2
const { snapshotId } = await builder.snapshot();
await builder.delete();
Run a Container
Boot a VM from the snapshot. Docker is already running, so vm.exec() can run containers immediately — no install, no systemctl start.
const { vm, vmId } = await freestyle.vms.create({ snapshotId, idleTimeoutSeconds: null });
const hello = await vm.exec("docker run --rm hello-world");
console.log(hello.stdout?.includes("Hello from Docker")); // true
// Any image works — this pulls alpine and runs a command in it.
const ok = await vm.exec("docker run --rm alpine echo 'container ok'");
console.log(ok.stdout?.trim()); // container ok
Run a Docker Compose Stack
Write a compose.yaml into the VM and bring it up. The Compose plugin came with the engine, so docker compose is available with no extra install. Published ports answer on the VM’s loopback.
await vm.exec("mkdir -p /root/app");
await vm.fs.writeTextFile(
"/root/app/compose.yaml",
`services:
web:
image: nginx:alpine
ports:
- "8080:80"
worker:
image: alpine
command: sh -c "while true; do echo 'worker tick'; sleep 5; done"
`,
);
await vm.exec("cd /root/app && docker compose up -d");
console.log((await vm.exec("cd /root/app && docker compose ps")).stdout);
// The web service's published port answers.
const res = await vm.exec("curl -s -o /dev/null -w '%{http_code}' localhost:8080");
console.log(res.stdout); // 200
Stream the Container Logs
vm.exec() buffers a command and only returns once it finishes, so it can’t show a container’s output as it happens. To watch the logs live, open a PTY on the VM — a real terminal streamed over a WebSocket (server-side only, Node 22+) — and follow the stack with docker compose logs -f (or docker logs -f <container> for a single one). onData delivers the bytes as they arrive:
const session = await vm.pty.open({
cols: 120,
rows: 30,
onData: (bytes) => process.stdout.write(bytes), // live log lines
});
// Follow every service in the stack; new lines stream in until you detach.
session.write("cd /root/app && docker compose logs -f\n");
// session.detach() drops your handle — the containers keep running.
Expose a Container on a Domain
A published container port behaves like any other service on the VM, so map a domain to it. The web service above publishes port 8080, so route the hostname there:
await freestyle.domains.mappings.create({
domain: "app.example.com",
vmId,
vmPort: 8080,
});
The domain must be verified first, and HTTPS is provisioned automatically.