Snapshots
Section titled “Snapshots”A snapshot records a container’s filesystem at a point in time: every file on its own storage, including database rows, config, logs, installed packages and environment files. It does not record memory or running processes, and it does not cover storage mounted from elsewhere, such as a share whose source is another container. Rather than a diff of selected paths, it is the complete disk state of the machine, frozen at a moment and restorable with one API call.
You do not decide what to include, write migration scripts, or track what changed. A button press or a single API call preserves the container’s own storage, and another puts it back. Restoring puts back that storage along with the container’s recorded configuration and devices: a container that was running is stopped and started again, so expect a short interruption, and one that was stopped stays stopped; anything outside that storage, such as a shared mount, a remote API call the container already made, or a credential it disclosed, is not reversed.
What a snapshot captures
Section titled “What a snapshot captures”Every snapshot records the complete filesystem state of a container:
| Component | Captured | What it means |
|---|---|---|
| Filesystem | Files, directories, permission bits | Code, configs, logs, and data, exactly as they were |
| Databases | Data, tables, indexes | SQLite files and PostgreSQL data directories, byte-identical on disk |
| Installed software | apt packages, npm modules, binaries | Restored at their exact versions, nothing to reinstall |
| Environment | Environment files, shell configs, crontabs | The on-disk runtime context is preserved |
| Network config | DNS settings, routing table, proxy configuration | On-disk network configuration is identical after restore |
If it is written to the container’s own disk, the snapshot captures it. Mounted shares belong to their source, and memory and in-flight work are not captured.
How snapshots work
Section titled “How snapshots work”Hoody uses Copy-on-Write (CoW) at the filesystem level. When you create a snapshot, Hoody does not copy the entire disk. It marks the current filesystem state as immutable and begins tracking changes. Only new or modified blocks are stored separately.
This means:
- Fast creation. A snapshot marks the current state rather than copying the disk, so creation cost does not grow with container size; nothing is copied or compressed.
- Minimal storage. The first snapshot references the existing filesystem, and each later snapshot stores only the delta. Ten snapshots of a 50GB container do not cost 500GB; they cost 50GB plus the changes.
- Frequent snapshots. The per-snapshot overhead is small enough to snapshot every commit, deployment, or experiment. Per-container snapshot caps apply (higher on rented servers than on the free tier), and expiring snapshots clean themselves up.
- Whole-machine restore. Restoring a snapshot returns the container to the captured state exactly, in one API call, with no migration scripts and no partial state. How long it takes depends on the container.
Snapshot 1 (baseline) ──→ Full filesystem referenceSnapshot 2 (after AI) ──→ Delta: 47 files changedSnapshot 3 (after deploy)──→ Delta: 12 files changedSnapshot 4 (new feature) ──→ Delta: 89 files changed
Total storage: baseline + 148 files of changesNOT: 4 full copies of the filesystemA restore point before AI changes
Section titled “A restore point before AI changes”AI generates code you cannot fully review. An LLM rewrites your authentication module: it looks correct at a glance and passes the tests you thought to write, but a subtle change in how sessions are invalidated slips past code review. Three days later you notice stale sessions. Two days after that, you trace them to the AI’s rewrite, and a week of development now sits on top of the bug.
Without a snapshot, that means reconstructing a week of changes by hand. With one, you restore before-ai-auth-rewrite, compare the two states, fix the specific issue, and move on.
# Before letting AI touch your codeSNAPSHOT_NAME=$(hoody snapshots create -c "$CONTAINER_ID" \ --alias "before-ai-refactor" -o json | jq -r '.snapshot.name')
# AI makes its changes...# If something breaks, restore. Restore prompts for confirmation, and in a# script the prompt resolves to "no", so pass -y when you mean it.hoody snapshots restore -c "$CONTAINER_ID" --name "$SNAPSHOT_NAME" -y
# Back to exactly where you wereimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Snapshot before AI makes changesconst snapshot = await client.api.snapshots.create(containerId, { alias: 'before-ai-refactor'});
// Let AI work...// If it breaks things:await client.api.snapshots.restore( containerId, snapshot.data.snapshot.name);// Container is exactly as it was before the AI touched it# Snapshot before AI changesSNAPSHOT_NAME=$(curl -s -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "before-ai-refactor"}' \ | jq -r '.data.snapshot.name')
# AI makes changes...
# If something broke, restorecurl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots/$SNAPSHOT_NAME" \ -H "Authorization: Bearer $HOODY_TOKEN"
# When the restore completes: everything is exactly as it wasOne request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
The first link snapshots the container before AI starts; the second restores it if the change turns out wrong. Restore uses the exact name from the create response — paste it in place of SNAPSHOT_NAME.
# Snapshot before AI changes
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots&method=POST&bearer_token=TOKEN&json={"alias":"before-ai-refactor"}&response=transparent
# Restore if something broke
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots/SNAPSHOT_NAME&method=PUT&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Snapshot before every AI interaction. Creating one takes seconds, and having one can save hours of debugging.
Branching with snapshots
Section titled “Branching with snapshots”Git lets you branch code so an experiment cannot damage the main line. Snapshots bring the same workflow to the whole container.
Main state (snapshot: "production-stable") │ ├──→ Experiment A: try new database schema │ Result: works. Create snapshot "with-new-schema" │ ├──→ Experiment B: try different AI model │ Result: failed. Restore to "production-stable" │ └──→ Experiment C: try new auth system Result: promising. Create snapshot "auth-v2-wip"Everything happens on the same container, with nothing to clone, provision, or wait for. You are not creating new machines; you are recording states of one machine and moving between them.
# Save the current stateSNAPSHOT_NAME=$(hoody snapshots create -c $CONTAINER_ID \ --alias "main-branch" -o json | jq -r '.snapshot.name')
# Experiment: try a risky database migrationhoody terminal commands run -c $CONTAINER_ID --ephemeral \ --command "python3 migrate.py --destructive"
# If it worked: save the resulthoody snapshots create -c $CONTAINER_ID --alias "after-migration"
# If it failed: restore and try something else (use the exact `name` returned by create/list)hoody snapshots restore -c $CONTAINER_ID --name "$SNAPSHOT_NAME" -yimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Save current stateconst mainSnapshot = await client.api.snapshots.create(id, { alias: 'main-branch'});
// Try experiment...
// Worked? Save the resultawait client.api.snapshots.create(id, { alias: 'experiment-success'});
// Failed? Restore (use the exact `name` returned by create/list)await client.api.snapshots.restore(id, mainSnapshot.data.snapshot.name);# Create a branch pointSNAPSHOT_NAME=$(curl -s -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "main-branch"}' \ | jq -r '.data.snapshot.name')
# Experiment...
# Branch back to main (use the exact `name` returned by create/list)curl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots/$SNAPSHOT_NAME" \ -H "Authorization: Bearer $HOODY_TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Saves the current state as a branch point, then returns to it if an experiment fails. As with any restore, use the exact snapshot name from create or list, not necessarily the alias you set.
# Save current state
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots&method=POST&bearer_token=TOKEN&json={"alias":"main-branch"}&response=transparent
# Branch back to main
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots/SNAPSHOT_NAME&method=PUT&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Deployment rollbacks
Section titled “Deployment rollbacks”Snapshots turn deployments into reversible operations. Capture the state before you deploy, verify the result, and restore if verification fails.
1. Snapshot: POST /api/v1/containers/{prod}/snapshots {"alias": "pre-deploy-v2.1.0", "expiry": 30}
2. Deploy: Execute your deployment scripts
3. Verify: Health checks, smoke tests, monitoring
4. Success: Delete the snapshot after 30 days (or let it expire)
5. Failure: PUT /api/v1/containers/{prod}/snapshots/{SNAPSHOT_NAME} (the exact `name` returned by the create response) Production restored to the pre-deploy stateA restore does not mean rolling back the code, re-running migrations, and hoping the data is consistent. It rolls back everything at once, in one API call: code, config, data, installed packages, the entire disk state.
# Before deploymentSNAPSHOT_NAME=$(hoody snapshots create -c $PROD_CONTAINER \ --alias "pre-deploy-v2.1.0" \ --expiry 30 -o json | jq -r '.snapshot.name')
# Deployhoody terminal commands run -c $PROD_CONTAINER --ephemeral \ --command "./deploy.sh v2.1.0"
# Verifyhoody terminal commands run -c $PROD_CONTAINER --ephemeral \ --command "curl -s localhost:3000/health | jq .status"
# If failed: roll back (use the exact `name` from the create output)hoody snapshots restore -c $PROD_CONTAINER --name "$SNAPSHOT_NAME" -yimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
async function deployWithRollback(containerId: string, version: string) { // 1. Pre-deploy snapshot const snap = await client.api.snapshots.create(containerId, { alias: `pre-deploy-${version}`, expiry: 30 });
// 2. Deploy. wait defaults to true, so the call returns the finished command. // With wait: false a single getResult can come back while it is still // running, and the health check below would then test a half-done deploy. const box = await client.withContainer(containerId); const exec = await box.terminal.commands.run({ command: `./deploy.sh ${version}` }); const deployed = exec.data.status === 'completed' && exec.data.exit_code === 0;
// 3. Health check. A refused connection rejects instead of returning a // response, so catch it: that is a failed check, not an error to escape. let healthy = false; if (deployed) { try { const health = await fetch( `https://${PROJECT_ID}-${containerId}-http-3000.${SERVER_NAME}.containers.hoody.com/health` ); healthy = health.ok; } catch { healthy = false; } }
if (!healthy) { // 4. Rollback await client.api.snapshots.restore( containerId, snap.data.snapshot.name ); throw new Error(`Deploy ${version} failed, rolled back`); }}# Complete deployment with rollback safety
# 1. Pre-deploy snapshotSNAP=$(curl -s -X POST "https://api.hoody.com/api/v1/containers/$PROD/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "pre-deploy-v2.1.0", "expiry": 30}' \ | jq -r '.data.snapshot.name')
# 2. Deploycurl -X POST "https://$PROJECT-$PROD-terminal-1.$SERVER.containers.hoody.com/api/v1/terminal/execute" \ -H "Content-Type: application/json" \ -d '{"command": "./deploy.sh v2.1.0", "wait": true}'
# 3. Health checkSTATUS=$(curl -s -o /dev/null -w "%{http_code}" \ "https://$PROJECT-$PROD-http-3000.$SERVER.containers.hoody.com/health")
# 4. Rollback if failedif [ "$STATUS" != "200" ]; then curl -X PUT "https://api.hoody.com/api/v1/containers/$PROD/snapshots/$SNAP" \ -H "Authorization: Bearer $HOODY_TOKEN" echo "Rolled back to $SNAP"fiOne request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
These three links do not chain themselves — each is a standalone request, and you carry the result from one into the next by hand. Click the first to snapshot the container before deploying, then run the deploy link. If the health check after it fails, take the name field from the snapshot response and paste it in place of SNAPSHOT_NAME in the rollback link before using it.
# Pre-deploy snapshot
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots&method=POST&bearer_token=TOKEN&json={"alias":"pre-deploy-v2.1.0","expiry":30}&response=transparent
# Deploy
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-terminal-1.SERVER.containers.hoody.com/api/v1/terminal/execute&method=POST&json={"command":"./deploy.sh%20v2.1.0","wait":true}&response=transparent
# Rollback if the health check fails
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots/SNAPSHOT_NAME&method=PUT&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Historical debugging
Section titled “Historical debugging”Something broke, but you are not sure when. With snapshots at regular intervals, you can binary-search for the change:
Monday snapshot: workingTuesday snapshot: workingWednesday snapshot: BROKENRestore Tuesday’s snapshot and confirm it still works: the bug was introduced between Tuesday and Wednesday. If you keep hourly snapshots, narrow the window to an hour, then compare the two states to find the exact change.
The bug might be in a config file, an environment variable, a system package update, or a cron job that ran at 3 AM. Everything on disk is in the snapshot, so the search covers the whole machine rather than only the source tree.
Snapshot management
Section titled “Snapshot management”Create a snapshot
Section titled “Create a snapshot”# Create with aliashoody snapshots create -c $CONTAINER_ID --alias "milestone-v1"
# Create with expiration (auto-delete after 7 days)hoody snapshots create -c $CONTAINER_ID \ --alias "temp-experiment" \ --expiry 7
# Create permanent snapshot (no expiration)hoody snapshots create -c $CONTAINER_ID \ --alias "golden-image"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Create a snapshotconst snapshot = await client.api.snapshots.create(containerId, { alias: 'milestone-v1', expiry: 90 // Days until auto-deletion});
console.log(snapshot.data.snapshot.name);// "milestone-v1" — with an alias, the name IS the alias (stripped to letters, digits, _ and -)# Create a snapshotcurl -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "alias": "milestone-v1", "expiry": 90 }'
# Response:# {# "data": {# "snapshot": {# "name": "milestone-v1",# "alias": "milestone-v1",# "created_at": "2026-03-04T14:30:45.000Z",# "stateful": false,# "size": 4589764321# }# }# }One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates a snapshot with alias milestone-v1 that expires automatically after 90 days. The response’s name field is what later restore or delete calls take; for an alias like milestone-v1 the name is the alias itself, because an alias is stripped to letters, digits, _ and - to form the name.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots&method=POST&bearer_token=TOKEN&json={"alias":"milestone-v1","expiry":90}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
List snapshots
Section titled “List snapshots”# List all snapshots for a containerhoody snapshots list -c $CONTAINER_IDimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const snapshots = await client.api.snapshots.list(containerId);
for (const snap of snapshots.data.snapshots) { console.log(`${snap.alias || snap.name} - ${snap.created_at} - ${snap.stateful ? 'stateful' : 'stateless'}`);}curl "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists every snapshot recorded for the container, including each one’s alias, name, and creation time.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots&method=GET&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Restore a snapshot
Section titled “Restore a snapshot”Restore returns 409 while the container’s network policy is still pending, with Container has no network policy in force and cannot be restored from a snapshot. Configure its network first, then retry. Restore also reapplies the container’s current API-managed environment variables afterwards, and the call returns without waiting for that.
# Restore from a snapshot (use the exact `name` returned by create/list).# The CLI asks you to confirm; add -y when running it unattended.hoody snapshots restore -c $CONTAINER_ID --name "$SNAPSHOT_NAME"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Restore container to a previous stateawait client.api.snapshots.restore(containerId, snapshotName);// The container's own disk is back to the snapshot. Mounted shares, remote// effects and API-managed environment variables are not rolled back with it.# Restore to snapshotcurl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots/$SNAPSHOT_NAME" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Container reverts to snapshot stateOne request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Restores the container to the snapshot whose name you paste in place of SNAPSHOT_NAME — take it from the create response or list. This is destructive — everything changed since that snapshot is lost, so snapshot the current state first if you want to keep it.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots/SNAPSHOT_NAME&method=PUT&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Delete a snapshot
Section titled “Delete a snapshot”# Delete a snapshot to free storage.# The CLI asks you to confirm; add -y when running it unattended.hoody snapshots delete -c $CONTAINER_ID --name "$SNAPSHOT_NAME"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.snapshots.delete(containerId, snapshotName);// Storage freed immediatelycurl -X DELETE "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/snapshots/$SNAPSHOT_NAME" \ -H "Authorization: Bearer $HOODY_TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Deletes the snapshot whose name you paste in place of SNAPSHOT_NAME — take it from the create response or list — and frees its storage immediately.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/snapshots/SNAPSHOT_NAME&method=DELETE&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Snapshot strategies
Section titled “Snapshot strategies”Snapshots before AI tasks
Section titled “Snapshots before AI tasks”Snapshot before every AI task and set the expiry to 7 days. If the AI’s changes survive a week of use, the snapshot deletes itself. If something surfaces before then, you have the week to catch it.
# Alias pattern: before-ai-{task}-{date}POST /api/v1/containers/{id}/snapshots{"alias": "before-ai-auth-rewrite-2026-03-04", "expiry": 7}Deployment milestones
Section titled “Deployment milestones”Snapshot before and after every deployment. Keep the “before” for 30 days (rollback window). Keep the “after” permanently if the version is a major release.
# Before deploy: temporary{"alias": "pre-deploy-v2.1.0", "expiry": 30}
# After deploy (major version): permanent{"alias": "v2.0.0-stable"}Daily snapshots
Section titled “Daily snapshots”Use cron or hoody-cron to snapshot every container daily. Set expiry to 30 days. You always have a month of daily restore points, and old snapshots clean themselves up.
Template images
Section titled “Template images”Set up a development environment once and snapshot it permanently. When a new team member joins, copy the container from that snapshot. One golden image produces as many copies as you need.
# The golden image: never expires{"alias": "dev-template-2026-q1"}
# New team member:POST /api/v1/containers/{template}/copy{"target_project_id": "...", "name": "alice-dev", "source_snapshot": "dev-template-2026-q1"}Snapshots in incident response
Section titled “Snapshots in incident response”When a container is compromised:
- Snapshot the compromised state for forensic analysis
- Restore the last known-good snapshot so production runs from a clean state
- Compare the two snapshots to identify exactly what changed: which files were modified, which files and packages were added or changed
- Delete the compromised snapshot after analysis
You keep the filesystem evidence: the attacker’s changes stay in a snapshot for later study while you restore a clean state. Restoring interrupts the container, so serve from a second one if you must stay up, and remember that a disk snapshot records the disk, not running processes or network traffic. The whole sequence is a handful of API calls.
What Git cannot do
Section titled “What Git cannot do”Git versions code. Snapshots version everything else.
| Git | Snapshots | |
|---|---|---|
| Source code | Yes | Yes |
| Database state | No | Yes |
| System configuration | Partially (dotfiles) | Yes (all of /etc) |
| Installed packages | No (requires rebuild) | Yes (exact binary state) |
| Environment files | No (.env in .gitignore) | Yes |
| On-disk app/browser data | No | Yes |
| Network configuration | No | Yes |
| Restore | Rebuild (clone + install + build + migrate) | One API call |
Git versions what you wrote and snapshots version what you run; together they cover the entire stack.
Next: Realms & Projects explains how containers are organized.