Install Lumio on your own server.
For studios with their own infrastructure — a mini PC, a retired workstation, a rented dedicated server or your own VM. The stack is the same as everywhere; the on-premise twist is reachability and who takes care of backups (you).
What you need
Lumio is a plain Docker Compose stack and brings everything with it (PostgreSQL, Redis, MinIO for object storage, Caddy, the app itself). It runs on just about any Linux machine if these points line up:
- 64-bit Linux (amd64 or arm64) — Ubuntu 22.04 LTS or Debian 12 are typical
- Docker ≥ 24 with Compose v2 (
docker compose version) - RAM: photos only from 4 GB, with video 8 GB, AI tagging another +4 GB
- A domain and a plan for how the server is reachable (next section)
- Storage for the image data — ideally mirrored (RAID) or on a NAS
First decide — reachability & TLS
This is where on-premise differs from a cloud server. Decide before installing which of the two cases applies to you:
Case A — server is reachable from outside
You have a public IP (static or kept updated via DynDNS) and can forward
ports 80 and 443 to the server. Then Caddy obtains the Let's Encrypt
certificate automatically — just like in the
Hetzner guide.
In .env this is enough:
LUMIO_HOST=photos.yourstudio.com
PUBLIC_URL=https://photos.yourstudio.com
S3_PUBLIC_URL=https://photos.yourstudio.com/s3 Case B — no inbound port 80/443 (CGNAT, corporate LAN)
Then you terminate HTTPS outside and forward plain HTTP to Lumio's Caddy — via a tunnel (Cloudflare Tunnel, Tailscale Funnel) or an existing reverse proxy. Caddy then runs on an HTTP port only; the valid certificate comes from the tunnel:
LUMIO_HOST=photos.yourstudio.com
LUMIO_S3_HOST=s3.yourstudio.com
PUBLIC_URL=https://photos.yourstudio.com
S3_PUBLIC_URL=https://s3.yourstudio.com
CADDY_HTTP_PORT=8080 # tunnel/proxy points here, it does the TLS
Caddy trusts X-Forwarded-Proto from private ranges, so it correctly sees the externally terminated HTTPS.
Step 1 — Prepare the server
Non-root user, SSH by key, firewall, Docker. Condensed — the Hetzner guide has this in more detail:
# Update the system
sudo apt update && sudo apt upgrade -y
# Firewall — only what's needed
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
# Docker engine + Compose plugin
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# log back in so the group takes effect, then test:
docker run --rm hello-world In case B (tunnel) you don't open 80/443 in the firewall — the tunnel builds the connection from the inside out.
Step 2 — Get Lumio & configure
sudo mkdir -p /opt/lumio && sudo chown $USER:$USER /opt/lumio
cd /opt/lumio
git clone https://github.com/markusthiel/lumio.git .
cp .env.example .env Generate secure secrets:
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')|" .env
sed -i "s|^JWT_SECRET=.*|JWT_SECRET=$(openssl rand -base64 32 | tr -d '/+=')|" .env
sed -i "s|^SESSION_SECRET=.*|SESSION_SECRET=$(openssl rand -base64 32 | tr -d '/+=')|" .env
sed -i "s|^S3_ACCESS_KEY=.*|S3_ACCESS_KEY=$(openssl rand -hex 12)|" .env
sed -i "s|^S3_SECRET_KEY=.*|S3_SECRET_KEY=$(openssl rand -base64 32 | tr -d '/+=')|" .env
Then enter the domain/TLS values for case A or B from the section above. DEPLOYMENT_MODE=single is the default — leave it; it auto-creates a studio on first start.
Step 3 — Start & create an admin
# Start the stack (the first run builds the images, takes a while)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker compose ps # are all services running?
docker compose logs -f # follow the logs Create the first user:
docker compose exec api npm run create-admin \
-- --email=you@yourstudio.com --password=atleast12chars
Studio login at https://photos.yourstudio.com. You're live once creating a test gallery, uploading an image, sharing the link and opening it from another device all work.
Step 4 — Operations: backup & updates
On-premise means backups are entirely your job. Back up the database and the object data regularly — and copy them to a second, off-site location.
# Database dump (e.g. daily via cron)
docker compose exec -T postgres pg_dump -U lumio lumio > backup_$(date +%F).sql
# Object data lives in the Docker volume (MinIO) — back that up too
docker volume ls | grep minio
# Update: pull the new version and rebuild
git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build Common questions about the on-premise setup
Do I need a static public IP? +
No. A dynamic IP is fine as long as you keep a domain updated via DynDNS/DDNS (many routers can do this, otherwise services like deSEC or Cloudflare). What matters more than a static IP is that your server is reachable from outside on port 80/443 — or that you use a tunnel (see below). It even works with no inbound reachability at all, in which case you terminate TLS elsewhere.
What if I can't or may not open port 80/443 (CGNAT, corporate LAN)? +
Then you terminate HTTPS outside of Lumio and forward plain HTTP to Lumio's Caddy. Proven options: a tunnel like Cloudflare Tunnel or Tailscale Funnel, or an existing reverse proxy. Lumio's Caddy explicitly supports this external reverse-proxy mode (set LUMIO_HOST + LUMIO_S3_HOST, run Caddy on an HTTP port only). The tunnel brings the valid certificate — you don't have to open anything inbound.
Can I run Lumio on the local network (LAN) only? +
Yes. For a valid HTTPS certificate, though, Let's Encrypt needs either public reachability (HTTP challenge) or a DNS challenge via your DNS provider. Purely on the LAN without internet, the route is an internal certificate or a tunnel. For sending gallery links to clients, real external reachability is the normal case anyway.
What hardware should the server have? +
64-bit Linux (amd64 or arm64), Docker ≥ 24 with Compose v2. RAM rule of thumb: photos only from 4 GB, with video transcoding 8 GB, optional AI auto-tagging another +4 GB. A retired workstation or a small mini PC is plenty for a solo studio. For the image data, use mirrored storage (RAID) or a NAS mount so a disk failure doesn't cost you the originals.
How do I do backups when nobody but me is responsible? +
Back up two things: the Postgres database (a daily pg_dump via cron) and the object data (the MinIO volume). Copy both regularly to a second, off-site location — a different disk, a different building, or a cheap object-storage bucket at a provider. On-premise means the backup is entirely on you, so plan it from the start.
Ready for your own gallery?
Source code on GitHub, questions and discussions there too. If anything in the guide trips you up, reach out — we're happy to improve the docs.