Troubleshooting
Common problems on self-hosted Mirlo instances. For routine operations (logs, restarts, updates, backups), see Maintenance and Updates.
Service won't start
Database connection errors
Verify connection string and credentials:
# Docker - connect to the pgsql container directly
docker compose exec pgsql psql -U mirlo mirloOnce connected, you can query the database. Useful commands:
\dt- list all tablesSELECT * FROM "User" LIMIT 5;- query a table\q- quit
"Authentication failed against database server" (Prisma) usually means one of two things:
The containers are running with stale environment.
docker compose restartdoes not re-read.env— rundocker compose up -dinstead. Compare what the app actually sees against your.env:bashdocker compose exec api printenv DATABASE_URLPostgreSQL was first started with different credentials.
POSTGRES_USER/POSTGRES_PASSWORDonly take effect when the data volume is empty; changing them later does nothing. Test whether the database accepts your current password over TCP (the same auth path the app uses):bashdocker compose exec pgsql psql "postgresql://mirlo:<password>@localhost:5432/mirlo" -c "select 1"If that fails too, either reset the password in place:
bashdocker compose exec pgsql psql -U mirlo -d postgres \ -c "ALTER ROLE mirlo WITH PASSWORD '<password-from-your-env>';"or — only on a fresh install with nothing in the database — wipe the volume and let it re-initialize from
.env:bashdocker compose down rm -rf ./data/pgsql docker compose up -d docker compose exec api yarn prisma:migrate:deploy
File upload failures
Check MinIO is running and has adequate storage:
# Docker
docker compose ps minio
docker compose exec minio mc admin info localOr check wherever your storage buckets are.
First upload to a newly-created bucket gets
403 AccessDenied(S3-compatible providers, e.g. Hetzner Object Storage on Ceph/RadosGW): Mirlo auto-creates buckets on first use (createBucketIfNotExistsinsrc/utils/minio.ts), and on at least one Ceph-backed provider the bucket isn't immediately writable — theCreateBucketcall succeeds but the very nextPutObject403s. This is propagation lag, not a credentials or token-scope problem. Just retry the upload (or the request that triggers it); it succeeds once the bucket has propagated, usually within seconds.
Track audio upload fails silently in the browser, nothing in the API logs past a bucket check: track audio is uploaded straight from the browser to your S3-compatible storage via a presigned URL — the API never sees the file bytes, so a failure here won't show up in
docker compose logs api. Check the browser's network tab instead: a CORS error on thePUTto your storage endpoint means the bucket's CORS configuration hasn't been applied yet. Mirlo sets this automatically the same way it does the public-read policy on image buckets (applyCorsPolicyS3insrc/utils/minio.ts, run whenever a bucket is created or checked at boot) — restarting the api/background containers re-runs that check and should resolve it.
Out of memory errors
Increase available memory or Node.js heap:
# Docker - edit docker-compose.api.yml
environment:
- NODE_OPTIONS=--max-old-space-size=2048Most VPS images ship without swap, which makes memory spikes (like the client build) fatal instead of just slow. Adding a swapfile is cheap insurance:
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabNginx shows its default "Welcome to nginx" page
The request is being answered by nginx's default site because no server_name matched. Check that:
server_namein your config is your actual domain (not theyourdomain.complaceholder);- the site is enabled:
ls -la /etc/nginx/sites-enabled/should show themirlosymlink; - the default site is removed:
rm /etc/nginx/sites-enabled/default.
Then nginx -t && systemctl reload nginx. If certbot ran while the config was still wrong, re-run it after fixing — it installs the certificate into whichever server block matches the requested domain.
Redirect loop, or HTTPS redirects to itself
curl -sk -D- -o /dev/null https://localhost -H "Host: yourdomain.com" on the server tells you which layer is redirecting:
- The origin returns
301to its own URL: a certbotreturn 301(orif ($host = ...)block) ended up inside thelisten 443 sslserver block. The HTTP→HTTPS redirect must live only in thelisten 80block; the 443 block should contain only theproxy_passsetup and certbot'sssl_*lines. Find it withgrep -rn "return 301" /etc/nginx/sites-enabled/. - The origin returns
200but the public URL loops: your DNS record is proxied through Cloudflare (orange cloud) with SSL mode "Flexible" — Cloudflare fetches your origin over plain HTTP, hits the HTTP→HTTPS redirect, and serves that redirect back on HTTPS, forever. Either switch the record to "DNS only" (grey cloud), or set the Cloudflare SSL/TLS mode to "Full (strict)" for this hostname.