Deployment
Running filex in production. This doc assumes you've already got an instance up (see INSTALLATION.md) and covers what sits around it: the reverse proxy, TLS, the one setting that trips everyone up (FILEX_PUBLIC_URL), scaling, backups, health checks, and a hardening pass.
filex is a single Go process that serves everything from one origin on port 5212 - the admin SPA, the JSON API, public share (/s/…) and file-drop (/d/…) pages, the /embed.js web component, the realtime WebSocket at /api/ws, the MCP stream at /api/ai/mcp, and /healthz. There is no separate frontend to host and no second port. Production deployment is therefore mostly "put a good reverse proxy in front of port 5212."
- Reverse proxy - Caddy · nginx · Serving filex under a sub-path
- HTTPS
- PUBLIC_URL
- Scaling / high availability
- What else runs beside filex
- Backup & restore
- Health & monitoring
- Hardening checklist
- See also
Reverse proxy
Terminate TLS at a reverse proxy and forward / to filex:5212 (Docker) or 127.0.0.1:5212 (binary) - or a path of a domain you share with other things, see Serving filex under a sub-path. filex speaks plain HTTP internally - don't try to make it terminate TLS itself. Whatever proxy you pick must:
- Set
FILEX_PUBLIC_URLto the externalhttps://…URL (see PUBLIC_URL) - this is on filex, not the proxy, but it only makes sense once you know the public hostname. - Pass the real client IP as
X-Real-IPand/orX-Forwarded-For. filex reads it for the audit log, the file-drop rate limiter and the sign-in attempt limit - without it every request looks like it came from the proxy, and one person's wrong passwords lock out everybody. filex believes the header only from a trusted proxy:FILEX_TRUSTED_PROXIES, by defaultauto- this machine (a proxy in front of the binary on127.0.0.1:5212) and, in a container, the other containers on its network; never a Docker gateway, never the LAN. A proxy on the host in front of filex's container (it arrives from the gateway), a proxy on another machine, one that reaches filex over100.64.0.0/10(Tailscale, Cloudflare WARP) and a CDN in front of your proxy must be listed (auto, <address>) - the Sign-in security page names such a peer and offers to add it (Trusted proxies). - Allow large request bodies (e.g.
5G) and long timeouts (~600 s) for uploads. For S3 storages the browser PUTs multipart chunks straight to the bucket and only the small init/finalize JSON transits the proxy - but local / SFTP / WebDAV / FTP uploads stream the whole file through it, so the limits have to be generous. - Allow WebSocket / SSE upgrades - on
/api/wsabove all. That is the socket an open explorer runs on, and an explorer that has one does not poll: the 12 s re-listing is the fallback for a socket that failed, not the normal mode. Scope the upgrade to/api/ai/mcpalone and every browser quietly degrades to a folder that refreshes twice a minute - which is exactly the symptom "I upload a file and it shows up ten minutes later" (REALTIME.md). ⚠ The same-origin, cookie-authenticated upgrade is origin-checked, so the proxy must pass the realHostheader through. The MCP endpoint at/api/ai/mcpis a long-lived streamable-HTTP transport (POST for requests, GET to open the SSE stream) and needs the same; download/upload streaming benefits from unbuffered proxying too. - gzip/zstd the admin SPA and JSON responses (the hashed Vite assets are large; filex already sets long
Cache-Controlon them).
Caddy
The bundled config - deploy/compose/Caddyfile - already does all of the above and gets HTTPS for free. A standalone equivalent:
files.example.com {
encode zstd gzip
# Large uploads: local/SFTP uploads stream through here (S3 multipart
# PUTs go browser→bucket directly, so only init/finalize JSON transits).
request_body {
max_size 5GB
}
# WebSocket/SSE (the MCP stream at /api/ai/mcp) upgrade automatically.
# Caddy also adds X-Forwarded-For / -Proto / -Host on its own.
reverse_proxy 127.0.0.1:5212 {
header_up X-Real-IP {remote_host}
}
}That's the whole file. Caddy obtains and renews the certificate automatically (see HTTPS). In the Compose stack the upstream is filex:5212 over the internal Docker network instead of 127.0.0.1:5212.
nginx
A complete server block. The map at the top turns the Upgrade header into the right Connection value so WebSocket/SSE upgrades work. ⚠ On a public port other than 443/80, pass $http_host instead of $host (which drops the port): S3 signatures, WebDAV MOVE/COPY and the realtime socket's origin check compare against the Host the client sent.
# /etc/nginx/conf.d/filex.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
http2 on;
server_name files.example.com;
ssl_certificate /etc/letsencrypt/live/files.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/files.example.com/privkey.pem;
# Large uploads (local/SFTP/WebDAV/FTP storages stream through the proxy).
client_max_body_size 5g;
# Compress the admin SPA + API JSON (hashed assets are already cached long).
gzip on;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml;
location / {
proxy_pass http://127.0.0.1:5212;
proxy_http_version 1.1;
# Real client IP - used for the audit log + file-drop rate limit.
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket / SSE upgrade.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Long timeouts for big uploads/downloads; don't buffer them to disk.
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_request_buffering off;
}
# The realtime socket. Without this block an open explorer never gets a
# change frame and falls back to re-listing every 12 s.
location /api/ws {
proxy_pass http://127.0.0.1:5212;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 3600s;
}
# The MCP stream needs unbuffered, long-lived proxying so SSE tokens flush
# immediately instead of piling up in nginx's buffer.
location /api/ai/mcp {
proxy_pass http://127.0.0.1:5212;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 3600s;
}
}
# Redirect http → https.
server {
listen 80;
server_name files.example.com;
return 301 https://$host$request_uri;
}Serving filex under a sub-path
filex can live under a path of a domain it shares with other things - https://example.com/filex/ - instead of on a host of its own. Tell filex the path, and have the proxy pass the full path through:
FILEX_PUBLIC_URL=https://example.com/filex # its path is the base; or set
# FILEX_BASE_PATH=/filex # explicitly (they must agree)The startup log says which base is in effect (http: serving under a base path base_path=/filex …). Every route, the web app, share links, e-mails, the OIDC callback, WebDAV (/filex/dav/), path-style S3 (/filex/s3/) and the realtime socket (/filex/api/ws) then live under it, and nothing outside it reaches filex: /api/… on the host's root is a plain 404. /healthz is the one exception - it answers at the root too, for container health checks. The rules for the value and what it changes are in CONFIGURATION.md → Base path.
⚠⚠ The proxy must NOT strip the prefix. filex receives /filex/api/files/manager and takes /filex off itself; a proxy that strips it sends /api/files/manager, which filex (correctly) refuses as outside its base. In Caddy that is handle, not handle_path; in nginx a proxy_passwithout a URI part (http://127.0.0.1:5212, not …:5212/).
Caddy - inside the site block of example.com, next to whatever else it serves:
example.com {
encode zstd gzip
request_body {
max_size 5GB
}
# filex under /filex. `handle`, NOT `handle_path`: filex needs the prefix.
# The matcher names /filex itself too - filex answers it with a redirect
# to /filex/ - and not /filexsomething.
@filex path /filex /filex/*
handle @filex {
reverse_proxy 127.0.0.1:5212 {
header_up X-Real-IP {remote_host}
}
}
# …the rest of example.com
handle {
respond "the rest of the site" 200
}
}nginx - in the server block of example.com (the map from the nginx example above is needed too):
# filex under /filex. ⚠ `proxy_pass` has NO URI part: nginx then passes
# the request's path unchanged. `proxy_pass http://127.0.0.1:5212/;`
# (trailing slash) would strip /filex, and filex would answer 404.
# ⚠ `$http_host`, not `$host`: `$host` drops the port, and on a port other
# than 80/443 that breaks three things that compare against the Host the
# client sent - S3 signatures (SignatureDoesNotMatch), a WebDAV MOVE/COPY
# Destination (502) and the realtime socket's origin check (403).
location = /filex {
return 301 /filex/;
}
location /filex/ {
proxy_pass http://127.0.0.1:5212;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 5g;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_request_buffering off;
}
# The realtime socket and the MCP stream, unbuffered and long-lived -
# the same two blocks as above, under the base.
location /filex/api/ws {
proxy_pass http://127.0.0.1:5212;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /filex/api/ai/mcp {
proxy_pass http://127.0.0.1:5212;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_buffering off;
proxy_read_timeout 3600s;
}Traefik / a Kubernetes Ingress - route PathPrefix(`/filex`) to filex and add no StripPrefix middleware; the Helm chart does this when you set basePath (see INSTALLATION.md → Kubernetes).
What else to know:
- Clients take the base as part of the server address:
https://example.com/filexin the desktop app's sign-in field and infilex client login --url,https://example.com/filex/dav/for WebDAV,https://example.com/filex/s3as a path-style S3 endpoint (the connection pages print all of these filled in). A dedicated S3 host (FILEX_S3_DOMAIN) is served at its own root and is not affected. - Cookies are scoped to the base (
Path=/filex), so the other applications on the host do not receive filex's session. - An explorer embedded in another app keeps pointing its
apiBaseat the server root -https://example.com/filex- see INTEGRATION.md. - Moving an existing install under a base (or out of one) changes the address of everything, including the installed web app's identity: people who installed it install it again, and links already sent point at the old address. A proxy can keep the old addresses alive with a redirect.
- Upgrading from a setup whose proxy stripped the prefix. Before 0.47.0 filex had no base path, and a
FILEX_PUBLIC_URLwith a path only changed the links it built; a proxy that stripped the prefix got share links working and the web app broken. From 0.47.0 on that path is the base: switch the proxy to pass the full path (handleinstead ofhandle_path).
HTTPS
Always terminate TLS at the proxy and keep filex plain-HTTP behind it.
- Caddy issues and renews certificates automatically via Let's Encrypt / ZeroSSL. Just point DNS at the host and open ports 80 and 443 so the ACME challenge succeeds. Nothing else to configure.
- nginx pairs with certbot:
certbot --nginx -d files.example.comwrites thessl_certificatelines and sets up a renewal timer. Or terminate at a managed load balancer and forward plain HTTP to filex. - Kubernetes: terminate at the Ingress with cert-manager (a
ClusterIssuer- a
tlsblock / annotation on the Ingress). The Helm chart'singress.*values wire this up - see INSTALLATION.md → Kubernetes.
- a
Whichever you use, the external scheme must be https and must match FILEX_PUBLIC_URL.
PUBLIC_URL
FILEX_PUBLIC_URL (default http://localhost:5212) is the externally resolvable URL users open in a browser - e.g. https://files.example.com, or https://example.com/filex for filex under a sub-path (its path is then the base). Behind a proxy this is the proxy's public hostname, not the internal filex:5212.
filex bakes this value into things it can't recompute from an incoming request:
- Share and file-drop links (
/s/…,/d/…) - the URLs handed to recipients. - The OIDC redirect - the callback is
<public>/api/auth/oidc/callback; it must match what's registered at your IdP. - OnlyOffice fetch/callback - the Document Server fetches the file and posts edits back server-to-server using filex's public URL.
A wrong value fails quietly in production: share links point at localhost and 404 for everyone else, SSO login bounces to an unreachable/invalid redirect, and OnlyOffice saves fail because the Document Server can't reach the callback. Set it to the real HTTPS URL and keep it in sync with your DNS and IdP config.
The binary/container listens on
FILEX_LISTEN(default0.0.0.0:5212);FILEX_PUBLIC_URLis a separate, public-facing value. They're almost never the same behind a proxy.
Scaling / high availability
Start with one vertically-scaled node. filex has no built-in clustering or leader election; a single instance backed by PostgreSQL handles real teams comfortably. Reach for multiple replicas only when you actually need them, and know the constraints first.
The database is the gate. The default SQLite store is a single file with a process-level lock - it's fine for one node but cannot be shared by replicas. For more than one filex instance you must move the database off SQLite:
FILEX_DB_DRIVER=postgres
FILEX_DB_DSN=postgres://filex:…@db:5432/filex?sslmode=requirePostgreSQL is the recommended production driver. MySQL/MariaDB is supported too - every engine's migrations, schema and first-install writes are exercised in CI (see DATABASES.md), which they were not before v0.38.0.
The queue must be shared too. With no FILEX_QUEUE_DRIVER the queue follows the database and lives in it. For multiple workers/nodes point it at Redis, or at a Postgres of its own:
FILEX_QUEUE_DRIVER=postgres # uses SELECT … FOR UPDATE SKIP LOCKED
# or
FILEX_QUEUE_DRIVER=redis
FILEX_QUEUE_DSN=redis://redis:6379/0⚠ Both drivers had a bug worth knowing about if you are upgrading. Before v0.34.0 the postgres driver could never claim an operation at all - every type-filtered dequeue failed with 42P18 - so on FILEX_QUEUE_DRIVER=postgres nothing in the queue ever ran: no antivirus scans, no content extraction, no async copy/move/delete. And the redis driver ignored Priority, so an interactive operation queued behind a bulk import waited for all of it. Both are fixed. The redis pending set changes from a LIST to a SORTED SET, converted at startup with every queued operation preserved - ⚠ after which downgrading is not supported.
/data is per-instance. Beyond the SQLite DB, each node keeps five things on local disk under FILEX_DATA_DIR:
search.bleve/- the full-text index (its embedded store takes an exclusive file lock, so it can't be shared read-write either),thumbs/- the thumbnail cache (released when a node is purged, and swept for orphans everyFILEX_THUMBS_SWEEP_INTERVAL),cache/- the read cache for slow storages,uploads/- staging for chunked and resumable uploads. ⚠ Not rebuildable while a transfer is in flight: until it commits, this is the file's only copy,ssh/+ftps/- the SFTP host keys and the FTPS certificate, generated on first boot. ⚠ Per-node local/datameans each replica generates its own, so a client load-balanced across them sees the host key change from connection to connection. Put the protocol listeners on one node, or hand every replica the same key material.
The first three are rebuildable (see Backup & restore), so for a multi-replica deployment you have two honest options:
- Per-node local
/data(an emptyDir-style volume each) - every replica builds its own search index and thumbnail cache. Simplest; costs some duplicate work and each node's index lags until it syncs. - A shared RWX volume for
/data- only safe if you're confident about the file-lock semantics of your shared filesystem; when in doubt prefer option 1.
Sessions and API tokens are validated against the database, so a shared Postgres keeps logins working across replicas without sticky sessions - just make sure every replica shares the same DB, the same secrets, and the same FILEX_PUBLIC_URL. Storage backends (S3, SFTP, …) are external and already shared by definition.
⚠ The sign-in attempt limit counts per instance: its settings reach every replica within a minute, but each replica keeps its own counters, so N replicas allow up to N times the limit, and a lock one replica places is not seen by the others until they restart.
Bottom line: filex scales up trivially and scales out to a Postgres + Redis/Postgres-queue topology with per-node (or carefully-shared) search/thumb state. It does not yet ship a turnkey active-active cluster.
What else runs beside filex
filex is one process, but five optional things run next to it, each as its own container or service, each reached over the network:
| Service | What it gives you | Configured |
|---|---|---|
ClamAV (clamav/clamav) | virus scanning of every file written | FILEX_CLAMAV_ADDR=clamav:3310 seeds it; after the first boot, Settings → Protection |
| OnlyOffice Document Server | in-browser Office editing, and since 0.50 the only office engine: office thumbnails and the apps' office conversions (no image carries LibreOffice) | Settings → External services, applies with no restart |
| drawio | diagram editing | same |
| converter side-car | format conversion from the UI | same |
S3 server (versity/versitygw) | an S3 storage on your own disk | the versitygw Compose profile or versitygw.enabled in Helm; STORAGE.md → A local S3 server |
⚠ The filex images ship no scanner. ClamAV plus its signature database is close to a gigabyte, so it is not baked in - which makes "run clamd next to filex" the normal shape rather than a fallback. Files are sent with clamd's INSTREAM, streamed down the same connection the command went out on, so the two containers need a network route and not a shared filesystem. There is a ready clamav profile in deploy/compose/docker-compose.full.yml; on Kubernetes it is a Deployment plus a Service, with the address handed to filex through the chart's antivirus: block:
antivirus:
mode: daemon # blank works too - a non-blank address seeds daemon
address: "clamav:3310"⚠ Like every other zero-touch value in that chart these are first-boot seeds, not live switches: once the install has a stored row, the switch, the mode and the address are edited in Settings → Protection and editing the values file does nothing. Details, including what an unreachable daemon does (it fails the scan loudly; it never marks a file clean), are in PROTECTION.md.
⚠ The three external services are read from the database on every use, so an edit in the admin UI reaches the running process. A service named in the environment or config.yaml is re-asserted onto that row at every boot and is labelled env_managed in the UI - an admin edit to one of those applies immediately and is reverted at the next restart. Pick one place per service and stay there.
Backup & restore
Three things hold real state; back up all three:
The database - the SQLite file (
<data_dir>/instance.sqlite) or your PostgreSQL/MySQL. This is the source of truth for users, storages, permissions, shares, tags, the file-node cache, and audit history.The storage backends - the actual file bytes. filex doesn't own these; a local disk, an S3 bucket, an SFTP server each have their own backup story. Back them up where they live.
⚠⚠
FILEX_SECRET_KEY- the key that seals the credentials filex has to be able to recover rather than merely compare. S3 access keys are the case that hurts most: SigV4 verifies a request by recomputing an HMAC chain from the secret, so it cannot be hashed the way an API token is, and it is stored sealed with AES-GCM under this key. The same key also seals a remote storage plugin's token, an app's secret settings and signing authority, and the copy of each share link's PIN that lets its creator read it back (the link itself keeps working without it).A restored database without the matching key is a database whose S3 access keys no longer verify - every
aws s3,rcloneandresticjob pointed at this server stops with a signature error, and nothing in that error says why. Keep it wherever you keep the database credentials, and do not rotate it casually. Losing it is not recoverable; the fix is minting every access key again.
Rebuildable, so backing them up is optional:
search.bleve/- regenerate withPOST /api/admin/search/rebuild(admin) or by deleting it and restarting. ⚠ A rebuild needs room for a second copy of the index while it runs (roughly 2x the current size in additional space); filex refuses and keeps serving the old one when the disk cannot take it. Sibling directories namedsearch.bleve.rebuilding/search.bleve.oldare a rebuild in flight or an interrupted one - they are cleaned up on the next start, and neither is worth backing up.thumbs/- regenerate withfilex thumb backfill(add--retry-failedto re-run failed rows,--storage <name|id>to scope it).
Rebuildable, and you should actively EXCLUDE it:
cache/- prepared copies of big files on slow storage, and folder-share ZIPs (cache/sharezips/, moved there from<data_dir>/sharezipson first start of v0.19.1+). These are the largest throwaway objects filex produces: one archive of a shared folder is as big as the folder. Backing the directory up puts them in every snapshot, every off-site copy and every restore, for bytes that are regenerated on demand. Exclude<data_dir>/cache(and, for older backup configs,**/sharezips/**).
Restore = restore the database, restore/attach the storage backends, start filex. If you skipped the search index and thumbnail cache in your backup, filex serves immediately and you trigger a rebuild/backfill to repopulate them.
Upgrades run migrations on startup. Pull the new image or binary and restart - schema migrations apply automatically. Back up the database before upgrading. To inspect or roll back one step manually:
filex migrate status # what's applied / pending
filex migrate down # roll back exactly one migration
# in Docker: docker exec filex filex migrate downHealth & monitoring
- Liveness / readiness:
GET /healthzreturns200with{"status":"ok"}and needs no authentication. Wire it to your load balancer health check and to Kubernetes liveness+readiness probes. The Docker image's built-inHEALTHCHECKalready polls it every 30 s. - Error reporting: set
FILEX_SENTRY_DSN(and optionallyFILEX_SENTRY_ENVIRONMENT=production) to ship crashes/errors to Sentry or a self-hosted GlitchTip. Empty DSN = off. See CONFIGURATION.md → Error reporting. - Structured logs:
FILEX_LOG_FORMAT=json(withFILEX_LOG_LEVEL=info|debug|…) emits JSON lines for Loki / ELK / Datadog ingestion. Each request'smsg=httpline names its caller (user_id,token_id,tenant) and, on the file manager, its verb (action) - never the query string; see CONFIGURATION.md → Logging. - Operational surfaces (admin session/token): the admin Dashboard, plus queue stats (
/api/admin/queue/stats) and storage sync-runs / drift (/api/admin/storages/{id}/sync-runs,…/drift) let you watch worker health and detect a backend that's drifting from the cache.
Hardening checklist
- [ ] Strong, unique secrets. Generate them (
openssl rand -hex 24) and keep them out of version control - never commit.env. TheONLYOFFICE_JWT_SECRETmust be identical on filex and the Document Server; the OIDC client secret must match your IdP. - [ ] HTTPS everywhere. TLS terminated at the proxy,
FILEX_PUBLIC_URLonhttps://. - [ ] Restrict CORS if you embed. The default
FILEX_CORS_ALLOWED_ORIGINS=*is convenient but permissive. If you embed the web component (/embed.js) or the explorer from specific host apps, pin the list to those origins. The same list is the only one that lets another origin change things with a visitor's session (cross-site request forgery is refused for every origin not on it,*included); prefer exact hosts over ahttps://*.example.compattern, which trusts every subdomain. See CONFIGURATION.md → Requests from other origins. - [ ] Keep filex's session off hosts you do not control.
FILEX_COOKIE_DOMAINshares the session cookie with every subdomain of that domain; filex refuses changes those pages send through it, but they still receive the cookie. Leave it empty unless tenants or sibling apps need it. - [ ] Read-only storages where writes aren't needed. Set
read_only: trueon archive/replica mounts so uploads, renames, moves and deletes are refused (see STORAGE.md → Read-only mounts). - [ ] RBAC per storage. Enable
rbac_enabledon storages that shouldn't be visible to every authenticated user; grants then gate access (see RBAC.md). - [ ] Least-privilege API tokens. Scope tokens to only the verbs they need (
read/write/delete/mcp/admin) and useroot:confinement to lock a token to one sub-folder. Confinement is enforced in the backend, so a confined token can't escape its root even if it knows other paths. The verbs hold on every surface the token reaches - the explorer's routes, the MCP tools, WebDAV, SFTP, FTPS, and the S3 keys and NFS exports minted from it (RBAC.md). Installing a plugin and making an administrator are never a token's: an administrator signed in to the panel does them. - [ ] Least-privilege roles. Give people the built-in User or Viewer role, or a custom role that holds only what their work needs - each file action, each kind of sharing, each protocol and each admin area is its own permission (PERMISSIONS.md).
- [ ] TOTP 2FA for admins. Enable it per admin in user settings → Security (the dialog behind the avatar menu).
- [ ] Keep the proxy the sole ingress. If you use proxy-header auth (
auth.header_proxy), filex trusts identity headers likeX-Auth-Email- so bind filex to localhost / the internal network (FILEX_LISTENon127.0.0.1:5212, or don't publish the container port), settrusted_ipsto the proxy, and make sure nothing can reach 5212 except the proxy, or a client could spoof those headers. - [ ] Trust only your own proxies.
FILEX_TRUSTED_PROXIESdefaults toauto: this machine and, in a container, the other containers on its network - never the gateway, never the LAN. A trusted peer can name any address inX-Forwarded-For, one on the sign-in IP allow-list included, so add only your own proxy's address next to it (auto, <address>), and trust a Docker gateway only when the published port is reachable by your proxy alone (127.0.0.1:5212:5212, or firewalled). Check Sign-in security → Trusted proxies: whatautoresolved to, and any peer that sends forwarded addresses without being trusted (Trusted proxies). - [ ] Put your own address on the sign-in allow-list. The sign-in attempt limit spares nobody, the first administrator included; an address on
login.ip_allowlist(Admin → Sign-in security) is the way back into a locked account. - [ ] Disable demo mode.
FILEX_DEMO_MODEisfalseby default - keep it that way in production (it auto-fills demo credentials on the login page).
See also
- INSTALLATION.md - get an instance running first
- CONFIGURATION.md - every
FILEX_*variable - STORAGE.md - storage backends, read-only mounts, sync
- RBAC.md - per-storage / per-file access control
- SSO.md · ONLYOFFICE.md
