# Install with the Linux installer (systemd)

> Install the Frame server with its installer on Debian, Ubuntu, Raspberry Pi OS, Fedora or any Linux with systemd: built-in HTTPS on a VPS, a Pi behind a Cloudflare Tunnel, or behind Caddy or nginx.

Source: https://crow.sg/apps/frame/docs/install-systemd/

For Debian, Ubuntu, Raspberry Pi OS, Fedora and other Linux systems with systemd, on amd64, arm64 or armv7. The installer sets up `frame-server` as a locked-down service; how the server is reached depends on your machine.

## What the installer does

Download it, read it, then run it:

```sh
curl -fsSLO https://github.com/Crow-Tech-Pte-Ltd/frame/releases/latest/download/install.sh
less install.sh                   # read it (press q to quit)
sudo sh install.sh
```

It does this, and nothing else:

1. picks the release binary for this CPU (amd64, arm64 or armv7);
2. downloads it plus `SHA256SUMS` from the GitHub release and **verifies the SHA-256 checksum** before installing anything;
3. installs `/usr/local/bin/frame-server`, the `/usr/local/bin/frame-admin` helper and `/etc/systemd/system/frame.service`;
4. creates `/etc/frame/frame.conf` (mode 0600) from the example on the first run; an existing config is never touched;
5. lets the service bind ports 80 and 443 only if `FRAME_DOMAIN` is set;
6. enables the service, and on upgrades restarts it if it was running.

Re-running it is safe. Data in `/var/lib/frame` is never touched. Use `sudo frame-admin …` for admin commands: it runs them as the service's own user.

## On a VPS with built-in HTTPS

The simplest always-on setup: Frame gets and renews its own certificate from Let's Encrypt. The [quickstart](https://crow.sg/apps/frame/docs/quickstart/) walks through it with every command.

1. **Point your domain at the server and open the firewall**

   Add an `A` record for `frame` with the server's IP (DNS only on Cloudflare). Then allow SSH and web traffic, and nothing else:

   ```sh
   sudo apt install -y ufw          # Debian, Ubuntu, Raspberry Pi OS
   sudo ufw allow OpenSSH
   sudo ufw allow 80,443/tcp
   sudo ufw enable
   ```

2. **Install, then configure**

   Run the installer as above, then `sudo nano /etc/frame/frame.conf`, remove the `#` and fill in:

   ```ini title="/etc/frame/frame.conf"
   FRAME_DOMAIN=frame.example.com
   FRAME_PUBLIC_URL=https://frame.example.com
   FRAME_ACME_EMAIL=you@example.com
   FRAME_NAME="Our Frame"
   ```

   Leave `FRAME_TRUSTED_PROXIES` unset: phones connect to Frame directly, with no proxy in between.

3. **Start it**

   ```sh
   sudo sh install.sh --reconfigure     # lets Frame use ports 80/443 because FRAME_DOMAIN is set
   sudo systemctl start frame
   systemctl status frame               # should say "active (running)"
   journalctl -u frame -f               # live logs; Ctrl+C to stop watching
   ```

   Open `https://frame.example.com/healthz`. It should say `ok`. Then [create your admin account](https://crow.sg/apps/frame/docs/first-run/).

## At home with Cloudflare Tunnel

Run Frame on a Raspberry Pi 3B+, 4, 5 or Zero 2 W, or any Linux PC, with **no open ports and no router configuration**. This also works behind carrier-grade NAT. You need a 64-bit OS (such as Raspberry Pi OS Lite 64-bit, Debian or Ubuntu) and a domain whose DNS is on Cloudflare: adding a domain to Cloudflare's free plan is free. Prefer an SSD or USB stick for the data over the SD card, which wears out, or keep media in [R2](https://crow.sg/apps/frame/docs/storage/).

1. **Install and configure Frame**

   Update the machine and run the installer as above; skip the firewall step, nothing needs to be opened. Then `sudo nano /etc/frame/frame.conf`:

   ```ini title="/etc/frame/frame.conf"
   FRAME_LISTEN=127.0.0.1:8080
   FRAME_PUBLIC_URL=https://frame.example.com
   FRAME_TRUSTED_PROXIES=cloudflare,127.0.0.1/32
   FRAME_NAME="Our Frame"
   ```

   `FRAME_LISTEN=127.0.0.1:8080` means only programs on this machine, such as the tunnel, can reach the server. `FRAME_TRUSTED_PROXIES=cloudflare,127.0.0.1/32` makes Frame trust the visitor IP that Cloudflare passes along (the `CF-Connecting-IP` header), but only from `cloudflared` on this machine: rate limits then apply per visitor, and nobody else can fake the header. Don't set `FRAME_DOMAIN` here.

2. **Start it**

   ```sh
   sudo systemctl start frame
   curl http://127.0.0.1:8080/healthz   # → ok
   ```

3. **Create the tunnel**

   In the Cloudflare dashboard, go to **Zero Trust → Networks → Tunnels → Create a tunnel**, choose **Cloudflared** and name it `frame`. Choose your OS and architecture (Debian, arm64 for a Pi). Cloudflare shows two commands: one installs `cloudflared` from Cloudflare's package repository, and one is `sudo cloudflared service install <token>`. Run both on your machine; the token is a secret. Under **Public Hostname**, set subdomain `frame`, your domain, service type **HTTP** and URL `localhost:8080`, and save.

4. **Test it**

   From your phone, on mobile data rather than Wi-Fi, open `https://frame.example.com/healthz`. It should show `ok`. Then [create your admin account](https://crow.sg/apps/frame/docs/first-run/).

> [!WARNING] Good to know
> Don't put Cloudflare Access (a login page) in front of this hostname: the apps can't get through it. Cloudflare's free plan limits uploads to 100 MB per request; Frame's default limit is 50 MB, so that's fine. Cloudflare terminates TLS, so it can technically see the traffic. If that bothers you, use built-in HTTPS on a VPS.

## Behind Caddy, nginx or another proxy

If the machine already serves other websites, let its web server handle HTTPS. Install Frame as above, but don't set `FRAME_DOMAIN`:

```ini title="/etc/frame/frame.conf"
FRAME_LISTEN=127.0.0.1:8080
FRAME_PUBLIC_URL=https://frame.example.com
FRAME_TRUSTED_PROXIES=127.0.0.1/32,::1/128
```

`FRAME_TRUSTED_PROXIES` tells Frame that the `X-Forwarded-For` header from the proxy on this machine carries the real visitor IP. For [Caddy](https://caddyserver.com/docs/install), add this to `/etc/caddy/Caddyfile` (the commented version is [`deploy/Caddyfile`](https://github.com/Crow-Tech-Pte-Ltd/frame/blob/main/deploy/Caddyfile)), then run `sudo systemctl reload caddy` and `sudo systemctl start frame`:

```caddy title="/etc/caddy/Caddyfile"
frame.example.com {
	request_body {
		max_size 64MB
	}
	reverse_proxy 127.0.0.1:8080 {
		flush_interval -1
		health_uri /healthz
	}
}
```

Using nginx or another proxy? You need four things:

- Pass `Host` and `X-Forwarded-For`.
- Allow request bodies of about 64 MB (nginx: `client_max_body_size 64m;`).
- Don't buffer `/api/v1/events`, which is a Server-Sent Events stream (nginx: `proxy_buffering off;` and `proxy_read_timeout 1h;`).
- Add the proxy's IP to `FRAME_TRUSTED_PROXIES`.

## Installer options

| Option | What it does |
|---|---|
| `--version vX.Y.Z` | Install that release instead of the latest. |
| `--binary PATH` | Install a locally built `frame-server` (no download); the unit, admin helper and example config come from the script's directory, a repository checkout's `deploy/`. See [Binary & source](https://crow.sg/apps/frame/docs/install-binary/#build). |
| `--start` | Start the service even on the first install. |
| `--reconfigure` | Only re-check `FRAME_DOMAIN`, reload and restart. Run it after setting or removing `FRAME_DOMAIN`. |
| `--uninstall` | Remove the binary, unit and helper; keeps the configuration in `/etc/frame` and the data. |

The environment variable `FRAME_REPO` (default `Crow-Tech-Pte-Ltd/frame`) points the installer at a fork.

## Useful commands

```sh
systemctl status frame             # running?
journalctl -u frame -n 100         # last 100 log lines
sudo systemctl restart frame       # after editing frame.conf
sudo frame-admin user list         # admin commands: always through frame-admin
```

> [!WARNING] Always use `sudo frame-admin`
> Running `frame-server` directly as root creates database files the service can't write. If that happened, see [Troubleshooting](https://crow.sg/apps/frame/docs/troubleshooting/#permission-denied).

The service runs with a throwaway system user that can write only its data directory, a read-only view of the system and no capabilities beyond binding ports 80/443 when `FRAME_DOMAIN` needs them. Details in [Security](https://crow.sg/apps/frame/docs/security/#hardening).
