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.
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
| Source | How it arrives | Built? |
|---|---|---|
| Console upload | A .zip or .tar.gz you drop on the project's Deployments page | When its dependencies are missing |
| API upload | A gzipped tar in the body of POST /projects/{name}/deploys | When its dependencies are missing |
| GitHub | The linked branch's commit, fetched by Fortly on a push or when you ask | Always — 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 you want deployed and send the archive as the raw request body. The body is the archive itself, not a multipart form.
API routes take the project's full name, normally the name you chose followed by your account's: a project you called my-app is my-app-ada on an account named ada. GET /projects returns the exact one as name on each project. The examples here use my-app-ada.
tar -C ./my-app -czf release.tgz .
curl -X POST https://api.fortly.com/projects/my-app-ada/deploys \
-H "Authorization: Bearer $FORTLY_TOKEN" \
--data-binary @release.tgzThe status line tells you whether the site has changed yet:
| Status | Meaning |
|---|---|
201 Created | Nothing needed building. The release is already live. |
202 Accepted | The release is being built. The reply carries "building": true, and the build continues after the request ends. |
Both replies carry deploy (the release record, including its id and state) and url (your project's address). To follow a 202 to the end, read the release list until its state leaves building:
curl -s https://api.fortly.com/projects/my-app-ada/deploys \
-H "Authorization: Bearer $FORTLY_TOKEN" | jq '.deploys[0] | {id, state, failure}'The full request and response reference is in Deploys API.
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.
Link a repository and branch
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:
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.
/ projects/ {name}/ githubThe linked repository and branch, if any/ projects/ {name}/ githubLink a repository: owner, repo, branch, and auto (defaults to true)/ projects/ {name}/ githubUnlink it and remove the webhook/ projects/ {name}/ github/ deployDeploy the branch as it is nowWhat 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 doesprocess.env.PORTwith a numeric fallback, such asapp.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
0and 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 withthis 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-cmdheader onPOST /projects/{name}/deploysorPOST /projects/{name}/github/deploy. - Once for the project, by sending
{"cmd": "node server.js"}toPATCH /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 aVAR=valueprefix — 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-cmdThe 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
| Limit | Value | What happens past it |
|---|---|---|
| Archive size | 256 MB | Refused with 413 |
| Unpacked size | 2 GB | that upload expands to more than 2 GB |
| Files in one release | 50,000 | that upload has more than 50000 files |
| Builds at once | One per project | 409, 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.