Install the server
Stavo runs on your hardware, not our cloud. The server is one container plus Postgres. Anything that runs docker compose works: Synology, Unraid, TrueNAS, a Raspberry Pi 4 or 5, an old Mac mini, a VPS.
The short version
No account here, no key, no card. The server and the web app are free forever, for everyone on it.
- Save the compose file and a .env in a folder, then run
docker compose up -d. - Put HTTPS in front (Caddy or a Cloudflare Tunnel) and open your address. The first visit asks you to create the admin account.
- Invite your people from the dashboard.
- Want the native apps? Connect them. They run a 30 day trial on every device, and a license key unlocks them for good.
What you need
- Docker with the compose plugin.
- 2 GB of RAM is comfortable for a household. Add more if you turn on smart search on the server.
- Disk space for your files, on the machine or on a NAS share.
- A domain or subdomain if you want to use the apps and share links outside your home.
1. docker compose
Make a folder (say ~/stavo) and save this as docker-compose.yml. The server image works on both Intel and ARM machines.
name: stavo
services:
server:
image: git.ronniie.com/nulldaily/stavo:${STAVO_VERSION:-latest}
restart: unless-stopped
env_file: .env
environment:
DATABASE_URL: postgres://stavo:${POSTGRES_PASSWORD}@db:5432/stavo
MEDIA_DIR: /data/media
STAVO_UPDATER_URL: http://watchtower:8080
STAVO_UPDATER_TOKEN: ${WATCHTOWER_HTTP_API_TOKEN:-}
labels:
com.centurylinklabs.watchtower.enable: "true"
ports:
- "127.0.0.1:8710:8710"
volumes:
- media:/data/media
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: stavo
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: stavo
volumes:
- db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U stavo"]
interval: 5s
retries: 20
# Only acts when you press "Update now" in Stavo. Never polls on its own.
watchtower:
image: nickfedor/watchtower:1.22.3@sha256:2f8f2a9bd21a195b3b650409532a20f6aada8ef5834f00d0d7a999dee0779a81
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
WATCHTOWER_HTTP_API_UPDATE: "true"
WATCHTOWER_HTTP_API_PERIODIC_POLLS: "false"
WATCHTOWER_HTTP_API_TOKEN: ${WATCHTOWER_HTTP_API_TOKEN}
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_CLEANUP: "true"
labels:
com.centurylinklabs.watchtower.enable: "false"
volumes:
db:
media:2. .env
Next to it, a .env file. These are the ones that matter:
# Where people reach your server
PUBLIC_URL=https://drive.example.com
# openssl rand -hex 32
SECRET=
POSTGRES_PASSWORD=pick-something-long
# openssl rand -hex 32 (turns on one click updates)
WATCHTOWER_HTTP_API_TOKEN=
# Only when your files live on a NAS share (see "Store files on your NAS")
NAS_HOST=192.168.1.20
NAS_SHARE_PATH=Media/stavo
NAS_USER=stavo
NAS_PASSWORD=docker compose up -d
docker compose logs -f serverThe server listens on 127.0.0.1:8710, on purpose only on the machine itself. Put HTTPS in front of it (next step). To try it out on the same machine first, just open http://localhost:8710.
3. Store files on your NAS
Docker runs on the NAS itself? Point the media volume at a folder on it instead of the named volume:
volumes:
- /volume1/stavo:/data/media # a folder on the NAS itselfDocker runs on another box? Mount an SMB share from the NAS as the media volume. Replace the volumes: block at the bottom with this, and create the folder on the NAS first:
volumes:
db:
media:
driver: local
driver_opts:
type: cifs
device: "//${NAS_HOST}/${NAS_SHARE_PATH}"
o: "username=${NAS_USER},password=${NAS_PASSWORD},vers=3.0,uid=1000,gid=1000,file_mode=0664,dir_mode=0775,noserverino,soft"Keep Postgres on a local volume (the db volume above), never on a network share. Databases and network shares don't get along.
4. HTTPS: Caddy or a Cloudflare Tunnel
Caddy gets you HTTPS certificates on its own. Point your DNS at the box and use:
drive.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8710
request_body {
max_size 2GB
}
}
# Optional: old short links keep working
share.example.com {
reverse_proxy 127.0.0.1:8710
}No open ports or static IP? Use a Cloudflare Tunnel: cloudflared tunnel create stavo, route your hostname to it, and point it at the server:
# ~/.cloudflared/config.yml
tunnel: <your-tunnel-id>
credentials-file: /home/you/.cloudflared/<your-tunnel-id>.json
ingress:
- hostname: drive.example.com
service: http://localhost:8710
- hostname: share.example.com # optional, redirects to the main domain
service: http://localhost:8710
- service: http_status:404Cloudflare's free plan caps a single request at 100 MB. Stavo uploads big files in chunks, so that's fine for the apps and the web app, but a single upload from ShareX or another uploader over 100 MB will fail through a tunnel.
Got an old short link domain like share.example.com? Point it at the same server and add it to LEGACY_HOSTS in .env. Stavo redirects it, so old links keep working.
5. Connect the apps
Stavo for Mac and for iPhone and iPad are coming soon. Everything below works in the browser today; this is how the apps will connect once they ship.
Install Stavo for Mac or for iPhone and iPad, type your server address (like https://drive.example.com) and approve the connection in the browser window that opens. No tokens to copy. Each person signs in with their own account on your server.
6. Add a license (optional)
The server never needs a key. To unlock the native apps after their trial, paste a Family or Community key in Dashboard → Instance → Apps license, or let people add their own Personal key in Profile → Your Stavo apps. How licensing works with your server.
7. Add an AI provider (optional)
In Dashboard → Instance → Assistant, pick a provider and paste its API key: Claude (recommended), OpenAI, Google Gemini, Ollama or any OpenAI-compatible server. That becomes the default for everyone; people can add their own in Profile → Assistant. More about the Assistant.
Updating
Your server checks for new versions every few hours and shows the release notes in Dashboard → Instance → Updates. With WATCHTOWER_HTTP_API_TOKEN set, Update now backs up the database, pulls the new image and restarts the server. The Watchtower container only acts when you press that button and only touches the Stavo server container.
Prefer doing it by hand? Take a backup, then:
docker compose pull
docker compose up -dWant to stay on a version? Set STAVO_VERSION=1.0.0 in .env. Every release is on the changelog.
Backups
Two things hold your data: the media folder (your files) and the Postgres database (everything about them).
- Files: back up the media folder or NAS share with whatever you already use (Hyper Backup, restic, Time Machine, a USB disk). It's plain files in folders.
- Database: one click updates already save a dump to
.backups/in your media folder. For regular backups, run this on a schedule:
# Database (small, do this often)
docker compose exec -T db pg_dump -U stavo stavo | gzip > stavo-db-$(date +%F).sql.gz
# Restore into a fresh database
gunzip -c stavo-db-2026-11-01.sql.gz | docker compose exec -T db psql -U stavo stavoEach person can also download everything they own from Profile → Your data, in open formats.
Optional extras
- Smart search on the server: an extra container that indexes every photo for search and people. Skip it on low power boxes; the iPhone app indexes on the phone instead.
- Code terminal: a sandboxed container for the terminal in Code and the Assistant's commands. No Docker socket, no network by default. Off until you turn it on.
Both are in the server's full compose file at git.ronniie.com/nulldaily/stavo, with the steps in its README.