Deploying from a ZIP
Uploading source directly, the limits an archive has to stay inside, and why an archive is refused whole.
A ZIP upload needs nothing connected anywhere — no repository, no key, no webhook. It is the fastest way to prove the platform works, and it stays useful for anything not in Git.
Upload
On the application, upload a .zip of your source. POST /deployments is
multipart/form-data — the only non-JSON request body in the API. The upload lands in the
control plane's staging directory (DEPLOY_UPLOAD_STAGING_PATH, default
/var/lib/sthora/uploads) and is then moved to the managed server by the driver.
Limits
| Limit | Default | Setting |
|---|---|---|
| Archive size | 200 MB | DEPLOY_MAX_ARCHIVE_BYTES |
| Entry count | 20,000 | DEPLOY_MAX_ARCHIVE_ENTRIES |
| Extracted size | 2 GB | DEPLOY_MAX_EXTRACTED_BYTES |
An archive past any of these is refused whole, not partially extracted. A half-extracted zip bomb is still a full disk.
What extraction refuses
Extraction rejects the entire archive on the first entry that:
- resolves outside its destination directory,
- carries an absolute path, or
- is a symlink pointing outside the tree.
Why the whole archive
Zip-slip is the first thing anyone will try on a deployment platform. Refusing
one entry and continuing leaves a tree that is partly the attacker's and looks
like a successful upload; refusing the archive is the only outcome that is
easy to reason about. The guard lives in archive-entry-path.ts and has its
own regression suite.
Archives are opened with decodeStrings: false, so the platform's own path guard decides how
an entry name is interpreted rather than the zip library.
Then
Detection runs exactly as it does for a Git clone — see build types. Nothing downstream of source materialisation knows or cares where the tree came from.