# Install with Docker Compose

> Run the official Frame server image with Docker Compose: behind Caddy with automatic HTTPS, through a Cloudflare Tunnel with no open ports, with built-in HTTPS, or behind your own proxy.

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

If you already use Docker, or don't want to touch systemd. The official image `ghcr.io/crow-tech-pte-ltd/frame-server:0` is built for amd64 and arm64, so it also runs on a Raspberry Pi with a 64-bit OS. The `:0` tag follows 0.x releases.

## Get the files

Install Docker Engine with the Compose plugin, from your distribution's packages or [Docker's official instructions](https://docs.docker.com/engine/install/). Then get the repository and create the config:

```sh
git clone https://github.com/Crow-Tech-Pte-Ltd/frame.git
cd frame/deploy
cp frame.conf.example frame.conf
chmod 600 frame.conf
```

Every `docker compose` command below runs in `frame/deploy`. In every setup, set `FRAME_PUBLIC_URL=https://frame.example.com` (your address) and `FRAME_NAME` in `frame.conf`; the Caddy file sets `FRAME_PUBLIC_URL` for you. Then pick one way to make the server reachable.

## With Caddy (automatic HTTPS)

Caddy sits in front of Frame and gets HTTPS certificates for your domain. Use this on a server with a public IP address.

1. **Point your domain at the server**

   At your DNS provider, add an `A` record (and optionally `AAAA`) for `frame` with the server's IP address. On Cloudflare DNS, set it to **DNS only**. Open ports 80 and 443 (TCP), plus 443/UDP for HTTP/3.

2. **Write the config**

   ```sh
   nano frame.conf          # set FRAME_NAME; leave FRAME_DOMAIN unset
   echo "FRAME_HOSTNAME=frame.example.com" > .env
   ```

   `docker-compose.caddy.yml` sets two things for you from `FRAME_HOSTNAME`: `FRAME_PUBLIC_URL=https://<FRAME_HOSTNAME>`, and `FRAME_TRUSTED_PROXIES` set to the Compose network Caddy lives on (`172.31.250.0/24`, pinned in the file), so Frame sees each visitor's IP instead of Caddy's.

3. **Start it**

   ```sh
   docker compose -f docker-compose.caddy.yml up -d     # pulls the official image
   docker compose -f docker-compose.caddy.yml logs -f frame
   ```

   Open `https://frame.example.com/healthz`. It should say `ok`. Caddy health-checks Frame through the same path. Then [create your admin account](https://crow.sg/apps/frame/docs/first-run/).

> [!NOTE] Keep passing the Caddy file
> Every `docker compose` command for this setup needs `-f docker-compose.caddy.yml`, including the admin, backup and update commands in these docs. In the Caddy setup the Frame container publishes no ports: only Caddy can reach it.

## With Cloudflare Tunnel

Run Frame at home with no open ports and no router changes. This also works behind carrier-grade NAT. You need a domain whose DNS is on Cloudflare (the free plan is fine).

1. **Create the tunnel**

   In the Cloudflare dashboard, open **Zero Trust → Networks → Tunnels → Create a tunnel**, choose **Cloudflared** and name it `frame`. Copy the tunnel token: it is a secret. Add a public hostname `frame.example.com` whose service is `http://frame:8080`.

2. **Write the config**

   ```sh
   nano frame.conf          # FRAME_PUBLIC_URL=https://frame.example.com and FRAME_NAME
   echo "CLOUDFLARE_TUNNEL_TOKEN=<your tunnel token>" > .env
   ```

3. **Switch docker-compose.yml to the tunnel**

   In `docker-compose.yml`:

   - remove the `ports:` block of the `frame` service;
   - uncomment the `cloudflared` service;
   - uncomment the line marked (b), `FRAME_TRUSTED_PROXIES: "cloudflare,172.31.251.0/24"`. Frame then trusts the visitor IP only when it comes from the tunnel container.

4. **Start it**

   ```sh
   docker compose up -d                 # pulls the official image
   docker compose logs -f frame
   ```

   On your phone, on mobile data, open `https://frame.example.com/healthz`. It should say `ok`.

> [!WARNING] Good to know
> Don't put Cloudflare Access or another 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, which is above Frame's 50 MB default. Cloudflare ends the HTTPS connection, so it can technically see the traffic; if that matters to you, use built-in HTTPS or Caddy instead.

## With built-in HTTPS

Frame can get its own Let's Encrypt certificate. Set `FRAME_DOMAIN=frame.example.com` and `FRAME_PUBLIC_URL=https://frame.example.com` in `frame.conf`, and in `docker-compose.yml` replace the `127.0.0.1:8080:8080` port line with `80:80` and `443:443`. DNS and firewall are as in the Caddy setup above. Leave `FRAME_TRUSTED_PROXIES` unset: phones connect to Frame directly. Then `docker compose up -d`.

## Behind your own proxy on the host

If nginx, Caddy or another proxy already runs on the host and forwards to `127.0.0.1:8080`: keep the `127.0.0.1:8080:8080` port line and uncomment the line marked (d), `FRAME_TRUSTED_PROXIES: "172.31.251.1/32"`. Docker delivers those connections from the network's gateway address. Your proxy needs the settings in [Behind Caddy, nginx or another proxy](https://crow.sg/apps/frame/docs/install-systemd/#reverse-proxy).

The compose file pins its Docker network to `172.31.251.0/24` so these lines can name it. If that collides with a network you already use, change it in the file and in the lines above.

## Admin commands in Docker

Find the one-time admin setup link, and run any admin command, through the container (add `-f docker-compose.caddy.yml` after `docker compose` for the Caddy setup):

```sh
docker compose logs frame | grep "admin account"
docker compose exec frame frame-server admin setup-link     # a fresh link, if needed
docker compose exec frame frame-server admin user list
docker compose exec frame frame-server admin user create --email you@example.com --name "You"
```

Every command is in the [configuration reference](https://crow.sg/apps/frame/docs/configuration/#commands).

## Build the image yourself

Both setups pull the official image. To build it from the repository instead (from `deploy/Dockerfile`), add the `docker-compose.build.yml` override:

```sh
# Cloudflare Tunnel, built-in HTTPS or host proxy (docker-compose.yml)
echo "COMPOSE_FILE=docker-compose.yml:docker-compose.build.yml" >> .env
docker compose up -d --build

# Caddy
docker compose -f docker-compose.caddy.yml -f docker-compose.build.yml up -d --build
```

With the `COMPOSE_FILE` line in `deploy/.env`, later `docker compose` commands use your own build too. With the Caddy file, keep passing both `-f` options.

## What the container does

The image is distroless: no shell and no package manager. The container runs as an unprivileged user (uid 65532) on a read-only filesystem, with no Linux capabilities and `no-new-privileges`. Everything Frame stores (database, local media and `secrets.json`) lives in the Docker volume `frame_frame-data`: back it up ([Backups & restore](https://crow.sg/apps/frame/docs/backups/)). There is no Docker health check, because the image has no shell or curl: monitor `https://frame.example.com/healthz` from outside, or use the Caddy setup, which checks it for you.
