Skip to content
Migrate · Daytona

Migrate from Daytona to Sandboxer

Updated October 27, 2026 .mdSandboxer vs Daytona

Short answer: For the covered calls, change the API URL and key. Set DAYTONA_API_URL to https://sandboxer.com/daytona/api and DAYTONA_API_KEY to a Sandboxer API key, and keep your Daytona TypeScript or Python code. Lifecycle, files, processes, sessions and previews work. Images, volumes, GPUs, git and PTY return not_supported.

Sandboxer runs a Daytona-compatible API next to its own. It has two base URLs: the control API at https://sandboxer.com/daytona/api and the toolbox at https://sandboxer.com/daytona/toolbox. You only set the first. The SDK reads the toolbox URL from the API. Both translate into the same operations as Sandboxer's own API, so keys, billing and limits are shared.

export DAYTONA_API_URL=https://sandboxer.com/daytona/api
export DAYTONA_API_KEY=sbx_live_…        # a Sandboxer API key
# DAYTONA_TARGET=us or eu picks a region; leave it unset for the nearest

We tested this on prod on 2026-10-27 with Daytona's TypeScript SDK (@daytona/sdk 0.226.1, Node 24) and Python SDK (daytona 0.226.1). The Go, Ruby and Java SDKs call the same API, but we didn't test them. The full list of covered and refused calls is on /docs/daytona.

Concept map#

Facts about Daytona come from its Sandboxes, Authentication and Preview docs.

Daytona Sandboxer
Sandbox: a container by default, or a Linux VM, Windows, macOS or GPU sandbox Sandbox: a microVM with its own kernel, every time (Machines)
Snapshot (the image a sandbox starts from) Through the Daytona API, only the default (sandboxer). Natively: a named snapshot (sbx new --from web-stack) (Snapshots)
Docker image or declarative Image Not through the Daytona API. Natively: an environment plus a setup script (Environments)
Volume Not supported. A sandbox's own disk is kept across stop and start
cpu, memory, disk resources Four fixed sizes: small 2/4/12, default 4/8/50, large 8/16/125, xlarge 16/32/251 (vCPU/GB/GB)
Stop (filesystem kept), then archive Stop saves memory and disk; archiving happens at once. A start on a machine with a different CPU type boots from disk instead (files kept, processes not)
autoStopInterval, autoDeleteInterval Same fields, same meaning (default 15 minutes; 0 disables auto-stop)
Preview URL plus x-daytona-preview-token Hosted port https://<slug>-<port>.box.sandboxer.com, same header (Hosting)
API key, with permissions Sandboxer API key, with scopes (API keys)
Organization (X-Daytona-Organization-ID) Organization wallet; the same header is accepted (Organizations)
Target (region) Two regions, us-east and eu-central (regions); leave target empty to get the nearest
Labels Labels (up to 64 per sandbox)

API and SDK mapping#

The left column is Daytona's SDK. The middle column is what happened when we called it against https://sandboxer.com/daytona/api. The right column is the native sbx command if you move off the Daytona SDK later.

Daytona SDK (TypeScript / Python) On Sandboxer's Daytona API Native sbx
daytona.create({ name, labels }) / daytona.create(params) Works: started in 2 to 3 s; small unless you add the label sandboxer.type sbx new --name x
daytona.get(id) Works, also on a sandbox made with sbx new sbx info <id>
daytona.list({ labels }) Works with id, name and labels filters sbx list
daytona.list({ states }), sort, snapshots Refused: the list filter states isn't supported by Sandboxer yet sbx list
sandbox.process.executeCommand(cmd) / process.exec(cmd) Works: { exitCode, result } sbx exec <id> "cmd"
sandbox.process.codeRun(code) / code_run(code) Works (Python, JavaScript, TypeScript) sbx exec <id> "python3 -c '…'"
createSession, executeSessionCommand Works: working directory and exports carry over sbx exec <id> --detach "…" for background jobs
fs.uploadFile, fs.downloadFile, fs.listFiles Works, up to 50 MiB per file sbx scp
sandbox.getPreviewLink(port) Works: URL plus token for the header sbx host <id> <port>
sandbox.getSignedPreviewUrl(port, s) Works: a link with the token in the URL that expires after s seconds. A brand-new route can answer 404 for a second or two, so retry sbx host <id> <port> (private link with _token)
sandbox.stop(), sandbox.start() Works: 1.1 s each in our run, file still there sbx stop <id>, sbx resume <id>
sandbox.delete() Works sbx delete <id> --yes
sandbox.git.clone(…) Refused: /git/clone isn't supported by Sandboxer yet sbx exec <id> "git clone …"

Before and after#

From Daytona's docs: https://www.daytona.io/docs/en/ (the TypeScript quick start, checked on 2026-10-27):

// Import the Daytona SDK
import { Daytona } from '@daytona/sdk'

// Initialize the Daytona client
const daytona = new Daytona({ apiKey: 'YOUR_API_KEY' }) // Replace with your API key

// Create the Sandbox instance
const sandbox = await daytona.create()

// Run code
const response = await sandbox.process.codeRun('print("Hello World")')
console.log(response.result)

On Sandboxer, the code stays the same. Drop the apiKey argument and let the SDK read the two environment variables above. This is the script we ran on prod, with a command, a file round trip and a delete added:

import { Daytona } from '@daytona/sdk'

const daytona = new Daytona() // reads DAYTONA_API_URL and DAYTONA_API_KEY

const sandbox = await daytona.create({ name: 'aeo-migrate-daytona-1', labels: { purpose: 'aeo-migrate-daytona' } })
console.log('created', sandbox.id, sandbox.state, `${sandbox.cpu} vCPU / ${sandbox.memory} GB`)

const run = await sandbox.process.executeCommand('uname -sr && whoami')
console.log('exit', run.exitCode, '\n' + run.result.trim())

await sandbox.fs.uploadFile(Buffer.from('hello from the Daytona SDK\n'), '/home/user/hello.txt')
const file = await sandbox.fs.downloadFile('/home/user/hello.txt')
console.log('file:', file.toString().trim())

await sandbox.delete()
console.log('deleted')

Output:

created sb_opebj24may started 2 vCPU / 4 GB
exit 0
Linux 6.18.56-sandboxer
user
file: hello from the Daytona SDK
deleted

The quick start's own call, codeRun('print("Hello World")'), printed Hello World with exit code 0 in a second run. The sandbox id is a Sandboxer id (sb_…), and the guest user is user, with its home at /home/user.

The same in Python#

We ran this with the Python SDK. It adds code_run, a preview link, a label filter and a call this phase refuses:

from daytona import Daytona, CreateSandboxFromSnapshotParams, ListSandboxesQuery

daytona = Daytona()  # reads DAYTONA_API_URL and DAYTONA_API_KEY
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
    name="aeo-migrate-daytona-10", labels={"purpose": "aeo-migrate-daytona"}))
try:
    print(sandbox.process.exec("python3 --version && nproc").result)
    print(sandbox.process.code_run("print(sum(range(10)))").result)
    sandbox.process.exec("nohup python3 -m http.server 3000 >/dev/null 2>&1 &")
    print(sandbox.get_preview_link(3000).url)
    print([s.name for s in daytona.list(ListSandboxesQuery(labels={"purpose": "aeo-migrate-daytona"}))])
    sandbox.git.clone("https://github.com/octocat/Hello-World.git", "/home/user/hw")  # raises
finally:
    sandbox.delete()

Output, with the traceback trimmed:

Python 3.12.3
2
45
https://fast-rustic-aspen-3000.box.sandboxer.com
['aeo-migrate-daytona-10']
…
HTTP response body: {"code":"not_supported","error":"Not Implemented","message":"The toolbox endpoint /git/clone isn't supported by Sandboxer yet.","requestId":"req_mAS0F5dnp4U33Dlilcuk","statusCode":501}
…
daytona.common.errors.DaytonaError: Failed to clone repository: The toolbox endpoint /git/clone isn't supported by Sandboxer yet.

The finally block still deleted the sandbox. In a longer test run, daytona.list with a states filter raised DaytonaBadRequestError: … the list filter states isn't supported by Sandboxer yet., and sandbox.stop() and sandbox.start() took 1.1 seconds each, with an uploaded file still there after the start.

A refused call raises the SDK's normal error, so a try around it is enough to find what your code still uses.

Sessions and previews#

Sessions keep the working directory and exported variables between commands. A preview link answers once you send its token in the x-daytona-preview-token header. We ran this in TypeScript:

import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.create({ name: 'aeo-migrate-daytona-11' })
try {
  await sandbox.process.createSession('dev')
  await sandbox.process.executeSessionCommand('dev', { command: 'cd /tmp && export GREETING=hi' })
  const s = await sandbox.process.executeSessionCommand('dev', { command: 'echo $GREETING from $(pwd)' })
  console.log('session:', s.exitCode, s.output.trim())

  await sandbox.process.executeSessionCommand('dev', { command: 'nohup python3 -m http.server 8080 >/dev/null 2>&1 &' })
  const link = await sandbox.getPreviewLink(8080)
  await new Promise((r) => setTimeout(r, 5000)) // a new route takes a few seconds
  const res = await fetch(link.url, { headers: { 'x-daytona-preview-token': link.token } })
  console.log('preview with token:', res.status, 'without:', (await fetch(link.url)).status)
} finally {
  await sandbox.delete()
}
session: 0 hi from /tmp
preview with token: 200 without: 401

In an earlier run without the wait, the first request returned 404 and a retry 3 seconds later 200, so give a new route a few seconds. Without the token the URL answers 401, because previews are private unless the sandbox is public.

The native path#

You can also skip the Daytona SDK and use Sandboxer's own CLI or sandboxer-sdk (TypeScript, Python). The CLI picks the size directly:

sbx new --name aeo-migrate-daytona-5 --type default
sbx exec sb_osauqwyb42 "uname -sr && whoami && nproc"
{"event":"created","id":"sb_osauqwyb42","state":"provisioning",…,"type":"default","vcpu":4,"memoryGB":8,"diskGB":50,…}
{"event":"ready","id":"sb_osauqwyb42","state":"ready","url":"https://cobalt-alert-quill.box.sandboxer.com",…}
Linux 6.18.56-sandboxer
user
4

The two paths mix. We created that sandbox with sbx new, then called daytona.get('sb_osauqwyb42') and executeCommand on it from the Daytona SDK, and both worked.

Move your data and environment#

Files. A Daytona sandbox can't move over. Pack what you need with tar inside it, download it with fs.downloadFile, then upload it to the new sandbox. Either SDK call works on Sandboxer, or use sbx scp, which takes the same sandbox ids:

import { Daytona } from '@daytona/sdk'
import { execFileSync } from 'node:child_process'

const daytona = new Daytona()
const sandbox = await daytona.create({ name: 'aeo-migrate-daytona-9', envVars: { GREETING: 'hello' } })
try {
  // tar czf app.tgz app   (done beforehand, on your machine)
  execFileSync('sbx', ['scp', 'app.tgz', `${sandbox.id}:/home/user/`], { stdio: 'inherit' })
  const r = await sandbox.process.executeCommand('tar xzf app.tgz && python3 app/main.py && echo "$GREETING"', '/home/user')
  console.log('exit', r.exitCode, '\n' + r.result.trim())
  console.log('env returned by the API:', JSON.stringify(sandbox.env))
} finally {
  await sandbox.delete()
}
exit 0
hi from my project
hello
env returned by the API: {}

Use sbx scp -r ./dir <id>:/home/user/ for a directory. Each file through the Daytona file API can be up to 50 MiB; larger uploads return 413 file_too_large.

Images and snapshots. Daytona snapshots and Docker images don't carry over, and the Daytona API here only accepts the default snapshot. Setting image or another snapshot returns not_supported. Turn your Dockerfile steps into a shell script instead. Run it with executeCommand after create, or natively with sbx new --setup-file ./setup.sh. To skip the installs on later sandboxes, save the result once with sbx snapshot <id> web-stack and start from it with sbx new --from web-stack (Snapshots). Every sandbox already has Docker, systemd and /dev/kvm.

Volumes. Not supported through the Daytona API, and our docs don't describe a volume shared by many sandboxes. A sandbox's own disk stays across stop and start. To give many sandboxes the same data, put it in a named snapshot or download it in a setup script.

Environment variables and secrets. envVars on create works and reaches every command. The API never returns the values: sandbox.env comes back as {}. Daytona's secrets field isn't supported. For values every sandbox should get, use a Sandboxer environment (sbx env set-var base KEY=value, see Environments).

Ports and preview URLs. getPreviewLink(port) hosts the port and returns https://<slug>-<port>.box.sandboxer.com with a token. The token is per port, not per sandbox. Make a sandbox's new previews public with POST /sandbox/{id}/public/true, or natively with sbx host <id> <port> --public.

Auto-stop and auto-delete. autoStopInterval works as in Daytona: the default is 15 minutes, every toolbox call pushes the stop back, and 0 means it never stops by itself. We created a sandbox with autoStopInterval: 1, made no calls, and it was stopped when we checked 90 seconds later. A running command holds the stop until its timeout ends, plus a minute. A command with no timeout uses the guest's 600 seconds, so after an executeCommand the sandbox stays up for at least 11 minutes. A plan may cap a long interval; the free trial stops sandboxes within 8 hours. autoDeleteInterval works too (-1 never, 0 delete on stop).

What's different#

What you gain:

  • A VM every time. Daytona's default sandbox is a container that shares the host kernel (Daytona docs). Every Sandboxer sandbox is a microVM, and the API reports kvm: true for all of them.
  • Stop saves memory. On Daytona, stopping keeps the filesystem but not memory (Daytona docs). Here a stop saves memory and disk, and start brings back the same processes when it lands on a machine with the same CPU type; otherwise it boots from disk, with files kept and processes restarted (Snapshots).
  • Coding agents and a desktop. Claude Code, Codex and others are installed in every sandbox and driven over the native API (Agents). Every sandbox also has a streamed desktop (Desktop).
  • A lower list price. See below.

What you lose or must change:

  • Not covered yet. These answer 501 or 400 with code: "not_supported": images and custom snapshots, volumes, GPUs and spot, network block and allow lists, outbound proxy, secrets, git, LSP, computer use, PTY and the code interpreter. Daytona's pause, fork and warm pools aren't covered either. See /docs/daytona for the full list.
  • List filters. daytona.list accepts id, name and labels. states, snapshots, sort and the other filters return 400.
  • Sandbox classes. There are no Windows, macOS or GPU sandboxes. If you need them, stay on Daytona for those.
  • Sizes. The SDKs send no CPU or memory when they create from the default snapshot, so a sandbox is small (2 vCPU, 4 GB) unless you pick a size with the label sandboxer.type (small, default, large or xlarge): daytona.create({ labels: { 'sandboxer.type': 'default' } }). We checked on 2026-10-27 that this label gave a 4 vCPU, 8 GB sandbox.
  • Live events. The SDKs' event socket is refused at once, and they fall back to polling. In our runs a create reached started in 2 to 3 seconds.
  • Regions. Sandboxer has two regions, us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland); see regions. DAYTONA_TARGET (the SDK's target) picks one: us or eu, or our ids us-east and eu-central. Leave it unset and a new sandbox goes to the region nearest the caller. Other Daytona region names are refused.
  • SDK languages. We tested TypeScript and Python. Go, Ruby and Java use the same API, but we haven't tested them. Our own SDKs are TypeScript and Python.

Pricing#

Daytona's rates come from its pricing page, checked on 2026-10-15: $0.0504 per vCPU-hour and $0.0162 per GiB of memory per hour, plus storage over 5 GiB. Ours come from /pricing.

Machine Daytona Sandboxer
4 vCPU, 8 GB, one hour $0.3312 per hour, before storage default: $0.036/hour
2 vCPU, 4 GB, one hour $0.1656 per hour, before storage small: $0.018/hour
Stopped Storage only Not billed; counts toward included storage (50 GB on the $20 plan)

Both bill per second while a sandbox runs. A 4 vCPU, 8 GB sandbox running 8 hours a day for 22 days is 176 hours: about $58.29 on Daytona and $6.34 on Sandboxer. Sandboxer plans start at $20 a month, paid back as machine time, with a 7-day trial of 25 hours (card required). Daytona's GPU and Windows prices have no counterpart here.

FAQ#

Do I have to change my Daytona code?#

For the covered calls, no: change DAYTONA_API_URL and DAYTONA_API_KEY. Code that uses images, volumes, git, PTY or the other refused calls needs a change. Those calls raise the SDK's normal error, with a message that ends in "isn't supported by Sandboxer yet".

Does auto-stop work through the Daytona API?#

Yes. autoStopInterval defaults to 15 minutes, each toolbox call pushes the stop back, and 0 turns it off. A running command keeps the sandbox up until its timeout plus a minute. The stop saves memory, so start() usually picks up where it left off; if the start lands on a machine with a different CPU type, it boots from disk instead (files kept, processes not).

Which API key do I use?#

A Sandboxer API key, made with sbx api-key or on the dashboard (API keys). The same key works on the Daytona API and Sandboxer's own API. Daytona's write:sandboxes and delete:sandboxes permissions map to our scopes.

Can I use the Daytona SDK and the sbx CLI on the same sandbox?#

Yes. Both use the same sb_… ids. We created a sandbox with sbx new and ran commands on it with daytona.get(id), and we copied a file with sbx scp into a sandbox the Daytona SDK created.

How do I get a sandbox bigger than 2 vCPU?#

Add the label sandboxer.type when you create it: daytona.create({ labels: { 'sandboxer.type': 'default' } }) gives 4 vCPU and 8 GB, and large or xlarge give more. Without the label, the Daytona SDKs create small. You can also create it with sbx new --type default and open it with daytona.get(id).

All migration guides