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:
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:
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:
// /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:
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:
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:
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.
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.
Reload nginx
After any change under /etc/nginx/sites-iwmwn/, run:
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:
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.
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:
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.
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.