Frame docs · Install
Install with Docker Compose
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. Then get the repository and create the config:
git clone https://github.com/Crow-Tech-Pte-Ltd/frame.gitcd frame/deploycp frame.conf.example frame.confchmod 600 frame.confEvery 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.
Step 1: Point your domain at the server
At your DNS provider, add an
Arecord (and optionallyAAAA) forframewith 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.Step 2: Write the config
Terminalnano frame.conf # set FRAME_NAME; leave FRAME_DOMAIN unsetecho "FRAME_HOSTNAME=frame.example.com" > .envdocker-compose.caddy.ymlsets two things for you fromFRAME_HOSTNAME:FRAME_PUBLIC_URL=https://<FRAME_HOSTNAME>, andFRAME_TRUSTED_PROXIESset 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.Step 3: Start it
Terminaldocker compose -f docker-compose.caddy.yml up -d # pulls the official imagedocker compose -f docker-compose.caddy.yml logs -f frameOpen
https://frame.example.com/healthz. It should sayok. Caddy health-checks Frame through the same path. Then create your admin account.
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).
Step 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 hostnameframe.example.comwhose service ishttp://frame:8080.Step 2: Write the config
Terminalnano frame.conf # FRAME_PUBLIC_URL=https://frame.example.com and FRAME_NAMEecho "CLOUDFLARE_TUNNEL_TOKEN=<your tunnel token>" > .envStep 3: Switch docker-compose.yml to the tunnel
In
docker-compose.yml:- remove the
ports:block of theframeservice; - uncomment the
cloudflaredservice; - 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.
- remove the
Step 4: Start it
Terminaldocker compose up -d # pulls the official imagedocker compose logs -f frameOn your phone, on mobile data, open
https://frame.example.com/healthz. It should sayok.
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.
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):
docker compose logs frame | grep "admin account"docker compose exec frame frame-server admin setup-link # a fresh link, if neededdocker compose exec frame frame-server admin user listdocker compose exec frame frame-server admin user create --email [email protected] --name "You"Every command is in the configuration reference.
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:
# Cloudflare Tunnel, built-in HTTPS or host proxy (docker-compose.yml)echo "COMPOSE_FILE=docker-compose.yml:docker-compose.build.yml" >> .envdocker compose up -d --build # Caddydocker compose -f docker-compose.caddy.yml -f docker-compose.build.yml up -d --buildWith 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). 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.
For AI tools: this page as Markdown, the page index and all pages in one file.