Copy & Sync
Section titled “Copy & Sync”A copy is a complete, independent duplicate of a container, and it can land in a different project or on a different server. A sync updates an existing copy with whatever changed on the source since.
This page covers both operations and the workflows built on them: redundancy, team environments, and disaster recovery. Snapshots are the transfer mechanism underneath.
API endpoints summary
Section titled “API endpoints summary”This page explains the concepts behind copy and sync. The endpoint reference carries the full request and response schemas.
Copy operations:
- POST /api/v1/containers/{id}/copy - Duplicate container
Sync operations:
- POST /api/v1/containers/{id}/sync - Sync copy with source
Related:
- GET /api/v1/containers/{id} - Check source_container_id for copies
- POST /api/v1/containers/{id}/snapshots - Source for copy operation
What a copy includes
Section titled “What a copy includes”The copy operation duplicates a source container into a new one:
Source Container (Production) ↓ (copy operation)New Container (Staging)The copy includes:
- Entire filesystem (all files, directories)
- Configuration (environment vars, resource allocation)
- Installed software (packages, dependencies)
- Data (databases, user files)
The copy gets:
- New container ID (independent lifecycle)
- New SSH key (security requirement)
- New service URLs (different project/container IDs)
- Reference to source (via
source_container_id)
The copy runs independently: changes to the copy do not affect the source, and changes to the source do not reach the copy until you sync.
When to copy a container
Section titled “When to copy a container”Staging from production
Section titled “Staging from production”Get the exact production state for testing:
Staging now matches production: same code, same data, same configuration. Test updates there before deploying to production.
Team development environments
Section titled “Team development environments”Every developer gets an identical setup:
One template produces any number of developer environments, each starting from the same filesystem, packages, and configuration.
Disaster recovery
Section titled “Disaster recovery”Redundancy across geographic regions:
If the US server fails: the EU backup is already provisioned and can take over.
A/B testing
Section titled “A/B testing”Duplicate for parallel testing:
# Copy to test different approachesPOST /api/v1/containers/{app_id}/copy{"target_project_id": "{project}", "name": "variant-a"}
POST /api/v1/containers/{app_id}/copy{"target_project_id": "{project}", "name": "variant-b"}Run the experiments in parallel without touching the original.
Copy a container
Section titled “Copy a container”Basic copy (same server, same project)
Section titled “Basic copy (same server, same project)” Response:
{ "statusCode": 201, "message": "Container copy initiated successfully", "data": { "id": "01bcdef123456789abcdef012", "name": "container-copy", "status": "copying", "source_container_id": "890abcdef12345678901cdef", "project_id": "67e89abc123def456789abcd", "server_id": "63f8b0e5c9a1b2d3e4f5a6b7", "server_name": "node-us", "created_at": "2025-11-09T15:00:00.000Z" }}The copy runs asynchronously. Status progresses from copying to running, and the copy starts automatically once the background job finishes.
Copy-on-write (CoW):
- Only unique or changed data blocks are transferred
- Shared blocks are referenced, not duplicated
- Incremental changes keep network transfer small
- Dozens of copies cost far less storage than dozens of full containers
Typical copy time: the Copy timing table below breaks this down by container size. Under 10 GB, a same-server copy usually finishes in one to two minutes; cross-server times scale with network bandwidth. A sync is faster, at 10 seconds to 3 minutes for an incremental update.
Cross-project copy
Section titled “Cross-project copy”Duplicate into a different project:
Use cases:
- Client demo environments
- Separate staging/production projects
- Team member personal projects
- Experiment isolation
Cross-server copy
Section titled “Cross-server copy”Duplicate to a different geographic location:
Benefits:
- Geographic redundancy
- Lower latency for EU users
- Disaster recovery (different datacenter)
Copy time is slower here, because the data crosses the network between servers.
Copy from a specific snapshot
Section titled “Copy from a specific snapshot”Use a known-good snapshot as the source:
Why pin a snapshot:
- The source container may have changed since you last tested it
- You copy from a state you know is stable
- Environments stay reproducible (always copy from the v1.0.0 snapshot)
If omitted: the copy takes the source’s current state, meaning its latest snapshot if it is running, or its current filesystem.
SSH key security
Section titled “SSH key security”A copy must use a different SSH key from its source.
Auto-generated keys
Section titled “Auto-generated keys”# Omit ssh_public_key → auto-generatedPOST /api/v1/containers/{source_id}/copy{ "target_project_id": "{project}", "name": "copy-with-auto-key"}Hoody generates a new key pair automatically, so the copy has unique credentials.
Custom keys
Section titled “Custom keys”# Provide your own public keyPOST /api/v1/containers/{source_id}/copy{ "target_project_id": "{project}", "name": "copy-with-custom-key", "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB..."}Best practice: give different team members different SSH keys.
Sync a copy
Section titled “Sync a copy”What sync transfers
Section titled “What sync transfers”Sync performs an incremental update from source to copy:
Source Container (updated with new features) ↓ (sync operation)Copy Container (receives updates incrementally)Sync transfers:
- Filesystem changes (new/modified files)
- Deleted files (removed from copy)
- Updated data (databases, caches)
Sync preserves:
- Copy’s unique settings (name, SSH key, color)
- Copy’s container ID
- Copy’s service URLs
- Copy’s network/firewall configuration
A sync is much faster than a full copy, because only the changes move.
Basic sync operation
Section titled “Basic sync operation” Response:
{ "statusCode": 200, "message": "Container sync initiated successfully", "data": { "container_id": "01bcdef123456789abcdef012", "source_container_id": "890abcdef12345678901cdef", "status": "copying" }}Sync runs asynchronously. Typical time is 10 seconds to 3 minutes, depending on how much data changed.
Sync requirements
Section titled “Sync requirements”Sync only works if:
- The container was created by a copy operation, so it has a
source_container_id - The source container still exists
- You have access to the source container
If the source is deleted: the copy is orphaned and can no longer sync.
Copy vs sync
Section titled “Copy vs sync”Copy (full duplication)
When: First time creating duplicate
Process:
- Creates complete independent container
- Full data transfer
- New container ID and URLs
- Can be in different project/server
Time:
- Same server: 1-5 minutes
- Cross-server: 5-15 minutes
Storage:
- Full container size
Use for:
- Initial environment setup
- Geographic replication
- Team onboarding
Sync (incremental update)
When: Updating existing copy
Process:
- Transfers only changes
- Preserves copy’s unique settings
- Same container ID and URLs
- Requires existing copy
Time:
- Typically 10 seconds to 3 minutes, longer for multi-GB diffs
- Only changed data transferred
Storage:
- Incremental (only changes)
Use for:
- Keeping staging updated
- Propagating bug fixes
- Syncing team environments
Workflow: copy once in full, then sync as often as you need.
Real-world scenarios
Section titled “Real-world scenarios”Staging synced with production
Section titled “Staging synced with production”# Day 1: Create staging from productionhoody containers copy $PROD_ID --target-project-id $STAGING_PROJECT --name staging-api
# Week 1: Sync staging to get production fixeshoody containers sync $STAGING_COPY_ID
# Week 2: Sync again (incremental, fast)hoody containers sync $STAGING_COPY_ID// Day 1: Create staging from productionconst copy = await client.api.containers.copy(PROD_ID, { target_project_id: STAGING_PROJECT, name: 'staging-api'});
// Week 1: Sync staging to get production fixesawait client.api.containers.sync(copy.data.id);
// Week 2: Sync again (incremental, fast)await client.api.containers.sync(copy.data.id);# Day 1: Create staging from productioncurl -X POST "https://api.hoody.com/api/v1/containers/{prod_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"target_project_id": "{staging_project}", "name": "staging-api"}'
# Week 1: Sync staging to get production fixescurl -X POST "https://api.hoody.com/api/v1/containers/{staging_copy_id}/sync" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Week 2: Sync again (incremental, fast)curl -X POST "https://api.hoody.com/api/v1/containers/{staging_copy_id}/sync" \ -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
Creates the staging copy once, then re-runs the sync link whenever production moves ahead — the sync link is the same request every time.
# Create staging from production
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/PROD_ID/copy&method=POST&bearer_token=TOKEN&json={"target_project_id":"STAGING_PROJECT_ID","name":"staging-api"}&response=transparent
# Sync staging with production
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/STAGING_COPY_ID/sync&method=POST&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.
Staging stays current without a full re-copy.
Team environment template
Section titled “Team environment template”# New team member Alice joins - copy templatehoody containers copy $TEMPLATE_ID --target-project-id $ALICE_PROJECT --name alice-dev-env
# Template gets updated - sync Alice's environmenthoody containers sync $ALICE_COPY_ID
# Another developer Bob joinshoody containers copy $TEMPLATE_ID --target-project-id $BOB_PROJECT --name bob-dev-env// New team member Alice joins - copy templateconst alice = await client.api.containers.copy(TEMPLATE_ID, { target_project_id: ALICE_PROJECT, name: 'alice-dev-env'});
// Template gets updated - sync Alice's environmentawait client.api.containers.sync(alice.data.id);
// Another developer Bob joinsawait client.api.containers.copy(TEMPLATE_ID, { target_project_id: BOB_PROJECT, name: 'bob-dev-env'});# New team member Alice joins - copy templatecurl -X POST "https://api.hoody.com/api/v1/containers/{template_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"target_project_id": "{alice_project}", "name": "alice-dev-env"}'
# Template gets updated - sync Alice's environmentcurl -X POST "https://api.hoody.com/api/v1/containers/{alice_copy_id}/sync" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Another developer Bob joinscurl -X POST "https://api.hoody.com/api/v1/containers/{template_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"target_project_id": "{bob_project}", "name": "bob-dev-env"}'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
Copies the template into a new teammate’s project — repeat the copy link with that person’s project id and a new name. The sync link refreshes one existing copy after the template changes.
# Copy template for Alice
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/TEMPLATE_ID/copy&method=POST&bearer_token=TOKEN&json={"target_project_id":"ALICE_PROJECT_ID","name":"alice-dev-env"}&response=transparent
# Sync Alice's environment
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/ALICE_COPY_ID/sync&method=POST&bearer_token=TOKEN&response=transparent
# Copy template for Bob
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/TEMPLATE_ID/copy&method=POST&bearer_token=TOKEN&json={"target_project_id":"BOB_PROJECT_ID","name":"bob-dev-env"}&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.
One template keeps the whole team synchronized.
Geographic redundancy
Section titled “Geographic redundancy”# Copy production to EU for redundancyhoody containers copy $PROD_US_ID --target-project-id $PROJECT_ID --target-server-id $EU_SERVER_ID --name prod-eu-replica
# Monthly: Sync EU replica with US changeshoody containers sync $PROD_EU_ID// Copy production to EU for redundancyconst euReplica = await client.api.containers.copy(PROD_US_ID, { target_project_id: PROJECT_ID, target_server_id: EU_SERVER_ID, name: 'prod-eu-replica'});
// Monthly: Sync EU replica with US changesawait client.api.containers.sync(euReplica.data.id);# Copy production to EU for redundancycurl -X POST "https://api.hoody.com/api/v1/containers/{prod_us_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "target_project_id": "{same_project}", "target_server_id": "{eu_server_id}", "name": "prod-eu-replica" }'
# Monthly: Sync EU replica with US changescurl -X POST "https://api.hoody.com/api/v1/containers/{prod_eu_id}/sync" \ -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
Copies production to the EU server once to stand up the replica, then the sync link is the one to re-run whenever US changes need to reach it.
# Copy production to EU
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/PROD_US_ID/copy&method=POST&bearer_token=TOKEN&json={"target_project_id":"PROJECT_ID","target_server_id":"EU_SERVER_ID","name":"prod-eu-replica"}&response=transparent
# Sync EU replica with US changes
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/PROD_EU_ID/sync&method=POST&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.
If the US datacenter fails: switch to the EU replica with a proxy alias.
Feature branch testing
Section titled “Feature branch testing”# Copy production to test new featurecurl -X POST "https://api.hoody.com/api/v1/containers/{prod_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -d '{ "target_project_id": "{same_project}", "name": "feature-auth-v2" }'
# Work on feature in the copy# (Terminal, display, files all available)
# When the feature is complete, snapshot itPOST /api/v1/containers/{feature_copy_id}/snapshots{"alias": "feature-auth-v2-complete"}
# Copy this feature container to productionPOST /api/v1/containers/{feature_copy_id}/copy{ "target_project_id": "{production_project}", "name": "prod-with-auth-v2"}Experiments run against real production data without touching production.
Copy operation details
Section titled “Copy operation details”Copy process flow
Section titled “Copy process flow”1. Copy initiated (status: copying)2. Source snapshot created (if needed)3. Snapshot transferred to target server4. New container provisioned from snapshot5. SSH keys generated/configured6. Copy complete and started (status: running)Track progress:
# Check copy statuscurl "https://api.hoody.com/api/v1/containers/{new_copy_id}" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Watch for: "status": "copying" → "status": "running"Copy parameters
Section titled “Copy parameters”Required:
target_project_id- Destination project
Optional:
target_server_id- Destination server (defaults to source server)name- Copy name (auto-generated if omitted)ssh_public_key- Custom SSH key (auto-generated if omitted)source_snapshot- Specific snapshot to copy from (uses latest if omitted)copy_firewall_rules- Copy the source’s firewall rules (ACL) to the copy. Default:falsecopy_network_rules- Copy the source’s network rules/settings to the copy. Default:false
Copy timing
Section titled “Copy timing”Same server:
- 10 GB container: ~1-2 minutes
- 50 GB container: ~3-5 minutes
- 100 GB container: ~5-10 minutes
Cross-server (data transfer over network):
- 10 GB container: ~3-5 minutes
- 50 GB container: ~10-15 minutes
- 100 GB container: ~20-30 minutes
Depends on:
- Container size
- Network bandwidth between servers
- Server load
- Snapshot size
Keep copies updated
Section titled “Keep copies updated”When to sync
Section titled “When to sync”Sync when the source has updates:
- Code deployments to production → sync staging
- Template improvements → sync team environments
- Security patches → sync all replicas
- Data updates → sync backups
How often:
- Development: Daily or on-demand
- Staging from production: After each prod deployment
- Disaster recovery: Weekly or monthly
- Team environments: When template updates
Sync operation
Section titled “Sync operation” What happens:
- Source’s latest snapshot is captured
- Changed files identified (incremental diff)
- Changes transferred to copy
- Copy’s filesystem updated
- Copy restarted (if was running)
Sync is incremental: only the changes are transferred, which is much faster than a full re-copy.
Sync vs re-copy
Section titled “Sync vs re-copy”# Update existing copyPOST /api/v1/containers/{copy_id}/syncAdvantages:
- Faster (incremental)
- Preserves copy’s unique settings
- Same container ID/URLs
- Less bandwidth usage
- Copy relationship preserved (source_container_id)
When: Source has incremental updates
# Delete old copyDELETE /api/v1/containers/{old_copy_id}
# Create fresh copyPOST /api/v1/containers/{source_id}/copy{"target_project_id": "{project}", "name": "new-copy"}Advantages:
- Clean slate
- New container ID
- Can change target project/server
When: Major source changes, or troubleshooting sync issues
Prefer sync for regular updates. Re-copy only when you want to start fresh.
Automation patterns
Section titled “Automation patterns”Nightly staging sync
Section titled “Nightly staging sync”// Automated sync script (run via cron at 2 AM)async function syncStaging() { const token = process.env.HOODY_TOKEN; const stagingCopyId = process.env.STAGING_CONTAINER_ID;
console.log('Starting nightly staging sync...');
// Sync staging with production const response = await fetch( `https://api.hoody.com/api/v1/containers/${stagingCopyId}/sync`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } } );
const result = await response.json();
if (response.ok) { console.log('Staging synced successfully'); console.log(`Sync status: ${result.data.status}`); } else { console.error('Sync failed:', result.message); // Send alert via hoody-notifications }}Run daily: staging stays current with production without manual steps.
Team environment sync
Section titled “Team environment sync”Copy and sync share an account-wide write limit. Two buckets guard both endpoints: a burst bucket (CONTAINER_COPY_BURST_RATE_LIMIT_MAX) and an hourly one (CONTAINER_COPY_HOURLY_RATE_LIMIT_MAX), each multiplied by the deployment’s RATE_LIMIT_MAX_MULTIPLIER. In the shipped configuration that works out to 100 requests per 5 minutes and 200 per hour. Fan out in bounded batches and retry on 429 instead of firing one request per copy at once.
// Sync all developer environments with template updatesasync function syncTeamEnvironments(templateId, teamCopyIds) { const token = process.env.HOODY_TOKEN; const headers = { 'Authorization': `Bearer ${token}` }; const BATCH_SIZE = 5;
async function syncOne(copyId, attempt = 1) { const response = await fetch(`https://api.hoody.com/api/v1/containers/${copyId}/sync`, { method: 'POST', headers });
if (response.status === 429 && attempt <= 5) { // Respect the account-wide copy/sync limit const retryAfter = Number(response.headers.get('retry-after')) || 30; await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); return syncOne(copyId, attempt + 1); }
const result = await response.json(); if (!response.ok) { console.error(`Sync failed for ${copyId}:`, result.message); return null; } return result; }
// Sync in bounded batches, never all at once const syncs = []; for (let i = 0; i < teamCopyIds.length; i += BATCH_SIZE) { const batch = teamCopyIds.slice(i, i + BATCH_SIZE); syncs.push(...await Promise.all(batch.map(copyId => syncOne(copyId)))); }
console.log(`Synced ${syncs.filter(Boolean).length} team environments`);
// Notify team of updates via Slack/email}
// Run after template updatessyncTeamEnvironments( 'template_container_id', ['alice_copy_id', 'bob_copy_id', 'charlie_copy_id']);The whole team ends up on the same tools and configuration.
Conditional sync on source changes
Section titled “Conditional sync on source changes”// Only sync if source has recent updatesasync function conditionalSync(sourceId, copyId) { const token = process.env.HOODY_TOKEN; const headers = { 'Authorization': `Bearer ${token}` };
// Get source container details const source = await fetch( `https://api.hoody.com/api/v1/containers/${sourceId}`, { headers } ).then(r => r.json());
// Get copy details const copy = await fetch( `https://api.hoody.com/api/v1/containers/${copyId}`, { headers } ).then(r => r.json());
// Compare update times const sourceUpdated = new Date(source.data.updated_at); const copyUpdated = new Date(copy.data.updated_at);
// Sync only if source is newer if (sourceUpdated > copyUpdated) { console.log('Source has updates, syncing...'); await fetch( `https://api.hoody.com/api/v1/containers/${copyId}/sync`, { method: 'POST', headers } ); } else { console.log('Copy is current, no sync needed'); }}This avoids syncs when the source has not changed.
Copy tracking
Section titled “Copy tracking”Identify copies
Section titled “Identify copies”Check whether a container is a copy:
Response includes:
{ "data": { "id": "01bcdef123456789abcdef012", "source_container_id": "890abcdef12345678901cdef", ... }}If source_container_id is not null, the container is a copy.
Find all copies of a source
Section titled “Find all copies of a source” Parse the response for containers where source_container_id matches your source container ID.
You can then sync every copy programmatically.
Copy lineage
Section titled “Copy lineage”Copy and sync operations do not return separate history IDs. Lineage is tracked
through the container’s source_container_id field, the only copy-relationship
field the API exposes.
{ "id": "01bcdef123456789abcdef012", "source_container_id": "890abcdef12345678901cdef"}Use for:
- Tracking the relationship between a copy and its source
- Discovering all copies of a source container (filter on
source_container_id) - Determining sync eligibility (only containers with a
source_container_idcan sync)
Common patterns
Section titled “Common patterns”Production to staging to development
Section titled “Production to staging to development”# Weekly: Copy production to staginghoody containers copy $PROD_ID --target-project-id $STAGING_PROJECT --name staging-app
# Daily: Sync staging with production changeshoody containers sync $STAGING_ID
# Developers: Copy staging to personal envshoody containers copy $STAGING_ID --target-project-id $DEV_PROJECT --name alice-dev
# As needed: Sync dev envs with staginghoody containers sync $ALICE_DEV_ID// Weekly: Copy production to stagingconst staging = await client.api.containers.copy(PROD_ID, { target_project_id: STAGING_PROJECT, name: 'staging-app'});
// Daily: Sync staging with production changesawait client.api.containers.sync(staging.data.id);
// Developers: Copy staging to personal envsconst aliceDev = await client.api.containers.copy(staging.data.id, { target_project_id: DEV_PROJECT, name: 'alice-dev'});
// As needed: Sync dev envs with stagingawait client.api.containers.sync(aliceDev.data.id);# Weekly: Copy production to stagingcurl -X POST "https://api.hoody.com/api/v1/containers/{prod_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"target_project_id": "{staging_project}", "name": "staging-app"}'
# Daily: Sync staging with production changescurl -X POST "https://api.hoody.com/api/v1/containers/{staging_id}/sync" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Developers: Copy staging to personal envscurl -X POST "https://api.hoody.com/api/v1/containers/{staging_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"target_project_id": "{dev_project}", "name": "alice-dev"}'
# As needed: Sync dev envs with stagingcurl -X POST "https://api.hoody.com/api/v1/containers/{alice_dev_id}/sync" \ -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
Chains the same two operations twice: copy once to create a level, sync that level whenever its source moves ahead. Run each copy link once and reuse the matching sync link on whatever cadence fits.
# Copy production to staging
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/PROD_ID/copy&method=POST&bearer_token=TOKEN&json={"target_project_id":"STAGING_PROJECT_ID","name":"staging-app"}&response=transparent
# Sync staging with production
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/STAGING_ID/sync&method=POST&bearer_token=TOKEN&response=transparent
# Copy staging to a personal dev environment
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/STAGING_ID/copy&method=POST&bearer_token=TOKEN&json={"target_project_id":"DEV_PROJECT_ID","name":"alice-dev"}&response=transparent
# Sync the dev environment with staging
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/ALICE_DEV_ID/sync&method=POST&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.
Updates cascade down the chain.
Multi-region deployment
Section titled “Multi-region deployment”# Primary in UScontainer: prod-us
# Copy to EUPOST /api/v1/containers/{prod_us_id}/copy{ "target_project_id": "{project}", "target_server_id": "{eu_server}", "name": "prod-eu"}
# Copy to AsiaPOST /api/v1/containers/{prod_us_id}/copy{ "target_project_id": "{project}", "target_server_id": "{asia_server}", "name": "prod-asia"}
# After US deployment, sync all regionsPOST /api/v1/containers/{prod_eu_id}/syncPOST /api/v1/containers/{prod_asia_id}/syncCopy and sync handle the global rollout.
Snapshot, copy, deploy
Section titled “Snapshot, copy, deploy”# 1. Snapshot tested containerhoody snapshots create -c $TEST_ID --alias ready-for-prod
# 2. Copy to production from that snapshot# (the sanitized alias supplied at creation is the snapshot's `name`)hoody containers copy $TEST_ID --target-project-id $PROD_PROJECT --name prod-v2 --source-snapshot ready-for-prod
# 3. Re-point alias to new container (delete + recreate)hoody proxy delete $ALIAS_ID --yes# For program "http"/"https", --port is the port your app listens on inside the containerhoody proxy create --container-id $NEW_PROD_ID --program http --port $APP_PORT --alias prod --target-path /// 1. Snapshot tested containerawait client.api.containers.createSnapshot(TEST_ID, { alias: 'ready-for-prod'});
// 2. Copy to production from that snapshot// (the sanitized alias supplied at creation is the snapshot's `name`)const prod = await client.api.containers.copy(TEST_ID, { target_project_id: PROD_PROJECT, name: 'prod-v2', source_snapshot: 'ready-for-prod'});
// 3. Re-point alias to new container (delete + recreate)await client.api.proxyAliases.delete(ALIAS_ID);await client.api.proxyAliases.create({ container_id: prod.data.id, alias: 'prod', program: 'http', // For program 'http'/'https', `port` is the port your app listens on inside the container port: APP_PORT, target_path: '/'});# 1. Snapshot tested containercurl -X POST "https://api.hoody.com/api/v1/containers/{test_id}/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "ready-for-prod"}'
# 2. Copy to production from that snapshot# (the sanitized alias supplied at creation is the snapshot's "name")curl -X POST "https://api.hoody.com/api/v1/containers/{test_id}/copy" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "target_project_id": "{prod_project}", "name": "prod-v2", "source_snapshot": "ready-for-prod" }'
# 3. Re-point alias to new container (delete + recreate)# For program "http"/"https", "port" is the port your app listens on inside# the container; substitute your own belowcurl -X DELETE "https://api.hoody.com/api/v1/proxy/aliases/{alias_id}" \ -H "Authorization: Bearer $HOODY_TOKEN"
curl -X POST "https://api.hoody.com/api/v1/proxy/aliases" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "container_id": "{new_prod_id}", "alias": "prod", "program": "http", "port": 3000, "target_path": "/" }'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
Runs the four steps in order: snapshot, copy from it, then delete and recreate the alias, since Hoody has no atomic re-point. Substitute your own port for 3000 if your app listens elsewhere.
# Snapshot the tested container
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/TEST_ID/snapshots&method=POST&bearer_token=TOKEN&json={"alias":"ready-for-prod"}&response=transparent
# Copy to production from that snapshot
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/TEST_ID/copy&method=POST&bearer_token=TOKEN&json={"target_project_id":"PROD_PROJECT_ID","name":"prod-v2","source_snapshot":"ready-for-prod"}&response=transparent
# Delete the old alias
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/proxy/aliases/ALIAS_ID&method=DELETE&bearer_token=TOKEN&response=transparent
# Create the alias on the new container
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/proxy/aliases&method=POST&bearer_token=TOKEN&json={"container_id":"NEW_PROD_ID","alias":"prod","program":"http","port":3000,"target_path":"/"}&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.
Deployments become reproducible from known-good snapshots.
Best practices
Section titled “Best practices”Use unique SSH keys
Section titled “Use unique SSH keys”# Correct - generate new key for copyPOST /api/v1/containers/{source_id}/copy{ "target_project_id": "{project}", "name": "copy", # Omit ssh_public_key → auto-generated unique key}
# Or provide different keyPOST /api/v1/containers/{source_id}/copy{ "target_project_id": "{project}", "name": "copy", "ssh_public_key": "ssh-rsa DIFFERENT_KEY..."}Never reuse an SSH key across containers.
Snapshot before a heavy sync
Section titled “Snapshot before a heavy sync”# Before syncing with major changes:
# 1. Snapshot copy's current statecurl -X POST "https://api.hoody.com/api/v1/containers/{copy_id}/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "before-sync"}'
# 2. Perform synccurl -X POST "https://api.hoody.com/api/v1/containers/{copy_id}/sync" \ -H "Authorization: Bearer $HOODY_TOKEN"
# 3. If sync breaks something, restore using the exact name returned by create/listcurl -X PUT "https://api.hoody.com/api/v1/containers/{copy_id}/snapshots/before-sync" \ -H "Authorization: Bearer $HOODY_TOKEN"Document copy relationships
Section titled “Document copy relationships”production: container_id: 890abcdef12345678901cdef server: node-us copies: - name: staging-api container_id: 01bcdef123456789abcdef012 project: staging-project sync_schedule: daily - name: prod-eu-replica container_id: 12cdef123456789abcdef0123 project: production-project server: node-eu sync_schedule: weeklyTrack which containers are copies, and on what schedule they sync.
Copy from stable snapshots
Section titled “Copy from stable snapshots”# Instead of copying current state (might be broken):
# 1. Identify stable snapshotGET /api/v1/containers/{source_id}/snapshots# Read its exact "name" field
# 2. Copy from that snapshotPOST /api/v1/containers/{source_id}/copy{ "target_project_id": "{project}", "name": "guaranteed-stable", "source_snapshot": "{snapshot_name_from_list}"}Environments rebuilt this way always start from a known-good state.
Verify the source exists before sync
Section titled “Verify the source exists before sync”# Check source still existscurl "https://api.hoody.com/api/v1/containers/{source_id}" \ -H "Authorization: Bearer $HOODY_TOKEN"
# If 404: Cannot sync (orphaned copy)# Need to delete and re-copy from different sourceUseful questions
Section titled “Useful questions”Can I copy a container to a different user’s account?
Section titled “Can I copy a container to a different user’s account?”No. Copies must be within your own projects. To share containers with others, use storage shares or export/import workflows.
What happens if the source container is deleted?
Section titled “What happens if the source container is deleted?”The copy keeps running independently; it is a separate container. You can no longer sync it, though: the relationship is broken and the copy is orphaned.
Can I sync multiple times?
Section titled “Can I sync multiple times?”Yes. Sync as often as you need. Each sync is incremental, transferring only what changed since the last one. A common pattern is a daily sync from production to staging.
Does a copy include the source’s snapshots?
Section titled “Does a copy include the source’s snapshots?”A copy duplicates the source’s current filesystem state, or the specific source_snapshot you choose. The source’s previous snapshot history is not carried into the copy; the new container starts its own snapshot timeline. Take fresh snapshots on the copy once it is running.
Can I change both project and server in one copy?
Section titled “Can I change both project and server in one copy?”Yes:
POST /api/v1/containers/{source}/copy{ "target_project_id": "{different_project}", "target_server_id": "{different_server}", "name": "cross-everything-copy"}Both parameters can differ from the source.
Do proxy aliases get copied?
Section titled “Do proxy aliases get copied?”No. Proxy aliases are separate configuration, not part of container state. After copying, create new aliases for the copy or update existing aliases to point to it.
Can I sync in reverse (copy → source)?
Section titled “Can I sync in reverse (copy → source)?”No. Sync runs one way, source to copy. To move changes the other way, transfer the data by hand or promote the copy to be the new source and stop using the old one.
What if the copy has local changes when I sync?
Section titled “What if the copy has local changes when I sync?”Sync applies the source’s changes to the copy. Data added independently on the copy is not preserved where it conflicts with the source: conflicting local changes are overwritten. If the copy holds changes you need, snapshot it before syncing, or propagate those changes to the source first.
How much does copy and sync cost?
Section titled “How much does copy and sync cost?”A copy costs the same as creating a new container: storage and compute. A sync costs bandwidth for the changed data only. Both use a source snapshot as the transfer mechanism.
Troubleshooting
Section titled “Troubleshooting”Copy operation fails
Section titled “Copy operation fails”Problem: Copy returns error or stays in “copying” state
Solutions:
-
Check source container exists and is accessible:
Terminal window GET /api/v1/containers/{source_id}# Should return 200, not 404 -
Verify target project exists:
Terminal window GET /api/v1/projects/{target_project_id} -
Check target server has capacity:
Terminal window GET /api/v1/servers/{target_server_id}# Verify enough resources -
If cross-server, check network connectivity:
- Network issues between servers can stall copy
- Contact support if persistent
Sync fails with 409
Section titled “Sync fails with 409”Problem: Sync operation returns 409 Conflict
Cause: Container is not a copy (no source_container_id), or the container is not in a valid state for sync
Solutions:
-
Verify container is a copy:
Terminal window GET /api/v1/containers/{container_id}# Check: source_container_id is not null -
Verify source still exists:
Terminal window GET /api/v1/containers/{source_container_id}# Should return 200 -
If source deleted:
- Copy is orphaned
- Cannot sync anymore
- Option A: Use copy as new source
- Option B: Create new copy from different source
Copy is slower than expected
Section titled “Copy is slower than expected”Problem: Copy taking very long
Typical times:
- Same server, 50 GB: ~3-5 minutes
- Cross-server, 50 GB: ~10-15 minutes
If much slower:
-
Check container size:
Terminal window GET /api/v1/containers/{source_id}# Check: container filesystem usage# Larger containers = longer copy time -
Cross-server copies are slower:
- Network transfer adds significant time
- 100+ GB containers can take 30+ minutes
-
Server load:
- High server load slows operations
- Try during off-peak hours
Sync does not update the copy
Section titled “Sync does not update the copy”Problem: Sync completes but copy still has old data
Possible causes:
-
Source hasn’t changed:
Terminal window GET /api/v1/containers/{source_id}# Check updated_at timestamp# If old, source hasn't been modified -
Sync transferred but copy not restarted:
Terminal window # Restart copy to apply changesPOST /api/v1/containers/{copy_id}/restart -
Changes in copy override sync:
- If copy has local modifications, check carefully
- Sync should overwrite but verify data is updated
What’s next
Section titled “What’s next”Container duplication:
- Snapshots → - Source for copy operations
- Images → - Template containers from images
- Create, Edit, Delete → - Container fundamentals
Use copies with:
- Proxy Aliases → - Route different aliases to copies
- Storage Shares → - Share data between copies
- Managing → - Operate copies independently
What this page covered:
- A copy is a complete, independent duplicate
- A sync updates a copy with the source’s changes
- Copies can cross projects and servers
- SSH keys must be unique per container
- Sync is incremental, so it is faster than a re-copy
source_container_idtracks the copy relationship- Copies survive deletion of the source, as orphans
Try it out
Section titled “Try it out”Copy and synchronize containers from your browser:
Copy a container - omit target_server_id to use same server as source
Path Parameters
Authentication
Use the authentication widget in the header to login or set an API token
Authentication: Authorization: Bearer header (auto-attached for trusted domains)
Custom Headers
Request Body (JSON)
Sync a copied container with its source - only works for containers created via copy
Path Parameters
Authentication
Use the authentication widget in the header to login or set an API token
Authentication: Authorization: Bearer header (auto-attached for trusted domains)