Self-hosting guide

Run CrabS3 with Docker Compose β€” or Proxmox VE

A full deployment is a clone, an env file and one command β€” or, without Docker, a single script from the community-scripts catalogue that sets up a Proxmox VE LXC bare-metal. This page covers the compose stack, every environment variable that matters, the S3 backends known to work, and what to check when something does not come up.

Requires
Docker + Compose, or Proxmox VE
Interface
localhost:3000
Containers
app Β· db Β· clamav Β· cron
Time to first upload
~10 minutes

Install

  1. Clone and prepare the environment

    Copy the sample env file and open it β€” nothing starts without a database URL and a bucket.

    git clone https://github.com/DoctorPok42/CrabS3.git
    cd CrabS3
    cp .env.example .env
  2. Point it at your storage

    One bucket. "Cold" is a class your provider's lifecycle rule assigns β€” CrabS3 just tracks which one a file is in, it never copies bytes to a second bucket.

    S3_ENDPOINT=http://rustfs:9000
    S3_ACCESS_KEY_ID=your-access-key
    S3_SECRET_ACCESS_KEY=your-secret-key
    S3_BUCKET_NAME=crabs3-hot
    
    S3_REGION=us-east-1
    EXPIRED_FILE_POLICY=cold        # or: delete
  3. Bring the stack up

    Migrations run on start. ClamAV downloads its signature database on first boot, which takes a few minutes β€” uploads are refused until it is ready.

    docker compose up -d
    docker compose logs -f app
    
    # ready when this answers 200
    curl -i http://localhost:3000/api/health
  4. Create the first account

    Signup is invite-only by design, so the first user cannot come from the signup form. Seed one admin directly, then issue invites from the admin panel. Safe to run again β€” it does nothing if an admin already exists.

    docker compose exec -T web npx tsx install/seed-admin.mjs
    # prints ADMIN_EMAIL and ADMIN_PASSWORD once β€” save them, they are not stored anywhere

    Set a strong JWT_SECRET before this step. Changing it later invalidates every session and every share link signature.

No Docker? Proxmox VE

A community-scripts LXC install runs Node.js, PostgreSQL and ClamAV directly in the container β€” no Docker inside it. Same environment variables as above, prompted for at install or set as `var_s3_endpoint` / `var_s3_access_key` / `var_s3_secret_key` beforehand.

COMMUNITY_SCRIPTS_URL="https://raw.githubusercontent.com/DoctorPok42/CrabS3/main"   bash -c "$(curl -fsSL https://raw.githubusercontent.com/DoctorPok42/CrabS3/main/ct/crabs3.sh)"

Environment reference

Keep these in .env, or manage them in Doppler β€” a doppler.yaml ships with the repo and is picked up automatically.

Application

Application environment variables
VariableDescriptionExample
DATABASE_URLPostgres connection stringpostgresql://user:pw@db:5432/crabs3
NEXT_PUBLIC_BASE_URLPublic origin used in share links and emailshttps://files.example.com
JWT_SECRETSigns sessions β€” long and randomopenssl rand -hex 32
COOKIE_SECUREDefaults to NODE_ENV==='production' if unset. Set false with no TLS in front, or the session cookie is silently dropped after logintrue | false
LOG_MIN_LEVELLowest level written to the audit logINFO

Storage

Storage environment variables
VariableDescriptionExample
S3_ENDPOINTS3 endpoint that serves downloadshttp://rustfs:9000
S3_ACCESS_KEY_IDAccess key for the hot bucketAKIA…
S3_SECRET_ACCESS_KEYSecret key for the hot bucketβ€’β€’β€’β€’β€’β€’β€’β€’
S3_BUCKET_NAMEBucket serving live filescrabs3-hot
S3_REGIONRegion string; any value for self-hosted S3us-east-1
EXPIRED_FILE_POLICYArchive to cold, or delete outrightcold | delete

Email, scanning and cron

Email, scanning and cron environment variables
VariableDescriptionExample
SMTP_HOST / USER / PASSRelay used for notification emailsmtp.example.com
SMTP_FROMSender shown to recipientsCrabS3 <bot@example.com>
CLAMAV_HOST / CLAMAV_PORTScanner reachable from the app containerclamav / 3310
CRON_SECRETShared secret the expiry job presentsopenssl rand -hex 24

Storage backends

Anything that speaks the S3 API works. RustFS is the default in compose.yml because the cold-storage transition is handled by a lifecycle rule on the bucket itself, not by the app β€” CrabS3 never copies bytes anywhere.

  • RustFS

    DEFAULT

    Ships in compose.yml β€” the fastest way to a working instance. Set tiering and replication on the bucket itself.

  • AWS S3

    TESTED

    Use a real region and IAM keys scoped to the bucket. A lifecycle rule transitions objects to Glacier on your schedule.

  • OVH Object Storage

    TESTED

    The S3-compatible endpoint, not Swift. Storage classes are set per container in the OVH console.

Retention and the cron container

Expiry is not evaluated lazily on access. A separate container calls the expiry endpoint on a schedule, authenticated with CRON_SECRET, and acts on everything past its deadline or download quota.

EXPIRED_FILE_POLICY=cold

Follow the lifecycle rule

Ageing files are marked cold, tracking the transition your bucket's own lifecycle rule performs. Nothing to configure in CrabS3 β€” the rule lives on the provider.

EXPIRED_FILE_POLICY=delete

Delete what is spent

Past its deadline or its download quota, the object is removed from the bucket and the row from the database. Gone means gone.

The rule belongs to your provider. CrabS3 has no tiering setting: you configure the transition once on the bucket β€” S3 lifecycle rules, an OVH storage class, a RustFS policy β€” and the app records the class each file ends up in.

Because the class is a field and not a location, promoting a file back to the fast tier is one click in the dashboard β€” no copy, no re-upload, no second bucket.

When it does not come up

Four failures account for nearly every unsuccessful first deploy.

Uploads fail immediately with a 403
The keys are right but the bucket does not exist, or the policy forbids multipart. Create the bucket first and confirm the credentials can call CreateMultipartUpload.
Everything works except the scan
ClamAV is still fetching signatures on first boot. Watch docker compose logs clamav until freshclam reports the database is up to date; uploads are refused until then.
Share links point at localhost
BASE_URL is still the default. Set it to the public origin and restart the app container β€” links and emails are built from it.
Files never expire
The cron container cannot authenticate. CRON_SECRET must be identical in the app and cron services, and the app must be reachable at the internal hostname the job calls.

Running. Now wire it up.

Point your own client at the HTTP API, or drop the health endpoint into your uptime monitor.

API reference β†’