What the deploy zip holds

The deploy zip is the artifact a deploy installs and runs: the contents of your application's folder, the folder whose top holds package.json. The turnzero-cloud command builds it from the folder its deploy line runs in. The platform's image build installs what it declares and starts it with npm start on Node.js 24.

What the zip contains

The zip contains:

  • the backend as ordinary Node.js code exposing one server that honors PORT;
  • the web client's built static assets, where there is one;
  • the migrations/ folder, where the application has a database;
  • the lib/ folder with the library packages the application declares as file: dependencies;
  • manifest.json, the project's copy, optional, which no deploy runs under, because the platform keeps the one submit_manifest recorded. A copy that differs gets manifest_notice, a warning that the deploy runs under the recorded manifest, listing the members that differ. The notice also lists each lib/<name>/package.json whose turnzero.entry and version match no entry of the recorded packages, and each the deploy left unread or could not compare, even without manifest.json in the zip. A manifest.json copy larger than 1 MiB or nested deeper than 64 levels is refused (below);
  • the package manifests, package.json and its lockfile.

A file the list does not name, such as a test/ folder, is still deployed: the image build copies it into the image unchanged, and it runs only where your code loads it.

The zip's root is the folder containing package.json and its lockfile. Where there is anything to install, the image build runs npm install --omit=dev there and links the copied library packages among the dependencies. It then runs npm start, so the start script is the entry point.

What the command leaves out

The command leaves out every node_modules and .git entry and every .env and .env.* file, wherever it stands and whatever the case of its name. So dependencies and environment files stay on your machine. It leaves out nothing else, because a file left out may be one your code loads at run time.

hash --list lists each file the zip would hold, and hash and deploy print the paths left out. They warn of a .zip file at the folder's top, which the zip includes, so keep a build's zip elsewhere. They also warn of a file that looks like a key or a credential. What it prints gives those lines.

The two project layouts

The command zips the folder it runs in, so the folder you run its lines in decides what the zip holds:

  • With an app/ folder. Where the application sits in app/ beside the project's library folder, system/, run the command's lines in app/, so the zip contains the contents of app/ alone and leaves system/ out.
  • With no app/ folder. In a project with no app/ folder, system/ sits beside package.json and rides the zip. The image build installs from package.json and the copies under lib/, never from system/.

Either way, the zip includes the application's own copy of each package under lib/, which package.json declares as a file: dependency (Use the library).

Build a database-backed service lays out such a project, shown here without the contents of system/. The zip contains the contents of app/ and leaves out node_modules, .git, and every .env and .env.* file wherever it stands, the .env file the provision line writes for a local run among them. app/ is the zip's root, containing package.json and its lockfile, manifest.json, and src/main.mjs, the entry its start script runs:

notes-service/
  system/                      the library entries as taken, never zipped
  app/                         the zip's root
    package.json
    package-lock.json
    manifest.json
    .gitignore
    AGENTS.md
    migrations/0001_notes.sql
    src/db.mjs
    src/server.mjs
    src/main.mjs
    test/notes.test.mjs
    lib/database/              the package's package.json and lib/, copied from system/

Names and limits

Every entry of the zip is a relative path inside its root, with / between its segments, and a folder entry such as zip -r writes becomes a folder. A deploy refuses an entry that is absolute, contains a backslash or a NUL, names a drive, or leaves the root through .., since the image build cannot write it.

A deploy refuses a zip naming one path as both a file and a folder the same way, and a name longer than 4,096 bytes or deeper than 128 segments. Each is refused artifact_layout_invalid, with a detail naming the entry, and for a backslash the tar.exe command of step 3 of Deploy an application. So is a zip of more than 65,535 entries, 500,000 name segments in all, or 128 MiB declared, its folder entries counted, and one whose root manifest.json nests deeper than 64 levels.

Bytes that are not a zip are refused artifact_unreadable. A zip with no package.json at its root, or whose package.json is not a JSON object, is refused artifact_layout_invalid. So is one with neither a start script nor a root server.js, since npm start then has nothing to run, and one whose root package.json or manifest.json is larger than 1 MiB. Where exactly one top-level folder of the zip contains a package.json, the detail names that folder: zip its contents instead.

A zip whose root package.json or manifest.json, or a nested package.json the deploy reads, inflates past the size its entry declares is refused artifact_unreadable too. The image build stops at any entry that does, naming it.

The deploy reads the zip before it adds a version, so none of these refusals counts against the day's deploys. Windows PowerShell's Compress-Archive writes \ between segments, which these rules refuse, so a zip you build yourself on Windows comes from tar.exe.

What the image build installs

The install runs your runtime dependencies alone. Development dependencies are never installed, whatever your .npmrc says. Your .npmrc still reaches the install for everything else, such as a private registry.

The image build removes the install scripts, such as postinstall and prepare, from your root package.json and from each workspace member's, one whose folder your root package.json's workspaces match. The image contains those copies. Every other dependency runs its own install scripts as npm runs them, a lib/ package declared as a file: dependency among them. The shell those scripts run under is the build's own.

A workspaces pattern may use literal segments, * within a segment, and ** segments, with each ! pattern after every other and none matching a positive pattern's own text. A * never matches a folder name's leading ., and a ** never passes through a folder whose name opens with ., so packages/* and ** skip .cache. A segment that spells the dot itself, such as .c*, still matches it.

A deploy refuses any other pattern by name, such as one using ?, braces, brackets, parentheses, a backslash, a leading #, or a .. segment. It also refuses a pattern longer than 4,096 bytes or deeper than 128 segments, the bound on an entry's name, and more than 256 patterns or 5,000,000 matching steps.

The image build skips npm install where it would install and run nothing: package.json declares no dependencies other than devDependencies, and no workspaces. The skip also needs a zip root containing no node_modules or .npmrc, and a package.json with no os, cpu, libc, or devEngines field, which npm's install checks. A skipped install changes nothing else: the image still starts with npm start.

Native modules

No binding.gyp of the root's or of a workspace member's is built, so such a native module ships prebuilt. A deploy refuses each one that ships no .node file, as artifact_layout_invalid, naming the entry. The root's ships its .node anywhere in the zip outside node_modules and the members' folders. A member's ships it in its own folder, less any member nested inside it. A lib/ package's binding.gyp builds on the platform's Linux builders, as npm builds it.

Build each .node file your zip ships for Linux, the platform's target. The deploy checks only that it exists; one built for Windows or macOS fails when your application loads it.

The deploy's notice

A deploy returns artifact_notice, listing each install script and binding.gyp of the root's or a workspace member's that the build will not run or rebuild. The deploy reads a package.json below the root only up to 64 KiB, and at most 50 of them. The notice lists each workspace member's it left unread, which the build strips all the same. Where one builds something your application needs, run it locally and ship its output in the zip.