Worked examples for common sandbox patterns: persistent storage, GPUs, exposed ports, and coding agents.
Each example builds on the quickstart and assumes you already have an initialised apiClient and a project ID.
Create a sandbox with persistent storage
Ephemeral storage is wiped whenever a sandbox restarts or pauses. Attach a volume when you need files to survive.
Create the volume first, then attach it as the service is created. Volumes are attached at creation time through createOptions.volumesToAttach.
const storageSandboxId = `sandbox-${crypto.randomUUID().split('-')[4]}`;
const volumeName = `data-${storageSandboxId}`;
await apiClient.create.volume({
parameters: {
projectId: 'your-project-id',
},
data: {
name: volumeName,
mounts: [
{
containerMountPath: '/workspace',
},
],
spec: {
accessMode: 'ReadWriteMany',
storageClassName: 'nf-multi-rw',
storageSize: 10240, // 10 GiB
},
},
});
await apiClient.create.service.deployment({
parameters: {
projectId: 'your-project-id',
},
data: {
name: storageSandboxId,
billing: {
deploymentPlan: 'nf-compute-200',
},
deployment: {
instances: 1,
docker: {
configType: 'customCommand',
customCommand: 'sleep infinity',
},
external: {
imagePath: 'ubuntu:22.04',
},
storage: {
ephemeralStorage: {
storageSize: 2048,
},
},
},
createOptions: {
volumesToAttach: [volumeName],
},
},
});
import uuid
storage_sandbox_id = f"sandbox-{uuid.uuid4().hex[:8]}"
volume_name = f"data-{storage_sandbox_id}"
volume = client.create.volume(
project_id="your-project-id",
data={
"name": volume_name,
"mounts": [{"containerMountPath": "/workspace"}],
"spec": {
"accessMode": "ReadWriteMany",
"storageClassName": "nf-multi-rw",
"storageSize": 10240, # 10 GiB
},
},
)
# Read the id back rather than assuming it matches the name you asked
# for. Use this id to attach, detach, or delete the volume.
volume_id = (volume.data or {}).get("id") or volume_name
client.create.service.deployment(
project_id="your-project-id",
data={
"name": storage_sandbox_id,
"billing": {"deploymentPlan": "nf-compute-200"},
"deployment": {
"instances": 1,
"docker": {
"configType": "customEntrypointCustomCommand",
"customEntrypoint": "/bin/bash",
"customCommand": "-c 'sleep infinity'",
},
"external": {"imagePath": "ubuntu:22.04"},
"storage": {"ephemeralStorage": {"storageSize": 2048}},
},
"createOptions": {"volumesToAttach": [volume_id]},
},
)
Files written to /workspace now survive pause and resume. A volume is billed for as long as it exists, including while the sandbox is paused.
See add a volume for access modes and storage classes.
Create a GPU sandbox
Run compute-intensive workloads with GPU acceleration. The project must be in a GPU-enabled region.
On Northflank cloud, GPU sandboxes are isolated with gVisor rather than the microVMs used for CPU sandboxes. You do not configure this.
const sandboxId = `jupyter-${crypto.randomUUID().split('-')[4]}`;
await apiClient.create.service.deployment({
parameters: {
projectId: 'your-project-id', // must be in a GPU-enabled region
},
data: {
name: sandboxId,
billing: {
deploymentPlan: 'nf-gpu-a100-80-1g', // 1x A100 80GB GPU
},
deployment: {
instances: 1,
external: {
imagePath: 'quay.io/jupyter/pytorch-notebook:cuda12-2026-02-09',
},
docker: {
configType: 'default',
},
gpu: {
enabled: true,
configuration: {
// Must match the GPU in the deployment plan above.
gpuType: 'a100-80',
gpuCount: 1,
// true shares one GPU across workloads; false is dedicated.
timesliced: false,
},
},
storage: {
ephemeralStorage: {
storageSize: 256000,
},
// Shared memory. Size generously: PyTorch dataloaders fail with
// confusing errors when this is too small.
shmSize: 174080,
},
},
ports: [
{
name: 'app',
internalPort: 8888,
public: true,
protocol: 'HTTP',
},
],
},
});
import uuid
sandbox_id = f"jupyter-{uuid.uuid4().hex[:8]}"
client.create.service.deployment(
project_id="your-project-id", # must be in a GPU-enabled region
data={
"name": sandbox_id,
"billing": {"deploymentPlan": "nf-gpu-a100-80-1g"},
"deployment": {
"instances": 1,
"external": {
"imagePath": "quay.io/jupyter/pytorch-notebook:cuda12-2026-02-09"
},
"docker": {"configType": "default"},
"gpu": {
"enabled": True,
"configuration": {
# Must match the GPU in the deployment plan above.
"gpuType": "a100-80",
"gpuCount": 1,
# True shares one GPU across workloads; False is dedicated.
"timesliced": False,
},
},
"storage": {
"ephemeralStorage": {"storageSize": 256000},
# Shared memory. Size generously: PyTorch dataloaders fail
# with confusing errors when this is too small.
"shmSize": 174080,
},
},
"ports": [
{
"name": "app",
"internalPort": 8888,
"public": True,
"protocol": "HTTP",
}
],
},
)
See GPU workloads for images, drivers, and optimisation.
GPU sandbox with persistent storage
Model weights, datasets, and notebooks are expensive to re-download. Attach a volume so they survive restarts.
Mount the volume at every path the image writes to. For the Jupyter PyTorch image that means the notebook home directory as well as your working directory.
const gpuStorageSandboxId = `training-${crypto.randomUUID().split('-')[4]}`;
const volumeName = `data-${gpuStorageSandboxId}`;
await apiClient.create.volume({
parameters: {
projectId: 'your-project-id',
},
data: {
name: volumeName,
spec: {
accessMode: 'ReadWriteMany',
storageClassName: 'nf-multi-rw',
storageSize: 102400, // 100 GiB
},
mounts: [
{ containerMountPath: '/home/jovyan' },
{ containerMountPath: '/workspace' },
],
},
});
await apiClient.create.service.deployment({
parameters: {
projectId: 'your-project-id',
},
data: {
name: gpuStorageSandboxId,
billing: {
deploymentPlan: 'nf-gpu-a100-80-1g',
},
deployment: {
instances: 1,
external: {
imagePath: 'quay.io/jupyter/pytorch-notebook:cuda12-2026-02-09',
},
docker: {
configType: 'default',
},
gpu: {
enabled: true,
configuration: {
gpuType: 'a100-80',
gpuCount: 1,
timesliced: false,
},
},
storage: {
ephemeralStorage: {
storageSize: 256000,
},
shmSize: 174080,
},
},
ports: [
{
name: 'app',
internalPort: 8888,
public: true,
protocol: 'HTTP',
},
],
createOptions: {
volumesToAttach: [volumeName],
},
},
});
import uuid
gpu_storage_sandbox_id = f"training-{uuid.uuid4().hex[:8]}"
volume_name = f"data-{gpu_storage_sandbox_id}"
volume = client.create.volume(
project_id="your-project-id",
data={
"name": volume_name,
"spec": {
"accessMode": "ReadWriteMany",
"storageClassName": "nf-multi-rw",
"storageSize": 102400, # 100 GiB
},
"mounts": [
{"containerMountPath": "/home/jovyan"},
{"containerMountPath": "/workspace"},
],
},
)
volume_id = (volume.data or {}).get("id") or volume_name
client.create.service.deployment(
project_id="your-project-id",
data={
"name": gpu_storage_sandbox_id,
"billing": {"deploymentPlan": "nf-gpu-a100-80-1g"},
"deployment": {
"instances": 1,
"external": {
"imagePath": "quay.io/jupyter/pytorch-notebook:cuda12-2026-02-09"
},
"docker": {"configType": "default"},
"gpu": {
"enabled": True,
"configuration": {
"gpuType": "a100-80",
"gpuCount": 1,
"timesliced": False,
},
},
"storage": {
"ephemeralStorage": {"storageSize": 256000},
"shmSize": 174080,
},
},
"ports": [
{
"name": "app",
"internalPort": 8888,
"public": True,
"protocol": "HTTP",
}
],
"createOptions": {"volumesToAttach": [volume_id]},
},
)
You can now pause the sandbox between training runs to stop GPU charges, and resume with your weights and notebooks still in place. The volume is billed while the sandbox is paused.
Expose a web server
If your sandbox runs a web server or any other network service, expose a port to make it reachable. Northflank provisions a public DNS name automatically for publicly exposed ports.
// Replaces the whole port list rather than appending, so include
// every port you want to keep.
await apiClient.update.service.ports({
parameters: {
projectId: 'your-project-id',
serviceId: sandboxId,
},
data: {
ports: [
{
name: 'http',
internalPort: 8080,
public: true,
protocol: 'HTTP',
},
],
},
});
// Retrieve the public DNS
const ports = await apiClient.get.service.ports({
parameters: {
projectId: 'your-project-id',
serviceId: sandboxId,
},
});
const publicUrl = ports.data.ports.find((p) => p.internalPort === 8080)?.dns;
console.log('Public URL:', publicUrl);
# Replaces the whole port list rather than appending, so include
# every port you want to keep.
client.update.service.ports(
project_id="your-project-id",
service_id=sandbox_id,
data={
"ports": [
{
"name": "http",
"internalPort": 8080,
"public": True,
"protocol": "HTTP",
}
]
},
)
svc = client.get.service(project_id="your-project-id", service_id=sandbox_id)
for port in (svc.data or {}).get("ports") or []:
print(port["name"], port["internalPort"], port.get("dns") or "pending DNS")
You can also expose a port at creation time by passing the same ports list in the create payload, which avoids a second call.
Set public: false if the port should only be reachable by other services in the same project. The protocol field accepts HTTP, HTTP/2, TCP, and UDP.
You can also attach your own domain to a publicly exposed port. See add a domain.
Run a coding agent in a sandbox
A sandbox makes a good runtime for a coding agent: the agent can install packages, run builds, and execute whatever it writes, without any of it touching your machine.
Pass the agent's credentials as runtime variables rather than baking them into the image.
await apiClient.create.service.deployment({
parameters: {
projectId: 'your-project-id',
},
data: {
name: `claude-agent-${crypto.randomUUID().split('-')[4]}`,
billing: {
deploymentPlan: 'nf-compute-200',
},
deployment: {
instances: 1,
external: {
imagePath: 'ubuntu:22.04',
},
docker: {
configType: 'customCommand',
customCommand: 'sleep infinity',
},
storage: {
ephemeralStorage: {
storageSize: 2048,
},
},
},
runtimeEnvironment: {
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
},
},
});
import os
import uuid
sandbox_id = f"claude-agent-{uuid.uuid4().hex[:8]}"
client.create.service.deployment(
project_id="your-project-id",
data={
"name": sandbox_id,
"billing": {"deploymentPlan": "nf-compute-200"},
"deployment": {
"instances": 1,
"external": {"imagePath": "ubuntu:22.04"},
"docker": {
"configType": "customEntrypointCustomCommand",
"customEntrypoint": "/bin/bash",
"customCommand": "-c 'sleep infinity'",
},
"storage": {"ephemeralStorage": {"storageSize": 2048}},
},
"runtimeEnvironment": {
"ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"],
},
},
)
Drive the agent with exec sessions, and attach a volume if its work should survive a restart.
For a managed alternative that handles agent authentication, repository connection, and local SSH access for you, see Cloud Harness.
Store real credentials in a secret group rather than reading them from your shell environment.