Frame app icon: the Crow Tech crow standing on a green camera
Documentation

Run your own Frame server

Host Frame for you and your friends. One small program, one data folder, and an address your friends type into the app.

What you need

  • A Linux machine that stays on. A small virtual server, a home computer or a Raspberry Pi 3B+, 4, 5 or Zero 2 W. One CPU core and 512 MB to 1 GB of RAM are enough; Frame itself uses about 30–60 MB.
  • Some disk. Ten friends sending five photos a day need about 8 GB a year. Videos need more. Media can also go to an S3-compatible bucket such as Cloudflare R2.
  • A domain name, such as frame.example.com. The apps need HTTPS, and HTTPS needs a name. A subdomain of a domain you already own works.
  • Docker with Compose on an amd64 or arm64 machine for recipes 1 and 2 (on a Raspberry Pi, a 64-bit OS), or systemd on an amd64, arm64 or armv7 machine for recipe 3.

Always set FRAME_PUBLIC_URL

Set it to the address people type into the app, such as FRAME_PUBLIC_URL=https://frame.example.com. Sign in with Apple and Google only work when it is set, because each sign-in is bound to your server's name. Frame also uses it in invite links. Every recipe below sets it.

Recipe 1: Docker Compose with Caddy

Caddy sits in front of Frame and gets HTTPS certificates for your domain automatically. 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. Get the files and write the config

    Terminal
    git clone https://github.com/Crow-Tech-Pte-Ltd/frame.gitcd frame/deploycp frame.conf.example frame.confnano frame.conf          # set FRAME_NAME; leave FRAME_DOMAIN unsetecho "FRAME_HOSTNAME=frame.example.com" > .env

    The Caddy compose file sets FRAME_PUBLIC_URL and FRAME_TRUSTED_PROXIES for you from FRAME_HOSTNAME.

  3. Start it

    Terminal
    docker compose -f docker-compose.caddy.yml up -d     # pulls the official imagedocker compose -f docker-compose.caddy.yml logs -f frame

    Open https://frame.example.com/healthz. It should say ok. Then create your admin account (first run).

Using the Caddy file

Every docker compose command for this recipe needs -f docker-compose.caddy.yml, including the admin, backup and update commands below. The image is ghcr.io/crow-tech-pte-ltd/frame-server:0, for amd64 and arm64.

Recipe 2: Docker 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. Get the files and write the config

    Terminal
    git clone https://github.com/Crow-Tech-Pte-Ltd/frame.gitcd frame/deploycp frame.conf.example frame.confnano frame.conf          # FRAME_PUBLIC_URL=https://frame.example.com and FRAME_NAMEecho "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

    Terminal
    docker compose up -d                 # pulls the official imagedocker compose logs -f frame

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

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 ends the HTTPS connection, so it can technically see the traffic. If that matters to you, use recipe 1 or 3.

Prefer to build the image yourself?

Both Docker recipes pull the official image. To build it from the repository instead, add the docker-compose.build.yml override:

Terminal
# recipe 2echo "COMPOSE_FILE=docker-compose.yml:docker-compose.build.yml" >> .envdocker compose up -d --build # recipe 1docker 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 recipe 1, keep passing both -f options.

Recipe 3: one-line installer with systemd

For Debian, Ubuntu, Raspberry Pi OS, Fedora and other Linux systems with systemd, on amd64, arm64 or armv7. Frame can get its own HTTPS certificate, so you don't need another web server.

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

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

    Terminal
    sudo apt install -y ufw          # Debian, Ubuntu, Raspberry Pi OSsudo ufw allow OpenSSHsudo ufw allow 80,443/tcpsudo ufw enable
  2. Download the installer, read it, run it

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

    The installer picks the release for your CPU and checks its SHA-256 checksum against the release's SHA256SUMS before installing anything. It installs frame-server, the frame-admin helper and a locked-down systemd service, and creates /etc/frame/frame.conf. It never touches your data in /var/lib/frame.

  3. Configure it

    Open sudo nano /etc/frame/frame.conf, remove the # and fill in:

    /etc/frame/frame.conf
    FRAME_DOMAIN=frame.example.comFRAME_PUBLIC_URL=https://frame.example.com[email protected]FRAME_NAME="Our Frame"
  4. Start it

    Terminal
    sudo sh install.sh --reconfigure     # lets Frame use ports 80/443 because FRAME_DOMAIN is setsudo systemctl start framesystemctl status frame               # should say "active (running)"

    Open https://frame.example.com/healthz. It should say ok.

Behind your own proxy or tunnel

Don't set FRAME_DOMAIN. Set FRAME_LISTEN=127.0.0.1:8080, FRAME_PUBLIC_URL, and FRAME_TRUSTED_PROXIES to match: cloudflare,127.0.0.1/32 for cloudflared on the same machine, or 127.0.0.1/32,::1/128 for Caddy or nginx on the same machine.

First run: create your admin account

On its first start, the server writes a one-time link to its log:

Output
Create your admin account: https://frame.example.com/admin/setup#t=…
  • systemd
    sudo journalctl -u frame | grep "admin account"
  • Docker
    docker compose logs frame | grep "admin account"
  • Fresh link
    sudo frame-admin setup-link or docker compose exec frame frame-server admin setup-link

With the Caddy recipe, add -f docker-compose.caddy.yml after docker compose. Open the link and choose your name, a username, your e-mail or phone number and a password. That is your normal Frame account and also the admin of the web panel at /admin/. The link works once and expires after 24 hours.

Then:

  • sign in to the Frame app with the same details;
  • in the panel, open Security and turn on two-factor authentication;
  • open Invites → New invite to get a link and QR code for each friend (one use within 7 days by default).

Keep the admin panel private

By default the panel is served at /adminon the same address as the app. If you don't need it on the internet, serve it only on a local address and reach it over SSH:

frame.conf
FRAME_ADMIN_LISTEN=127.0.0.1:8081

The main address then answers /admin with 404. From your laptop:

Terminal
ssh -N -L 8081:127.0.0.1:8081 you@your-server

Then open http://localhost:8081/admin/. With Docker, publish the admin port on the host's loopback only, with ports: ["127.0.0.1:8081:8081"] and FRAME_ADMIN_LISTEN=:8081.

Other options: FRAME_ADMIN=local answers only to this machine and your trusted proxy networks, and FRAME_ADMIN=off turns the panel off (the command line still works). Keep FRAME_PUBLIC_URLset, so invite links made in the panel don't point at localhost.

Key settings

Settings live in /etc/frame/frame.conf (systemd) or deploy/frame.conf (Docker), one KEY=value per line. Restart after a change: sudo systemctl restart frame or docker compose up -d. The server name, who can join and the upload limits can also be changed in the admin panel, unless they are set in the file.

  • FRAME_PUBLIC_URL
    The address people type into the app. Needed for Apple and Google sign-in and used in invite links. Several names? Separate them with commas, main one first.
  • FRAME_NAME
    The server name shown in the app.
  • FRAME_REGISTRATION
    invite (default): new accounts need an invite. open: anyone who knows the address. closed: only admins create accounts.
  • FRAME_USER_INVITES
    On invite-only servers, whether members' invite links from the app can bring in new people (default true).
  • FRAME_AUTH_METHODS
    Sign-in methods offered: any of email, phone, apple, google (default: all four).
  • FRAME_PUSH_RELAY
    Push relay for notifications. Default https://frame.crow.sg; set it empty to turn notifications off.
  • FRAME_SMTP_URL, FRAME_SMTP_FROM
    Send password-reset e-mails. Without them, admins reset passwords in the panel.
  • FRAME_TRUSTED_PROXIES
    Proxies whose visitor-IP headers Frame trusts, so rate limits see real visitors. Leave unset when phones connect directly.
  • FRAME_STORAGE=s3, FRAME_S3_*
    Store media in Cloudflare R2 or another S3 bucket. Files are encrypted before upload by default.

For e-mail password resets, for example:

frame.conf
FRAME_SMTP_URL='smtps://user:[email protected]:465'[email protected]

Keep frame.conf private: it can contain passwords and keys. Every setting is described in CONFIG.md.

Notifications and the push relay

Apple and Google only deliver notifications signed with the app publisher's keys, so your server hands them to Crow Tech's relay at frame.crow.sg. Each phone makes its own key and shares it only with your server, so the relay, Apple and Google only see encrypted data. The relay stores nothing and never logs tokens, contents or IP addresses.

To turn notifications off, set FRAME_PUSH_RELAY= (empty) and restart. The apps still work: they update live while open, and widgets refresh about every 15 minutes.

If notifications don't arrive:

Terminal
curl -sI https://frame.crow.sg/healthz      # can the server reach the relay?journalctl -u frame | grep "push failed"    # or: docker compose logs frame | grep "push failed"

Backups

Back up three things, and copy them off the server:

  • The database, made with backup
    Accounts, friendships, moments and messages.
  • secrets.json
    Signing keys and the key that encrypts S3/R2 media. Lose it and that media can't be read.
  • media/ (local storage)
    The photos and videos.

systemd

Terminal
sudo frame-admin backup /root/frame-$(date +%F).dbsudo tar -C /var/lib/frame -czf /root/frame-files-$(date +%F).tgz secrets.json media

Docker

Terminal
docker compose exec frame frame-server backup /data/frame-backup.dbdocker compose cp frame:/data/frame-backup.db ./frame-$(date +%F).dbdocker run --rm -v frame_frame-data:/data:ro -v "$PWD":/backup alpine \  tar -C /data -czf /backup/frame-files-$(date +%F).tgz secrets.json media

The database backup is consistent and safe to take while Frame runs. Run it nightly, for example with cron, and try a restore once. Restoring is covered in the full guide.

Updates

Back up first. Database changes run automatically when the new version starts.

systemd

Terminal
curl -fsSLO https://github.com/Crow-Tech-Pte-Ltd/frame/releases/latest/download/install.shless install.shsudo sh install.sh                     # or: sudo sh install.sh --version v1.2.3

The installer checks the checksum, replaces the program and restarts the service. Your configuration and data are untouched.

Docker

Terminal
cd frame && git pull                      # updates the compose filescd deploy && docker compose pull && docker compose up -d

If you build the image yourself, run docker compose up -d --build after git pull instead. The :0 tag follows 0.x releases; pin an exact version in the compose file if you only want to upgrade by hand. To hear about new releases, use Watch → Custom → Releases on the GitHub repository.

Connect the app

  1. Open Frame. When it asks Where's your Frame?, type your address, such as frame.example.com, without https://.
  2. Sign in with the account you made with the setup link.
  3. Invite friends from the app, or from Invitesin the admin panel. With an invite from a member, they're friends as soon as they join.

If the app can't connect, open https://frame.example.com/api/v1/serverin your phone's browser. You should see your server's name. The apps only accept plain http:// for local network addresses, so anything on the internet needs HTTPS.

Full documentation

Whoever runs a server can see what is stored on it. Your friends are trusting you with their photos: keep the server updated and backed up.

WhatsApp