Sandcastle Documentation

Everything you need to go from “AI wrote some code” to a live website on your own server.

What is Sandcastle?

Sandcastle is a small program you install on a Linux server or home device. Once it's running, you can take any code — a website, a small app, a tool — and turn it into a real, working website with its own address like my-app.yourname.com.

It's designed for people who use AI tools (ChatGPT, Claude, Cursor, etc.) to generate code and want a simple way to put that code online. You don't need to understand programming or servers — Sandcastle figures out what the AI made and handles the rest.

What it does:

Three parts:

What you need

You need two things to get started:

  1. A Linux server or computer — Ubuntu 22.04 or 24.04 (x86_64). A cheap VPS works great — a Hetzner CX22 (~$4/month) is more than enough. A home server, old laptop, or Raspberry Pi (with x86) also works.
  2. A domain name you control — something like sandcastle.example.com. You can buy one from Namecheap, Cloudflare, or any registrar.

DNS setup

Point your domain and a wildcard subdomain at your server's IP address. Go to your domain registrar's DNS settings and add two A records:

DNS Records
A     sandcastle.example.com       → your-server-ip
A     *.sandcastle.example.com     → your-server-ip
Important: DNS must point directly to the server — not through Cloudflare's proxy (orange cloud). Caddy needs direct access to provision TLS certificates.

Setting up the server

The installer sets up everything on your server over SSH — Docker, gVisor (the security sandbox), Caddy (for HTTPS), and the Sandcastle server itself.

Step 1: Build the installer and server

On your local machine (the one you're working from):

Terminal
make installer
make deploy

Step 2: Run the installer

Terminal
sandcastle-installer ssh \
  --host 1.2.3.4 \
  --domain sandcastle.example.com \
  --binary ./bin/sandcastle-server-linux
Installer flags
FlagDefaultDescription
--hostServer IP address (required)
--domainYour domain (required)
--binaryPath to server binary (required)
--userrootSSH user
--keySSH private key path
--passwordSSH password (if no key)
--port22SSH port
--admin-keyauto-generatedAdmin API key
--max-deploys10Max deployments per user
--default-ttl72Default TTL in hours
What the installer does (step by step)
  1. Verifies root access
  2. Installs system packages (ca-certificates, curl, jq, sqlite3, dnsutils)
  3. Installs Docker CE
  4. Installs gVisor (runsc) and configures Docker to use it
  5. Tests gVisor runtime
  6. Installs Caddy (for automatic HTTPS)
  7. Pulls base Docker images (node:20-slim, caddy:2-alpine)
  8. Creates data directories (/var/lib/sandcastle/)
  9. Creates shared Docker volumes (npm cache)
  10. Writes server config (/var/lib/sandcastle/config.json)
  11. Initializes SQLite database with admin user
  12. Writes Caddyfile with Let's Encrypt config
  13. Uploads server binary to /usr/local/bin/sandcastle-server
  14. Writes sandcastle-admin helper script
  15. Writes systemd service unit
  16. Enables and starts Caddy + Sandcastle services
  17. Configures UFW firewall (ports 22, 80, 443)
  18. Verifies DNS resolution
  19. Runs health check
  20. Prints summary with your admin API key

Installing the CLI

The CLI is the command you run on your laptop to deploy code. Build it from source:

Terminal
make cli
sudo mv bin/sandcastle /usr/local/bin/

Then tell it where your server is:

Terminal
sandcastle config set server https://sandcastle.example.com
sandcastle config set key sc_your_admin_key_here

You can also log in via the browser instead:

Terminal
sandcastle login

Your first deploy

Once the CLI is configured, navigate to a project directory. First, set up your project:

Terminal
$ sandcastle init
  🏰 Sandcastle Project Setup
  Detected: Next.js
  ✓ Saved .sandcastle.json

This walks you through naming your deployment, setting a TTL, memory limit, and access protection. Then deploy:

Terminal
$ sandcastle deploy
  Uploading 47 files
  Building...

  ✓ Live at https://my-app.sandcastle.example.com

That's it. Open the link and your app is running. You can skip init and go straight to deploy — it works fine with defaults.

You can also deploy a single file directly:

Terminal
sandcastle deploy App.jsx          # Single React file
sandcastle deploy index.html       # Static HTML file
sandcastle deploy app.py            # Python file

Single .jsx, .tsx, .vue, .svelte, and .html files are auto-scaffolded with Vite + Tailwind CSS.

Use cases

People use Sandcastle for all kinds of things:

The common pattern: tell an AI tool what you want, it writes the code, and Sandcastle makes it a real website you can share.

Sharing & access control

By default, every deployment is public — anyone with the link can see it. You can add protection:

Password protection (basic auth)

Terminal
sandcastle protect abc123 --mode basic_auth --username admin --password secret

Visitors see a browser login prompt.

API key protection

Terminal
sandcastle protect abc123 --mode api_key

Access requires an API key in the header or query string.

User-based protection

Terminal
sandcastle protect abc123 --mode user

Only logged-in Sandcastle users can access the deployment. Optionally restrict to specific users:

Terminal
sandcastle protect abc123 --mode user --users alice,bob

The deployment owner and admins always have access regardless of the user list.

Remove protection

Terminal
sandcastle protect abc123 --mode public

CLI Reference

Terminal
sandcastle <command> [options]

Commands

CommandDescription
initConfigure project for Sandcastle (creates .sandcastle.json)
deploy [file|dir]Deploy a file, directory, or current directory
dev [dir]Start sandbox with live file sync (hot reload)
update <id>Push code update to a running deployment
promote <id>Promote a sandbox to a production deployment
listList your deployments
status <id>Get deployment details
logs <id>View container logs
protect <id>Set access control (basic_auth, api_key, user, public)
pull <id>Download deployment source code to local directory
destroy <id>Remove a deployment
destroy --allRemove all your deployments
loginLogin via browser (sets up API key automatically)
configManage CLI configuration

Init options

FlagDefaultDescription
--yesAccept all defaults (non-interactive)
--dir <path>current dirProject directory

Running sandcastle init creates a .sandcastle.json file in your project with deployment name, TTL, memory limit, port, and access protection settings. These are used automatically by sandcastle deploy and sandcastle dev.

Deploy options

FlagDefaultDescription
--name <name>directory nameCustom subdomain name
--ttl <duration>72hTime to live: 24h, 7d, 0 for never
--env <KEY=VAL>Environment variable (repeatable)
--memory <limit>512mMemory limit
--cpus <limit>0.5CPU limit
--dir <path>current dirProject directory
--port <port>auto-detectPort your app listens on
--env-file <path>Load env vars from file (also auto-loads .env)
--protect <mode>Access control: basic_auth, api_key, user
--users <user,...>Restrict access to specific users (for user mode)
--network-quota <q>Network transfer quota (e.g. 1g, 5g, 10g)

Dev options

FlagDefaultDescription
--attach <id>Reattach file watcher to existing sandbox
--name <name>directory nameCustom subdomain name
--ttl <duration>4hTime to live
--env <KEY=VAL>Environment variable (repeatable)
--memory <limit>1gMemory limit
--port <port>auto-detectPort override
--protect <mode>Access control: basic_auth, api_key, user
--users <user,...>Restrict access to specific users (for user mode)
--network-quota <q>Network transfer quota (e.g. 500m, 1g)

Examples

Terminal
sandcastle init                          # Configure project interactively
sandcastle init --yes                    # Non-interactive with defaults
sandcastle deploy                        # Deploy current directory
sandcastle deploy App.jsx                # Single-file React deploy
sandcastle dev                           # Dev sandbox with file watcher
sandcastle dev App.jsx                   # Single-file dev with hot reload
sandcastle dev --attach abc123           # Reattach to existing sandbox
sandcastle deploy App.jsx --name cool    # Custom subdomain
sandcastle update abc123def              # Push code update
sandcastle pull abc123def                # Download deployment source
sandcastle pull abc123def --output ./bak # Download to specific directory
sandcastle logs abc123def --tail 50      # View last 50 log lines
sandcastle config set server https://sandcastle.example.com
sandcastle config set key sc_your_api_key_here

MCP Setup

MCP (Model Context Protocol) lets AI tools deploy code automatically. Sandcastle has two MCP implementations:

Claude Desktop

Build the MCP binary, then add it to your Claude Desktop config:

Terminal
make mcp

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

claude_desktop_config.json
{
  "mcpServers": {
    "sandcastle": {
      "command": "/usr/local/bin/sandcastle-mcp",
      "env": {
        "SANDCASTLE_SERVER": "https://sandcastle.example.com",
        "SANDCASTLE_KEY": "sc_your_api_key_here"
      }
    }
  }
}

Cursor

Add to .cursor/mcp.json in your project or global config:

mcp.json
{
  "mcpServers": {
    "sandcastle": {
      "command": "/usr/local/bin/sandcastle-mcp",
      "env": {
        "SANDCASTLE_SERVER": "https://sandcastle.example.com",
        "SANDCASTLE_KEY": "sc_your_api_key_here"
      }
    }
  }
}

Claude.ai (server-side MCP)

The server exposes an MCP endpoint at /mcp using Streamable HTTP transport with OAuth 2.1 authentication. Claude.ai can connect directly — no separate binary needed. Point Claude.ai at https://sandcastle.example.com/mcp.

MCP tools

Both MCP implementations expose these tools. The standalone MCP also adds sandcastle_deploy_dir and sandcastle_update_dir for deploying local directories.

ToolDescription
sandcastle_deployDeploy inline files to a live HTTPS URL
sandcastle_deploy_dirDeploy a local directory (standalone only)
sandcastle_updateUpdate a deployment with new inline files
sandcastle_update_dirUpdate from a local directory (standalone only)
sandcastle_statusCheck deployment status and URL
sandcastle_logsGet container logs
sandcastle_listList all active deployments
sandcastle_sourceGet the source code files of a deployment
sandcastle_destroyDestroy a deployment
sandcastle_protectSet access control on a deployment
sandcastle_sandbox_createCreate a dev sandbox with hot reload
sandcastle_sandbox_pushPush file changes to a sandbox (instant)
sandcastle_sandbox_installInstall/remove packages in a sandbox
sandcastle_sandbox_execRun a shell command in a sandbox
sandcastle_sandbox_logsGet sandbox logs
sandcastle_sandbox_destroyDestroy a sandbox
sandcastle_sandbox_promotePromote a sandbox to production
sandcastle_update_quotaUpdate network transfer quota

Supported Languages

Sandcastle auto-detects the language and framework from your project files. Detection order: Go → Rust → Python → Node.js → Static HTML (first match wins).

Node.js

Detection: package.json exists with a start script or a recognized framework dependency.

Default version: Node 20 (override with .nvmrc, .node-version, or engines.node in package.json).

Recognized frameworks:

FrameworkDetectionDefault Port
Next.jsnext in dependencies3000
Nuxtnuxt in dependencies3000
SvelteKit@sveltejs/kit in dependencies3000
Vitevite in dependencies4173
Create React Appreact-scripts in dependencies3000
Vue CLI@vue/cli-service in dependencies8080
Angular@angular/cli or @angular/core in dependencies4200
Generic Nodestart script in package.json3000

Package managers: npm, yarn, and pnpm are supported. Sandcastle detects the lock file and uses the correct installer.

Python

Detection: Any of these files: requirements.txt, pyproject.toml, Pipfile, setup.py, setup.cfg, app.py, main.py, manage.py.

Default version: Python 3.12 (override with .python-version, requires-python in pyproject.toml, or runtime.txt).

Recognized frameworks:

FrameworkDetectionDefault Port
Djangomanage.py exists8000
FastAPIfastapi in requirements/pyproject8000
Flaskflask in requirements/pyproject8000
Generic Pythonapp.py or main.py exists8000

Go

Detection: go.mod exists.

Default version: Go 1.22 (reads the go directive from go.mod).

Build: CGO_ENABLED=0 go build produces a static binary. Runtime uses a minimal debian:bookworm-slim image.

Default port: 8080. Use the PORT environment variable in your code.

Rust

Detection: Cargo.toml exists.

Default version: latest stable (override with rust-toolchain.toml, rust-toolchain, or rust-version in Cargo.toml).

Build: cargo build --release. Runtime uses a minimal debian:bookworm-slim image.

Default port: 8080. Use the PORT environment variable in your code.

Static HTML

Detection: index.html exists (and no other language matched first).

Served directly by Caddy (Alpine). No build step. Supports the /_data/* REST API for storing data without a backend.

Sandbox / Dev Mode

Sandboxes are live development environments with hot reload. Changes apply instantly without a full rebuild — much faster for iterating.

CLI: sandcastle dev

Terminal
sandcastle dev              # Start dev sandbox with file watcher
sandcastle dev App.jsx      # Single-file dev with hot reload
sandcastle dev --attach id  # Reattach to existing sandbox

The CLI watches your local files and pushes changes to the sandbox as you edit. For Node/Vite projects, changes appear via HMR (Hot Module Replacement). For Go and Rust, the sandbox automatically rebuilds the binary.

MCP: sandbox tools

AI tools use these MCP tools for the same workflow:

  1. sandcastle_sandbox_create — create the sandbox with initial files
  2. sandcastle_sandbox_push — push file changes (instant, no rebuild)
  3. sandcastle_sandbox_install — add or remove packages
  4. sandcastle_sandbox_exec — run commands inside the container
  5. sandcastle_sandbox_logs — check logs for errors
  6. sandcastle_sandbox_promote — ship it to production when ready

HTTP API

EndpointDescription
POST /api/sandboxCreate a sandbox (JSON body with files)
POST /api/sandbox/{id}/pushPush file changes
POST /api/sandbox/{id}/installInstall packages
POST /api/sandbox/{id}/execRun a command
GET /api/sandbox/{id}/logsGet logs
DELETE /api/sandbox/{id}Destroy sandbox
POST /api/sandbox/{id}/promotePromote to production

Promote to production

When you're happy with your sandbox, promote it. This stops the dev server, runs a full optimized build, and starts a production runtime. The URL stays the same.

Terminal
sandcastle promote abc123

Persistent Data

Deployments get two ways to persist data across rebuilds:

1. /data directory (backend apps)

Every dynamic container (Node.js, Python, Go, Rust) has a persistent /data directory. Use the DATA_DIR environment variable to reference it.

Node.js
const path = require('path');
const dbPath = path.join(process.env.DATA_DIR, 'app.db');
Python
import os
db_path = os.path.join(os.environ["DATA_DIR"], "app.db")

This directory survives code updates and rebuilds. It's deleted when the deployment is destroyed.

2. /_data/* REST API (any deployment)

A built-in document store accessible via fetch() from any deployed page. No backend code needed — works with static HTML.

All requests go to /_data/ on the deployment's own domain:

JavaScript
// Create a document
await fetch('/_data/votes', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({option: 'pizza'})
});

// List documents
const {documents} = await fetch('/_data/votes').then(r => r.json());

// Get a document
const doc = await fetch('/_data/votes/cs1abc').then(r => r.json());

// Update a document
await fetch('/_data/votes/cs1abc', {
  method: 'PUT',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({option: 'pizza', count: 2})
});

// Delete a document
await fetch('/_data/votes/cs1abc', {method: 'DELETE'});

Endpoints

MethodPathDescription
GET/_dataList collections
POST/_data/{collection}Create a document
GET/_data/{collection}List documents (?limit=10&offset=20)
GET/_data/{collection}/{id}Get a document
PUT/_data/{collection}/{id}Update a document
DELETE/_data/{collection}/{id}Delete a document
DELETE/_data/{collection}Delete entire collection

Limits

LimitValue
Max document size100 KB
Max documents per collection10,000
Max collections per deployment100
Max total storage50 MB

Server Configuration

The server reads configuration from /var/lib/sandcastle/config.json. The installer creates this file for you.

config.json
{
  "domain": "sandcastle.example.com",
  "port": 3456,
  "dataDir": "/var/lib/sandcastle",
  "defaultTTLHours": 72,
  "defaultMaxDeploys": 10,
  "containerDefaults": {
    "memory": "512m",
    "cpus": "0.5",
    "pidsLimit": 256,
    "diskSize": "2g",
    "networkQuota": "5g"
  }
}

Configuration fields

FieldDefaultDescription
domainlocalhostYour domain name
port3456Port the server listens on (behind Caddy)
dataDir/var/lib/sandcastleData directory (DB, deploys, uploads)
defaultTTLHours72Default deployment lifetime in hours
defaultMaxDeploys10Max deployments per user
containerDefaults.memory512mDefault memory limit per container
containerDefaults.cpus0.5Default CPU limit per container
containerDefaults.pidsLimit256Max processes per container
containerDefaults.diskSizeContainer root filesystem limit (requires overlay2+xfs+pquota)
containerDefaults.networkQuotaMax network transfer per container (requires iptables quota module)

Environment variable

DATA_DIR — overrides the data directory. Defaults to /var/lib/sandcastle.

File paths on server

PathDescription
/var/lib/sandcastle/config.jsonServer configuration
/var/lib/sandcastle/sandcastle.dbSQLite database (WAL mode)
/var/lib/sandcastle/deploys/{id}/Deployment source code
/var/lib/sandcastle/persist/{id}/Persistent data (/data directory)
/etc/caddy/sites/{subdomain}.caddyCaddy reverse proxy configs
/etc/systemd/system/sandcastle.serviceSystemd service unit
/usr/local/bin/sandcastle-serverServer binary

API Reference

All API endpoints require authentication via Authorization: Bearer <api_key> header. The server listens on 127.0.0.1:3456 behind Caddy.

Deployments

POST /api/deploy

Upload a tarball to deploy. Multipart form with tarball field.

POST /api/deploy/code

Deploy inline files as JSON. Body: {"files": {"index.html": "..."}, "name": "my-app"}

POST /api/deploy/{id}/update

Update a deployment with a new tarball.

POST /api/deploy/{id}/update/code

Update a deployment with inline files.

GET /api/deployments

List all deployments for the authenticated user.

DELETE /api/deployments

Destroy all deployments for the authenticated user.

GET /api/deployments/{id}

Get deployment status, URL, framework, and details.

PATCH /api/deployments/{id}

Update deployment metadata (e.g., network quota).

DELETE /api/deployments/{id}

Destroy a deployment permanently.

GET /api/deployments/{id}/logs

Get container logs. Optional ?tail=N query parameter.

GET /api/deployments/{id}/source

Download deployment source code. Optional ?format=json for JSON response with file contents.

POST /api/deployments/{id}/protect

Set access control. Body: {"mode": "basic_auth", "username": "...", "password": "..."} or {"mode": "user", "users": ["alice", "bob"]}

Sandbox

POST /api/sandbox

Create a dev sandbox. Body: {"files": {...}, "name": "...", "env": {...}}

POST /api/sandbox/{id}/push

Push file changes. Body: {"changes": [{"op": "write", "path": "app.js", "content": "..."}]}

POST /api/sandbox/{id}/install

Install packages. Body: {"packages": ["express", "cors"]}

POST /api/sandbox/{id}/exec

Execute a command. Body: {"command": "ls -la", "timeout": 30}

GET /api/sandbox/{id}/logs

Get sandbox logs. Optional ?tail=N.

DELETE /api/sandbox/{id}

Destroy a sandbox.

POST /api/sandbox/{id}/promote

Promote to production deployment.

Data API

Per-deployment document store. See Persistent Data for details.

GET /_data
POST /_data/{collection}
GET /_data/{collection}
GET /_data/{collection}/{id}
PUT /_data/{collection}/{id}
DELETE /_data/{collection}/{id}
DELETE /_data/{collection}

Gallery

GET /api/gallery

Public gallery listing of deployments.

Admin

GET /api/admin/users

List all users (admin only).

POST /api/admin/users

Create a user.

DELETE /api/admin/users/{name}

Delete a user.

PATCH /api/admin/users/{name}

Update user settings.

GET /api/admin/settings

Get server settings.

POST /api/admin/settings

Update server settings.

OAuth 2.1

GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-authorization-server
POST /oauth/register
GET /oauth/authorize
POST /oauth/token

MCP

POST /mcp

MCP Streamable HTTP endpoint (JSON-RPC 2.0). Used by Claude.ai web interface.

Security

Sandcastle assumes all deployed code is potentially hostile and sandboxes everything:

Container isolation

Resource limits

ResourceDefault Limit
Memory512 MB (runtime), 1 GB (sandbox)
CPU0.5 cores
PIDs256 processes
DiskConfigurable via diskSize
Network transferConfigurable via networkQuota (iptables)

HTTPS

All traffic is served over HTTPS. Caddy automatically provisions and renews TLS certificates from Let's Encrypt using HTTP-01 challenges. No manual certificate management needed.

Hard delete

Destroying a deployment removes everything — the database record, source code, Docker volumes, persistent data, and Caddy proxy config. Nothing is left behind.

Two-phase deployment

Blue-green updates

When you update a deployment, a new volume is created and the build runs while the old container continues serving. On success, traffic swaps to the new container. On failure, the old container stays running (automatic rollback).