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

Source: https://docs.fortly.com/deploy

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](https://docs.fortly.com/deploy/builds) explains when a build runs and what it does.

## Deploy an archive

**Console**

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

**API**

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

```bash
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.tgz
```

The 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`:

```bash
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](https://docs.fortly.com/api/deploys).

> **Tip:**
>
> 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:

```bash
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}/github` — The linked repository and branch, if any
- `POST /projects/{name}/github` — Link a repository: owner, repo, branch, and auto (defaults to true)
- `DELETE /projects/{name}/github` — Unlink it and remove the webhook
- `POST /projects/{name}/github/deploy` — Deploy 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](https://docs.fortly.com/deploy/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](https://docs.fortly.com/deploy/builds).

### 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.`

> **Note:**
>
> 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](https://docs.fortly.com/deploy/environment#names-fortly-reserves).

## 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`:

```text
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](https://docs.fortly.com/deploy/builds#node-servers) 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`.

## Related

- [How builds work](https://docs.fortly.com/deploy/builds): When Fortly installs and builds, what it can build, and how to read a failed build.
- [Releases and rollbacks](https://docs.fortly.com/deploy/releases): List releases and put an earlier one back without rebuilding it.
- [Environment and secrets](https://docs.fortly.com/deploy/environment): Values your build and your running app receive, encrypted at rest.
- [Deploys API](https://docs.fortly.com/api/deploys): Every deploy route, its parameters and its responses.
