You've finished the Quick Start and now want to run Lumio cleanly under your own domain, with HTTPS and backups. This guide takes 15 minutes and assumes:
- A Linux server with a public IP (Hetzner, Netcup, your own metal – doesn't matter), amd64 or arm64 — both are supported
- A domain (e.g.
gallery.your-studio.com) - Docker + Docker Compose v2
Detailed hardware/architecture requirements: REQUIREMENTS.md.
This guide covers single studio. For multi-tenant see MULTI_TENANT.md.
Running on a Synology NAS? See the Synology-specific companion: SELFHOSTING-SYNOLOGY.md.
At your DNS provider, create an A record (and optionally AAAA for IPv6):
gallery.your-studio.com. A <server-ip>
Keep the TTL low for now (300s); you can raise it later.
Check:
dig gallery.your-studio.com +shortShould return your server IP.
On the server (or via the cloud console):
# UFW as an example
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enableImportant on Hetzner Cloud: in addition to the OS firewall there's also the Cloud Firewall in the Hetzner console – open 80 and 443 there too, otherwise nothing gets through.
mkdir -p /opt/docker && cd /opt/docker
git clone https://github.com/markusthiel/lumio.git
cd lumio
cp .env.example .envGenerate 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 '/+=')|" .envnano .envSet these values:
LUMIO_HOST=gallery.your-studio.com
PUBLIC_URL=https://gallery.your-studio.com
# S3 needs its OWN subdomain — a path prefix (…/s3) does NOT work:
# MinIO verifies the AWS V4 signature over the full path incl. bucket,
# so path rewriting breaks every upload. Set BOTH values and add a
# DNS A record for the subdomain pointing to the same server IP:
LUMIO_S3_HOST=s3.your-studio.com
S3_PUBLIC_URL=https://s3.your-studio.com(Without a domain — plain IP testing — leave both S3 values unset: the API
then automatically signs against http://<host>:9000; make sure port 9000
is reachable.)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dCaddy automatically obtains a Let's Encrypt certificate (takes about 30 seconds). Watch it:
docker compose logs -f caddySuccess looks like this: certificate obtained successfully ... gallery.your-studio.com.
In single mode Lumio automatically creates a tenant named "My Studio" on first start – you don't need a super admin and don't create a tenant manually. Just the first user:
docker compose exec api npm run create-admin -- \
--email=you@your-studio.com \
--password=atleast12chars \
--name="Your Studio"→ https://gallery.your-studio.com
On first login create a test gallery, upload an image, share the gallery link, open it from another device. If everything works: you're live.
Beta/experimental features are behind feature flags and disabled by default.
Self-hosted (no super admin UI) you enable them globally via .env:
FEATURES_ENABLED=print_shop,advanced_analyticsAvailable keys: print_shop, lightroom_plugin, advanced_analytics,
ai_tagging (requires the ML worker, docker-compose.ml.yml),
video_streaming_4k. Restart the API after changing:
docker compose up -d api. In multi mode, per-tenant overrides from the
super admin UI take precedence — including for disabling.
The print shop additionally has provider-level activation (normally done in the super admin UI). Self print always works; for the lab integrations set:
PRINT_PROVIDERS_ENABLED=prodigi,gelatoEach studio then enters its own API credentials under Print shop → Providers.
Back up at least two things:
- Postgres – the whole application database (users, galleries, permissions)
- S3 bucket – the actual images/videos
Add to crontab (crontab -e):
0 3 * * * cd /opt/docker/lumio && docker compose exec -T postgres pg_dump -U lumio lumio | gzip > /backup/lumio-$(date +\%Y\%m\%d).sql.gz && find /backup -name "lumio-*.sql.gz" -mtime +14 -deleteKeeps 14 days. Then sync off-site (e.g. with rclone to a backup provider).
With MinIO simply rsync the minio_data volume. With external S3 (Hetzner, R2, B2) enable versioning + lifecycle – the providers do this in their console.
Run a restore test on a test VM regularly. An untested backup is not a backup.
cd /opt/docker/lumio
git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --buildPrisma migrations run automatically on API start. Take a Postgres dump before every update – migrations are rarely reversible.
You can see the currently running version with docker compose logs api | grep "Lumio API ready".
MinIO works well for smaller setups (up to ~500 GB). For more volume external S3 pays off:
- Hetzner Object Storage – cheap, GDPR, can be in the same datacenter as the server
- Cloudflare R2 – free egress, good for CDN setups
- Backblaze B2 – cheapest storage price, slightly higher latency
Setup steps: see STORAGE.md.
Important with external S3: set CORS on the bucket! Lumio uploads images directly from the browser to S3 (presigned URLs). Without CORS that fails. Specifically:
{
"CORSRules": [{
"AllowedOrigins": ["https://gallery.your-studio.com"],
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}]
}For Hetzner Object Storage additionally set S3_FORCE_PATH_STYLE=true and S3_REGION=fsn1 (or your bucket location).
- Never commit
.env– it's in.gitignore, but check anyway - Don't expose the Postgres port externally – the default is already correct, only reachable internally
- Secure the MinIO console (port 9001) – in the production compose it isn't reachable from outside by default, only via
docker exec - Encrypt backups when stored on third-party storage (
gpg,restic,borg) - SSH key-only login on the server, no password auth
- Enable unattended upgrades for OS patches
→ see TROUBLESHOOTING.md