DEV Community

Cover image for Filux: Open-Source File Sharing with Docker or Native Linux
FlatlinerAtheist | CLI-NERDS
FlatlinerAtheist | CLI-NERDS

Posted on Originally published at clinerds.com

Filux: Open-Source File Sharing with Docker or Native Linux

Sending one large file should not require creating another cloud account. Nor should collecting a document from a client mean asking them to email six compressed attachments. A small open-source, self-hosted file-sharing service can handle both jobs: you upload a file, send an expiring link, or give somebody a private guest upload link that drops files into a folder you choose.

That is the appeal of Filux, an AGPL-licensed open-source alternative to WeTransfer-style transfer links. Its clean interface, controlled public shares, and Request Files workflow cover the most common file-transfer jobs without another subscription. This guide covers both Docker Compose and native Linux installations, with HTTPS, restricted network access, and a backup plan.

What Filux does

Filux sits between a full cloud suite such as Nextcloud and a temporary file-transfer service. You get nested folders, multi-file uploads, previews, storage quotas, and user management without having to adopt an entire groupware platform. It is a focused self-hosted file-sharing app, not a complete Google Drive replacement.

The sharing controls are the interesting part:

  • Public download links can have passwords, expiry times, and usage limits.
  • User-to-user shares distinguish viewing, downloading, editing, and deleting.
  • Request Files links reverse the flow, allowing guests to upload into a selected folder.
  • Accounts can use TOTP two-factor authentication, passkeys, or an OIDC identity provider.
  • Administrators can control registration and assign default or per-user storage quotas.

The current Filux repository also documents bulk archive handling, shared-item download counts, HEIC/HEIF previews, and paid-download support. WebDAV is still on the roadmap, so do not choose Filux expecting native WebDAV mounts today.

Its stack is React, Node.js with Fastify, PostgreSQL, Redis, and local filesystem storage. The production container serves the application through Nginx and applies database migrations during startup.

Before you begin

You need a Linux server and a domain such as filux.example.com pointing to it. The Docker method requires Docker Engine and the Compose plugin; the native method has its own dependency checklist below. Check Compose if you will use Docker:

docker compose version
Enter fullscreen mode Exit fullscreen mode

The Docker examples use /srv/filux for the stack and expose the application only on the server's loopback address. Nginx accepts public traffic and forwards it to Filux. PostgreSQL and Redis do not have host ports at all.

Storage warning: Neither a container volume nor a native upload directory is a backup. Put the uploads on storage with enough free space, set quotas before inviting users, and keep a separate copy of anything you cannot afford to lose.

Create the Docker Compose file

Create the application directory:

sudo mkdir -p /srv/filux
cd /srv/filux
Enter fullscreen mode Exit fullscreen mode

Save this as compose.yaml. It is adapted from the upstream production file, with the application port bound to 127.0.0.1 instead of every network interface:

name: filux

services:
  postgres:
    image: postgres:17-alpine
    container_name: filux-postgres
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: filux-redis
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  filux:
    image: bmcgonag/filux:latest
    container_name: filux
    env_file:
      - .env.production
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379
      APP_URL: ${APP_URL}
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - uploads:/app/uploads
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:80/api/v1/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:
  uploads:
Enter fullscreen mode Exit fullscreen mode

The three named volumes keep the database, Redis data, and uploaded files independent of the container lifecycle. A normal docker compose down preserves them. Never add -v unless you deliberately intend to delete those volumes.

Configure secrets and the public domain

Create .env.production in the same directory:

# Database
POSTGRES_USER=filux
POSTGRES_PASSWORD=REPLACE_WITH_A_RANDOM_HEX_VALUE
POSTGRES_DB=filux

# Redis
REDIS_PASSWORD=REPLACE_WITH_A_DIFFERENT_RANDOM_HEX_VALUE

# Application secrets
JWT_SECRET=REPLACE_WITH_A_THIRD_RANDOM_HEX_VALUE
ENCRYPTION_KEY=REPLACE_WITH_EXACTLY_32_RANDOM_ASCII_CHARACTERS

# Uploads and sessions
UPLOAD_DIR=/app/uploads
MAX_UPLOAD_SIZE_MB=10240
SESSION_TTL_HOURS=24
RATE_LIMIT_MAX=5
RATE_LIMIT_WINDOW_MINUTES=15

# Public URL and passkey settings
CORS_ORIGIN=https://filux.example.com
WEB_AUTHN_RP_NAME=Filux
WEB_AUTHN_RP_ID=filux.example.com
WEB_AUTHN_RP_ORIGIN=https://filux.example.com
APP_URL=https://filux.example.com
Enter fullscreen mode Exit fullscreen mode

Generate four independent values:

openssl rand -hex 32      # POSTGRES_PASSWORD
openssl rand -hex 32      # REDIS_PASSWORD
openssl rand -hex 32      # JWT_SECRET
openssl rand -base64 24   # ENCRYPTION_KEY: exactly 32 ASCII characters
Enter fullscreen mode Exit fullscreen mode

Use hexadecimal passwords for PostgreSQL and Redis so the connection URLs do not need percent-encoding. Filux currently converts ENCRYPTION_KEY to a 32-byte UTF-8 key; the base64 command produces exactly 32 ASCII characters. Do not reuse the example placeholders.

Replace filux.example.com everywhere with your real hostname. WEB_AUTHN_RP_ID is the bare domain, while APP_URL, CORS_ORIGIN, and WEB_AUTHN_RP_ORIGIN include https:// and no trailing slash. These values must match the public address or passkeys and redirects will fail.

Protect the file and restrict its ownership to the account that administers the stack:

chmod 600 .env.production
Enter fullscreen mode Exit fullscreen mode

Anybody who can read this file or inspect the containers can recover the application secrets. Membership in the docker group is effectively root access, so do not hand it out casually.

Start Filux and check its health

Run Compose with --env-file explicitly:

cd /srv/filux
docker compose --env-file .env.production pull
docker compose --env-file .env.production up -d
docker compose --env-file .env.production ps
Enter fullscreen mode Exit fullscreen mode

That flag matters: the service's env_file: directive passes variables into the container, but --env-file also lets Compose substitute values such as ${POSTGRES_USER} while constructing the database and Redis connection URLs.

Wait for the health checks, then query the application from the server:

curl --fail http://127.0.0.1:8080/api/v1/health
Enter fullscreen mode Exit fullscreen mode

If it is not ready, inspect the logs rather than repeatedly recreating the containers:

docker compose --env-file .env.production logs --tail=100 filux postgres redis
Enter fullscreen mode Exit fullscreen mode

Put Nginx and HTTPS in front

Because port 8080 is bound to loopback, it is not exposed to the LAN or internet. Install Nginx and Certbot, then create /etc/nginx/sites-available/filux:

server {
    listen 80;
    server_name filux.example.com;

    client_max_body_size 10g;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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_request_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
Enter fullscreen mode Exit fullscreen mode

The body-size setting matches MAX_UPLOAD_SIZE_MB=10240. Lower both if you do not want 10 GB uploads. Disabling request buffering avoids Nginx first spooling the entire upload to its own temporary storage, but you should still monitor disk space while testing large transfers.

Enable the site, test the configuration, and request a certificate after DNS resolves correctly:

sudo ln -s /etc/nginx/sites-available/filux /etc/nginx/sites-enabled/filux
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d filux.example.com
Enter fullscreen mode Exit fullscreen mode

Only ports 80 and 443 need to be reachable from the internet. Do not add a firewall rule for port 8080. The Nginx reverse proxy guide explains the forwarding headers and certificate flow in more detail.

Native installation on Linux without Docker

Use this section instead of the Docker Compose and Docker-specific Nginx configuration above if you want to run Filux directly on the host. The application has two parts: Nginx serves the compiled React client, while a systemd service runs the Fastify API on port 3001.

For a new native installation, use Node.js 22 LTS, PostgreSQL 17 or newer, Redis 7, Git, Python 3, and a C/C++ build toolchain. Install them through your distribution or the projects' official repositories. The build tools are needed if native Node.js modules cannot use prebuilt binaries. Check the toolchain before continuing:

node --version
npm --version
psql --version
redis-server --version
git --version
python3 --version
make --version
g++ --version
Enter fullscreen mode Exit fullscreen mode

Create the service account and database

Create an unprivileged account and directories for the source and uploads:

sudo useradd --system --user-group --home-dir /srv/filux --shell /usr/sbin/nologin filux
sudo mkdir -p /srv/filux/uploads /etc/filux
sudo chown -R filux:filux /srv/filux
sudo -u filux -H git clone https://forge.routemehome.org/brian/Filux.git /srv/filux/source
Enter fullscreen mode Exit fullscreen mode

Create a PostgreSQL login and database. The --pwprompt option avoids putting the database password in your shell history:

sudo -u postgres createuser --pwprompt filux
sudo -u postgres createdb --owner=filux filux
Enter fullscreen mode Exit fullscreen mode

Bind PostgreSQL and Redis to loopback, and do not expose ports 5432 or 6379 to the network. Configure Redis with a strong requirepass value in your distribution's Redis configuration, restart Redis, and use that password in REDIS_URL below.

Build the client and API

Build the browser application first, then the server and its generated Prisma client:

cd /srv/filux/source/client
sudo -u filux -H npm ci
sudo -u filux -H npm run build

cd /srv/filux/source/server
sudo -u filux -H npm ci
sudo -u filux -H npx prisma generate
sudo -u filux -H npm run build
Enter fullscreen mode Exit fullscreen mode

The client build creates static files in client/dist; the server build creates executable JavaScript in server/dist. Do not run the Vite development server as a production service.

Configure the native service

Create /etc/filux/filux.env with your own secrets and domain. Use the same four random-value commands from the Docker section:

NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://filux:POSTGRES_PASSWORD@127.0.0.1:5432/filux
REDIS_URL=redis://default:REDIS_PASSWORD@127.0.0.1:6379

JWT_SECRET=REPLACE_WITH_A_RANDOM_HEX_VALUE
ENCRYPTION_KEY=REPLACE_WITH_EXACTLY_32_RANDOM_ASCII_CHARACTERS

UPLOAD_DIR=/srv/filux/uploads
MAX_UPLOAD_SIZE_MB=10240
SESSION_TTL_HOURS=24
RATE_LIMIT_MAX=5
RATE_LIMIT_WINDOW_MINUTES=15

CORS_ORIGIN=https://filux.example.com
WEB_AUTHN_RP_NAME=Filux
WEB_AUTHN_RP_ID=filux.example.com
WEB_AUTHN_RP_ORIGIN=https://filux.example.com
APP_URL=https://filux.example.com
Enter fullscreen mode Exit fullscreen mode

Replace POSTGRES_PASSWORD and REDIS_PASSWORD inside the connection URLs. If either password contains characters such as @, :, /, or #, percent-encode it or use the hexadecimal values shown earlier. Keep this file readable by root and the service group only:

sudo chown root:filux /etc/filux/filux.env
sudo chmod 640 /etc/filux/filux.env
Enter fullscreen mode Exit fullscreen mode

Apply the committed database migrations with the production command, not prisma migrate dev:

sudo -u filux -H sh -c 'set -a; . /etc/filux/filux.env; set +a; cd /srv/filux/source/server; npx prisma migrate deploy'
Enter fullscreen mode Exit fullscreen mode

Keep the API running with systemd

Check where Node.js is installed with command -v node. If it is /usr/bin/node, save this as /etc/systemd/system/filux.service; otherwise adjust ExecStart to the reported path:

[Unit]
Description=Filux API
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=filux
Group=filux
WorkingDirectory=/srv/filux/source/server
EnvironmentFile=/etc/filux/filux.env
ExecStart=/usr/bin/node /srv/filux/source/server/dist/index.js
Restart=on-failure
RestartSec=5
UMask=0027
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/srv/filux/uploads

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

ProtectSystem=strict makes the host filesystem read-only to the service except for the upload directory listed in ReadWritePaths. Start the API and check it from the host:

sudo systemctl daemon-reload
sudo systemctl enable --now filux
systemctl status filux
curl --fail http://127.0.0.1:3001/api/v1/health
Enter fullscreen mode Exit fullscreen mode

If the service fails, use journalctl -u filux -n 100 --no-pager to see the actual error. The upstream API currently listens on 0.0.0.0:3001, so keep port 3001 blocked by the host firewall even though Nginx connects through loopback. Only Nginx should be public.

Serve the native client through Nginx

For a native installation, use this /etc/nginx/sites-available/filux instead of the Docker proxy block:

server {
    listen 80;
    server_name filux.example.com;

    root /srv/filux/source/client/dist;
    index index.html;
    client_max_body_size 10g;

    location /api/ {
        proxy_pass http://127.0.0.1:3001;
        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_request_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}
Enter fullscreen mode Exit fullscreen mode

The try_files fallback is required because Filux is a single-page application: Nginx should serve real files when they exist and send other client-side routes to index.html. Enable the site and use the same nginx -t, reload, and Certbot commands shown in the Docker section. Then continue with the first-run checklist below.

For later native updates, take a backup, pull the chosen source revision, repeat the client and server install/build steps, run prisma migrate deploy, and restart the filux service. Test login, uploads, and existing shares before returning the instance to normal use.

Complete the first-run setup carefully

Open your HTTPS URL and register the first account. According to the upstream documentation, the first registered user becomes the administrator automatically. Complete this step yourself immediately after deployment; do not leave a fresh public instance unattended.

After signing in:

  1. Disable open registration if the instance is only for people you know.
  2. Create users manually and assign storage quotas appropriate to your disk.
  3. Enable TOTP or a passkey on the administrator account.
  4. Configure SMTP if you want email shares and password-reset messages to work.
  5. Upload a harmless test file and verify both a public link and a Request Files link from a private browser window.

That last test catches mismatched APP_URL values, proxy timeouts, and permission problems before somebody sends you an urgent upload.

Share files without sharing your whole server

For an ordinary outbound share, upload the file or folder, open the sharing dialog, and create a public link. Use an expiry and download limit that match the purpose. A password adds a useful barrier, but it does not identify the person downloading the file: anybody who obtains both the link and password may be able to use them.

If a link leaks, revoke or delete it and create a new one rather than assuming the old URL will expire before someone finds it.

Request Files makes Filux particularly useful for collecting documents and other uploads. Instead of giving a collaborator an account, you create an inbound link for a destination folder. The guest opens that link and uploads directly to your server.

Use a separate destination folder for each outside party or job. Set a password, expiry, and upload limit where appropriate. Treat every uploaded file as untrusted: do not run guest-supplied scripts, open documents with macros enabled, or automatically extract archives merely because they came through your own website.

Back up three different things

A useful Filux backup includes more than uploaded files:

  1. The PostgreSQL database, which holds users, permissions, shares, and application state.
  2. The uploaded files, in the Docker uploads volume or the native /srv/filux/uploads directory.
  3. The environment and configuration, including .env.production for Docker or /etc/filux/filux.env, Compose, Nginx, and systemd for native. Preserve the encryption key needed to recover encrypted application secrets.

Create a consistent database dump through PostgreSQL rather than copying its live data directory. For Docker:

mkdir -p /srv/filux/backups
chmod 700 /srv/filux/backups

docker compose --env-file .env.production exec -T postgres \
  pg_dump -U filux -Fc filux \
  > "/srv/filux/backups/filux-$(date +%F-%H%M).dump"
Enter fullscreen mode Exit fullscreen mode

For a native installation, dump through the local PostgreSQL service instead:

sudo -u postgres pg_dump -Fc filux \
  > "/srv/filux/backups/filux-native-$(date +%F-%H%M).dump"
Enter fullscreen mode Exit fullscreen mode

Back up the Docker volume or native upload directory with your normal filesystem/restic process, and store the environment file separately in an encrypted backup. Do not publish it alongside the Compose file. The self-hosted backup strategy explains how to turn these pieces into local and off-site copies.

Most importantly, restore a database dump and open some restored files on a test instance. A backup job that has never been restored is only a promise.

Before updates, take a fresh backup and read the project's release notes. The upstream Docker example uses bmcgonag/filux:latest; for a stable production system, record the digest of the image you tested and pin it rather than allowing every pull to select a new build. For native updates, follow the build and migration sequence in the native section. After an update, test login, uploads, existing public links, and file requests—not just the home page.

Filux or Nextcloud?

Choose Filux when the main job is focused file storage, controlled public downloads, and collecting files from other people. Its Request Files links are a natural fit for designers collecting assets, accountants receiving documents, or a small team that needs a private alternative to transfer links.

Choose Nextcloud when you want desktop and mobile synchronization, calendars, contacts, collaborative apps, and a broader personal cloud. It carries more moving parts because it solves a bigger problem. My Nextcloud self-hosting guide covers that broader setup.

The important distinction is operational, not just visual. Filux can make sharing simple for other people, but you still own the disk capacity, HTTPS renewal, updates, account security, and restores. Start with a small quota and a test folder, prove the backup, and only then send the link to somebody who depends on it.

Top comments (0)