# Serve an app

> Serve a static site or a running app from a Rootsy computer through nginx on port 8090, deploy new versions safely, and the request limits that apply.

This page covers how a Rootsy computer serves the internet: static sites, apps behind a proxy, safe deploys and the request limits. Everything a computer serves goes through your own nginx, listening on port 8090. That holds for any app: a static site, a dynamic app, an API or a dashboard.

## How a request reaches your app

A visitor opens your computer's address (`https://<name>.rtsy.dev`) or one of your custom domains. Rootsy terminates HTTPS and forwards the request to the platform's nginx on port 8080. The platform's nginx passes it to your nginx on port 8090. Your nginx serves files or proxies to your app.

Nothing else is reachable from outside. An app listening on port 3000 can't be opened at `:3000`. It has to sit behind a server block on 8090. Only HTTP requests come in, so there's no way to expose SSH, a database port or raw TCP.

The `Host` header your nginx sees is the hostname the visitor used: your address or the custom domain. Use `server_name` to serve different apps on different hostnames. Requests for a hostname that no server block names go to the default server.

To print the address and any custom domains:

```sh
rootsy domains
```

## Serve a static site

The default server lives in `/etc/nginx/sites-iwmwn/000-default.conf`. It serves `~/sites/default` at your address, with `index.html` served for `/`. To change what's there, edit the files in `~/sites/default`.

To serve another static site on its own hostname, such as a custom domain you've connected, put the files in a directory under `~/sites/` and add a server block for it:

```sh
mkdir -p ~/sites/docs
sudo tee /etc/nginx/sites-iwmwn/docs.conf > /dev/null <<'EOF'
server {
  listen 8090;
  server_name docs.example.com;
  root /home/dev/sites/docs;
  index index.html;
  absolute_redirect off;
  location / { try_files $uri $uri/ =404; }
}
EOF
sudo nginx -t && sudo nginx -s reload
```

`absolute_redirect off` keeps nginx's own redirects (such as `/guide` to `/guide/`) relative, so they don't point visitors at port 8090.

## Proxy to an app

To serve a dynamic app, run it on a local port and proxy to it. Bind the app to `127.0.0.1` and pick a port the platform doesn't use. The platform uses 7681–7687, 8080 and 8081. This Node server listens on port 3000:

```js
// /home/dev/apps/hello/src/server.js
const http = require("node:http");

http
  .createServer((req, res) => {
    res.writeHead(200, { "content-type": "text/plain" });
    res.end(`Hello from ${req.headers.host}\n`);
  })
  .listen(3000, "127.0.0.1");
```

Serve from a copy of the source. Copy it into a release directory and point a `current` symlink at it, so a later deploy can build a new release and move the symlink:

```sh
mkdir -p ~/apps/hello/releases
cp -a ~/apps/hello/src ~/apps/hello/releases/v1
ln -sfn releases/v1 ~/apps/hello/current
```

Run the app as a systemd service, so it starts again when the computer boots:

```sh
sudo tee /etc/systemd/system/hello.service > /dev/null <<'EOF'
[Unit]
Description=hello app
After=network.target

[Service]
User=dev
WorkingDirectory=/home/dev/apps/hello/current
ExecStart=/usr/bin/node server.js
Restart=always
Slice=customer.slice

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now hello
```

`Slice=customer.slice` throttles the service if your work uses more than 80% of the computer's memory, so the platform's services keep room.

Then point nginx at it. To serve the app at your address in place of `~/sites/default`, name the address in `server_name`. First read the address from `rootsy domains`:

```sh
ADDRESS=$(rootsy domains | awk '/^site:/ { sub("https://", "", $2); print $2 }')
echo "$ADDRESS"
```

Check that `echo "$ADDRESS"` prints your address before running the `tee` command. An empty variable writes a broken config. If `nginx -t` fails, delete the file you wrote.

```sh
sudo tee /etc/nginx/sites-iwmwn/hello.conf > /dev/null <<EOF
server {
  listen 8090;
  server_name $ADDRESS;
  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host \$host;
  }
}
EOF
sudo nginx -t && sudo nginx -s reload
```

To serve it on a custom domain as well, add the domain to `server_name`.

`proxy_set_header Host $host` passes the visitor's hostname to your app. Without it, the app sees `127.0.0.1:3000`.

A process you start by hand in the terminal keeps serving until the computer restarts. Then it's gone, because a fresh start runs only what's enabled at boot. Keep sessions and other state on disk for the same reason. See [Sleep and wake](https://rootsy.io/docs/reference/sleep.md).

## Reload nginx

After any change under `/etc/nginx/sites-iwmwn/`, run:

```sh
sudo nginx -t && sudo nginx -s reload
```

`nginx -t` checks the whole configuration first. A reload with a broken file leaves the previous configuration running, but nginx won't start with it on the next boot. Breaking your own nginx only breaks what you serve. Don't edit the platform's nginx on port 8080 or anything under `/opt/platform`.

## Build, then switch

Never build inside the directory a running server serves from. An interrupted build deletes the last working output, and the app won't start on the next wake. Build each version in a fresh directory. Switch the `current` symlink to it only after the build succeeds. Replace `npm ci && npm run build` with your app's own build command:

```sh
cd ~/apps/hello
NEW=releases/$(date +%Y%m%d%H%M%S)
cp -a src "$NEW"
(cd "$NEW" && npm ci && npm run build) \
  && ln -sfn "$NEW" current.next \
  && mv -T current.next current \
  && sudo systemctl restart hello
```

`mv -T` replaces the `current` symlink in one step, so the server never sees a half-written directory. If the build fails, `current` still points at the last good version, and you can delete the failed release directory. For a static site, point nginx's `root` at a `current` symlink the same way. nginx serves the new files after the switch without a reload.

Long builds that spend their time waiting on the network, or run inside Docker, can be interrupted by sleep. Wrap them in `rootsy keep-awake -- <command>`. See [Jobs](https://rootsy.io/docs/reference/jobs.md).

## Uploads and request size

Rootsy accepts a request body of up to 1,048,576 bytes (1 MiB) on your address and custom domains. The limit covers the whole body. Multipart boundaries, part headers, filenames and other form fields all count, so a 1 MiB file sent as a form upload is over it. Headers don't count. Sending the body with `Transfer-Encoding: chunked` doesn't get around it.

Rootsy refuses a larger request before it reaches your nginx, usually with a `413`. A much larger upload can fail as a dropped connection instead. The `413` is an HTML error page with these headers:

```text
X-Rootsy-Error: request_body_too_large
X-Rootsy-Max-Request-Body-Bytes: 1048576
X-Rootsy-Upload-Guidance: Include multipart overhead. Send smaller requests or use application-level chunked uploads.
Cache-Control: no-store
```

Rootsy removes the three `X-Rootsy-` headers from your app's own responses. A `413` that carries them came from Rootsy. A `413` without them didn't come from Rootsy's limit. It usually came from your nginx or your app, and it reaches the visitor unchanged. Your nginx and app can set a lower limit. They can't raise this one, and Rootsy has no setting to raise it.

To accept larger files, have the browser send them in pieces of at most 524,288 bytes (512 KiB), each as the raw body of its own request, and join them on the server. Your app owns everything around that: authentication, quotas, retries, cleanup of abandoned uploads and checking the final file. Sending the same oversized request again won't succeed.

## What Reset does to your config

Reset keeps only `/home`, `/root` and `/srv`. It removes your nginx files in `/etc/nginx/sites-iwmwn/` and any services in `/etc/systemd/system/`. Keep a copy of each under `~` so you can put them back. See [Backups and Reset](https://rootsy.io/docs/reference/backups-and-reset.md).

## Limits

| What | Limit |
| --- | --- |
| Ports the platform uses | 7681–7687, 8080, 8081. Don't bind to them. 8090 is your nginx. |
| Protocols | HTTP requests only. WebSocket upgrades aren't passed through to your nginx. |
| Response headers for `GET` and `HEAD` | Must start within 15 seconds, including any wake. After that the visitor gets a 503 page titled "This website isn't responding". |
| Silence while answering | Your nginx's default `proxy_read_timeout` is 60 seconds. The platform's nginx gives up at 90. After either, the visitor gets a 503. To use the full 90 seconds, add `proxy_read_timeout 90s;` to your `location`. |
| Request body size | 1,048,576 bytes, counting multipart framing. Larger requests usually get 413. |
| Retries | Rootsy sends each request to your nginx once and never retries it. |
| Paths | `/_iw` and paths under `/_iw/` belong to the platform and never reach your app. |
| Status 502, 503, 504 from your app, or nginx down | Replaced with Rootsy's "Your web server config looks broken" page, status 503. |
| Outbound email | TCP port 25 is blocked. Use a provider's HTTPS API or SMTP submission on 587 or 465. |

Rootsy doesn't put a login in front of anything you serve. It's reachable at its address and custom domains until you add one. See [Protect an app](https://rootsy.io/docs/reference/protect-an-app.md).

---
Page: https://rootsy.io/docs/reference/serving/ · Updated: 2026-10-21 · All docs: https://rootsy.io/llms.txt
