Docs

Deploying your code

Get your code onto Fortly from the console, the API or a GitHub branch, and know what happens between the upload and the new release going live.

Updated October 12, 2026
On this page
  1. Ways to deploy
  2. Deploy an archive
  3. Deploy from a GitHub repository
  4. What happens when you deploy
  5. Node servers and ports
  6. The start command
  7. Limits
  8. Related

Your code reaches Fortly as an archive of your project: a file you upload in the console, a gzipped tar you send to the API, or a commit Fortly fetches from a linked GitHub branch. Every deploy becomes a release. Fortly reads the files, builds them when they need building, publishes the result, and only then moves your project's traffic onto it. Until that last step, the release that was already live keeps serving.

Ways to deploy

SourceHow it arrivesBuilt?
Console uploadA .zip or .tar.gz you drop on the project's Deployments pageWhen its dependencies are missing
API uploadA gzipped tar in the body of POST /projects/{name}/deploysWhen its dependencies are missing
GitHubThe linked branch's commit, fetched by Fortly on a push or when you askAlways — a repository never carries node_modules

All three take the same path once the bytes arrive: the same limits, the same build rules and the same release history. How builds work explains when a build runs and what it does.

Deploy an archive

Open the project's deployments

In the console, open your project and go to Deployments, then choose Upload.

Drop your project

Drop a .zip or .tar.gz (up to 256 MB) on the upload area, or use Choose file. A .zip is repacked as a .tar.gz in your browser before it is sent. The console reads the archive first and shows the framework, runtime and package manager it found, so you can catch the wrong folder before anything is uploaded.

Deploy it

Choose Deploy release. The progress shows each step as it happens: packing, uploading, building (only when a build is needed) and going live.

Pack the directory's contents, not the directory: tar -C ./my-app -czf release.tgz . puts package.json at the root of the archive, which is where Fortly looks for it.

Deploy from a GitHub repository

Link a repository and a branch once, and every push to that branch becomes a release.

Connect GitHub

On the project's Deployments page, choose GitHub, then Connect GitHub. GitHub asks you to grant the repo scope. Fortly uses it to read archives of the repositories you link and to manage their webhook.

Pick a repository, check the Branch, and leave Deploy on every push ticked if you want pushes deployed automatically. Choose Link repository.

Deploy the branch

Choose Deploy this branch for the first release. After that, with Deploy on every push ticked, each push to the branch deploys the commit that was pushed.

Pushes to other branches, tag pushes and branch deletions are ignored. Fortly fetches the exact commit that was pushed, so the release records the commit it was built from.

Automatic deploys need a webhook on the repository, and creating one needs admin access to it. Without that, linking still works: the console says Automatic deploys could not be set up, and you deploy with the button instead.

Once a repository is linked, you can also deploy its branch from the API:

Terminal
curl -X POST https://api.fortly.com/projects/my-app-ada/github/deploy \
  -H "Authorization: Bearer $FORTLY_TOKEN"

A project with no linked repository answers 428 with this project is not pointed at a repository.

GET/projects/{name}/githubThe linked repository and branch, if any
POST/projects/{name}/githubLink a repository: owner, repo, branch, and auto (defaults to true)
DELETE/projects/{name}/githubUnlink it and remove the webhook
POST/projects/{name}/github/deployDeploy the branch as it is now

What happens when you deploy

Each step is narrated in the project's log, under the system source, so you can read a deploy back afterwards in Logs.

Unpack

The archive is unpacked into a fresh workspace and checked against the limits below. The log says Unpacked 214 files · 3.4 MB.

Read the project

Fortly reads the files to find the runtime, the framework and the package manager, and logs what it found, for example Node.js · Next.js · npm. Nothing is guessed: where your files say nothing, the field stays empty.

Build, if it needs building

A project whose dependencies are missing is installed and built by Fortly's build service. A project that arrived with its dependencies, or a folder of static files, skips this step. See How builds work.

Publish and validate

The output is uploaded to storage and read back. A static site is served straight from storage. A server-rendered app or a Node server runs as a Worker on Fortly's runtime. A release that fails validation is not published.

Go live

The new release becomes live and the previous one is marked superseded, in one step. Cached responses for your project are cleared, and the log ends with Deployment ready.

If anything fails along the way, the new release does not go live and the release that was already live keeps serving. A failed build is recorded on the release as failed, with its reason. When the install or build step itself fails, the log says Deploy failed. The release that was already live is still live.

Only one release can build at a time per project. Deploying while another is building is refused with 409 and a release is already being built for this project — wait for it to finish.

Node servers and ports

An Express, Koa, NestJS or Fastify app does not run as a separate process with a port of its own. Fortly builds it into a Worker and takes over the HTTP server your code starts, matching it by the port number you pass to listen().

That means:

  • Call listen() with a port number. A fixed number works, and so does process.env.PORT with a numeric fallback, such as app.listen(process.env.PORT || 3000).
  • The last server you start is the one that answers. If you start a separate health or metrics server first, start your app's own server last.
  • Port 0 and Unix sockets cannot be used. There is no port number to match, and requests fail with an error that says so.
  • A server that never calls listen() cannot be served. Its requests fail with this application never called listen().

Express, Koa and NestJS apps boot on their first request after a deploy, not at deploy time. Hono and Elysia apps export a fetch handler instead of starting a server, so they need no port at all.

PORT is a reserved name: you cannot set an environment value called PORT yourself. See Environment and secrets.

The start command

Fortly reads how a project starts from its files, in this order: the start script in package.json, then the file named by main, then server.js, index.js, app.js or main.js, and finally npm start (or your package manager's equivalent). A folder with an index.html and no manifest is served as a static site and needs no command.

When the files cannot answer, you name one:

  • Per deploy, with the x-fortly-cmd header on POST /projects/{name}/deploys or POST /projects/{name}/github/deploy.
  • Once for the project, by sending {"cmd": "node server.js"} to PATCH /projects/{name}. An empty string clears it and hands the question back to your files. A saved command cannot contain shell syntax — pipes, &&, redirects, quotes or a VAR=value prefix — because it is not run through a shell.

A command you send takes precedence over the one Fortly reads, and the command is recorded on the release. A deploy where neither you nor your files name a command is refused with 400:

this project has no start command yet, and what was deployed is nothing this node recognises — send one as x-fortly-cmd

The command does not decide how a new release is served. A static site is served from its files, and a Worker from what the build produced; for a Node server, the file that is run is chosen during the build, and How builds work lists the order.

Limits

LimitValueWhat happens past it
Archive size256 MBRefused with 413
Unpacked size2 GBthat upload expands to more than 2 GB
Files in one release50,000that upload has more than 50000 files
Builds at onceOne per project409, see above

The archive is checked as it is unpacked:

  • A path that would land outside the project, such as ../ or an absolute path, refuses the whole archive: the archive contains a path that escapes the project.
  • Symbolic links and hard links are skipped, not followed.
  • An archive with no files is refused: that archive has no files in it.
Something missing or unclear? Tell us and we will fix the page.© 2026 Fortly