Skip to content
Hoody.com

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.


This page explains the concepts behind copy and sync. The endpoint reference carries the full request and response schemas.

Copy operations:

Sync operations:

Related:


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.


Get the exact production state for testing:

POST Copy production to staging
/api/v1/containers/{container_id}/copy
Click "Run" to execute the request

Staging now matches production: same code, same data, same configuration. Test updates there before deploying to production.

Every developer gets an identical setup:

POST Copy template for new team member
/api/v1/containers/{container_id}/copy
Click "Run" to execute the request

One template produces any number of developer environments, each starting from the same filesystem, packages, and configuration.

Redundancy across geographic regions:

POST Copy to different geographic region
/api/v1/containers/{container_id}/copy
Click "Run" to execute the request

If the US server fails: the EU backup is already provisioned and can take over.

Duplicate for parallel testing:

Terminal window
# Copy to test different approaches
POST /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.


POST Copy a container
/api/v1/containers/{container_id}/copy
Click "Run" to execute the request

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.

Duplicate into a different project:

POST Copy to different project
/api/v1/containers/{container_id}/copy
Click "Run" to execute the request

Use cases:

  • Client demo environments
  • Separate staging/production projects
  • Team member personal projects
  • Experiment isolation

Duplicate to a different geographic location:

POST Copy to different server
/api/v1/containers/{container_id}/copy
Click "Run" to execute the request

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.

Use a known-good snapshot as the source:

POST Copy from specific snapshot
/api/v1/containers/{container_id}/copy
Click "Run" to execute the request

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.


A copy must use a different SSH key from its source.

Terminal window
# Omit ssh_public_key → auto-generated
POST /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.

Terminal window
# Provide your own public key
POST /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 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.

POST Sync copy with source container
/api/v1/containers/{container_id}/sync
Click "Run" to execute the request

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 only works if:

  1. The container was created by a copy operation, so it has a source_container_id
  2. The source container still exists
  3. You have access to the source container

If the source is deleted: the copy is orphaned and can no longer 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.


Terminal window
# Day 1: Create staging from production
hoody containers copy $PROD_ID --target-project-id $STAGING_PROJECT --name staging-api
# Week 1: Sync staging to get production fixes
hoody containers sync $STAGING_COPY_ID
# Week 2: Sync again (incremental, fast)
hoody containers sync $STAGING_COPY_ID

Staging stays current without a full re-copy.

Terminal window
# New team member Alice joins - copy template
hoody containers copy $TEMPLATE_ID --target-project-id $ALICE_PROJECT --name alice-dev-env
# Template gets updated - sync Alice's environment
hoody containers sync $ALICE_COPY_ID
# Another developer Bob joins
hoody containers copy $TEMPLATE_ID --target-project-id $BOB_PROJECT --name bob-dev-env

One template keeps the whole team synchronized.

Terminal window
# Copy production to EU for redundancy
hoody 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 changes
hoody containers sync $PROD_EU_ID

If the US datacenter fails: switch to the EU replica with a proxy alias.

Terminal window
# Copy production to test new feature
curl -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 it
POST /api/v1/containers/{feature_copy_id}/snapshots
{"alias": "feature-auth-v2-complete"}
# Copy this feature container to production
POST /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.


1. Copy initiated (status: copying)
2. Source snapshot created (if needed)
3. Snapshot transferred to target server
4. New container provisioned from snapshot
5. SSH keys generated/configured
6. Copy complete and started (status: running)

Track progress:

Terminal window
# Check copy status
curl "https://api.hoody.com/api/v1/containers/{new_copy_id}" \
-H "Authorization: Bearer $HOODY_TOKEN"
# Watch for: "status": "copying" → "status": "running"

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: false
  • copy_network_rules - Copy the source’s network rules/settings to the copy. Default: false

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

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
POST Sync copy with source
/api/v1/containers/{container_id}/sync
Click "Run" to execute the request

What happens:

  1. Source’s latest snapshot is captured
  2. Changed files identified (incremental diff)
  3. Changes transferred to copy
  4. Copy’s filesystem updated
  5. Copy restarted (if was running)

Sync is incremental: only the changes are transferred, which is much faster than a full re-copy.

Terminal window
# Update existing copy
POST /api/v1/containers/{copy_id}/sync

Advantages:

  • 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

Prefer sync for regular updates. Re-copy only when you want to start fresh.


// 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.

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 updates
async 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 updates
syncTeamEnvironments(
'template_container_id',
['alice_copy_id', 'bob_copy_id', 'charlie_copy_id']
);

The whole team ends up on the same tools and configuration.

// Only sync if source has recent updates
async 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.


Check whether a container is a copy:

GET Check container copy status
/api/v1/containers/{container_id}
Click "Run" to execute the request

Response includes:

{
"data": {
"id": "01bcdef123456789abcdef012",
"source_container_id": "890abcdef12345678901cdef",
...
}
}

If source_container_id is not null, the container is a copy.

GET List all containers to find copies
/api/v1/containers
Click "Run" to execute the request

Parse the response for containers where source_container_id matches your source container ID.

You can then sync every copy programmatically.

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_id can sync)

Terminal window
# Weekly: Copy production to staging
hoody containers copy $PROD_ID --target-project-id $STAGING_PROJECT --name staging-app
# Daily: Sync staging with production changes
hoody containers sync $STAGING_ID
# Developers: Copy staging to personal envs
hoody containers copy $STAGING_ID --target-project-id $DEV_PROJECT --name alice-dev
# As needed: Sync dev envs with staging
hoody containers sync $ALICE_DEV_ID

Updates cascade down the chain.

Terminal window
# Primary in US
container: prod-us
# Copy to EU
POST /api/v1/containers/{prod_us_id}/copy
{
"target_project_id": "{project}",
"target_server_id": "{eu_server}",
"name": "prod-eu"
}
# Copy to Asia
POST /api/v1/containers/{prod_us_id}/copy
{
"target_project_id": "{project}",
"target_server_id": "{asia_server}",
"name": "prod-asia"
}
# After US deployment, sync all regions
POST /api/v1/containers/{prod_eu_id}/sync
POST /api/v1/containers/{prod_asia_id}/sync

Copy and sync handle the global rollout.

Terminal window
# 1. Snapshot tested container
hoody 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 container
hoody proxy create --container-id $NEW_PROD_ID --program http --port $APP_PORT --alias prod --target-path /

Deployments become reproducible from known-good snapshots.


Terminal window
# Correct - generate new key for copy
POST /api/v1/containers/{source_id}/copy
{
"target_project_id": "{project}",
"name": "copy",
# Omit ssh_public_key → auto-generated unique key
}
# Or provide different key
POST /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.

Terminal window
# Before syncing with major changes:
# 1. Snapshot copy's current state
curl -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 sync
curl -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/list
curl -X PUT "https://api.hoody.com/api/v1/containers/{copy_id}/snapshots/before-sync" \
-H "Authorization: Bearer $HOODY_TOKEN"
containers-mapping.yml
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: weekly

Track which containers are copies, and on what schedule they sync.

Terminal window
# Instead of copying current state (might be broken):
# 1. Identify stable snapshot
GET /api/v1/containers/{source_id}/snapshots
# Read its exact "name" field
# 2. Copy from that snapshot
POST /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.

Terminal window
# Check source still exists
curl "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 source

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.

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:

Terminal window
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.

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.

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.

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.


Problem: Copy returns error or stays in “copying” state

Solutions:

  1. Check source container exists and is accessible:

    Terminal window
    GET /api/v1/containers/{source_id}
    # Should return 200, not 404
  2. Verify target project exists:

    Terminal window
    GET /api/v1/projects/{target_project_id}
  3. Check target server has capacity:

    Terminal window
    GET /api/v1/servers/{target_server_id}
    # Verify enough resources
  4. If cross-server, check network connectivity:

    • Network issues between servers can stall copy
    • Contact support if persistent

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:

  1. Verify container is a copy:

    Terminal window
    GET /api/v1/containers/{container_id}
    # Check: source_container_id is not null
  2. Verify source still exists:

    Terminal window
    GET /api/v1/containers/{source_container_id}
    # Should return 200
  3. If source deleted:

    • Copy is orphaned
    • Cannot sync anymore
    • Option A: Use copy as new source
    • Option B: Create new copy from different source

Problem: Copy taking very long

Typical times:

  • Same server, 50 GB: ~3-5 minutes
  • Cross-server, 50 GB: ~10-15 minutes

If much slower:

  1. Check container size:

    Terminal window
    GET /api/v1/containers/{source_id}
    # Check: container filesystem usage
    # Larger containers = longer copy time
  2. Cross-server copies are slower:

    • Network transfer adds significant time
    • 100+ GB containers can take 30+ minutes
  3. Server load:

    • High server load slows operations
    • Try during off-peak hours

Problem: Sync completes but copy still has old data

Possible causes:

  1. Source hasn’t changed:

    Terminal window
    GET /api/v1/containers/{source_id}
    # Check updated_at timestamp
    # If old, source hasn't been modified
  2. Sync transferred but copy not restarted:

    Terminal window
    # Restart copy to apply changes
    POST /api/v1/containers/{copy_id}/restart
  3. Changes in copy override sync:

    • If copy has local modifications, check carefully
    • Sync should overwrite but verify data is updated

Container duplication:

  1. Snapshots → - Source for copy operations
  2. Images → - Template containers from images
  3. Create, Edit, Delete → - Container fundamentals

Use copies with:

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_id tracks the copy relationship
  • Copies survive deletion of the source, as orphans

Copy and synchronize containers from your browser:

Copy a container - omit target_server_id to use same server as source

Path Parameters

Authentication

Not Authenticated

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

Not Authenticated

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)