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
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
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
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
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_SECRETbefore 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
| Variable | Description | Example |
|---|---|---|
DATABASE_URL | Postgres connection string | postgresql://user:pw@db:5432/crabs3 |
NEXT_PUBLIC_BASE_URL | Public origin used in share links and emails | https://files.example.com |
JWT_SECRET | Signs sessions β long and random | openssl rand -hex 32 |
COOKIE_SECURE | Defaults to NODE_ENV==='production' if unset. Set false with no TLS in front, or the session cookie is silently dropped after login | true | false |
LOG_MIN_LEVEL | Lowest level written to the audit log | INFO |
Storage
| Variable | Description | Example |
|---|---|---|
S3_ENDPOINT | S3 endpoint that serves downloads | http://rustfs:9000 |
S3_ACCESS_KEY_ID | Access key for the hot bucket | AKIA⦠|
S3_SECRET_ACCESS_KEY | Secret key for the hot bucket | β’β’β’β’β’β’β’β’ |
S3_BUCKET_NAME | Bucket serving live files | crabs3-hot |
S3_REGION | Region string; any value for self-hosted S3 | us-east-1 |
EXPIRED_FILE_POLICY | Archive to cold, or delete outright | cold | delete |
Email, scanning and cron
| Variable | Description | Example |
|---|---|---|
SMTP_HOST / USER / PASS | Relay used for notification email | smtp.example.com |
SMTP_FROM | Sender shown to recipients | CrabS3 <bot@example.com> |
CLAMAV_HOST / CLAMAV_PORT | Scanner reachable from the app container | clamav / 3310 |
CRON_SECRET | Shared secret the expiry job presents | openssl 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
DEFAULTShips in compose.yml β the fastest way to a working instance. Set tiering and replication on the bucket itself.
AWS S3
TESTEDUse a real region and IAM keys scoped to the bucket. A lifecycle rule transitions objects to Glacier on your schedule.
OVH Object Storage
TESTEDThe 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.