Repository navigation
Description
On a fresh Linux deployment, the rustfs container exits on startup, and it takes the API down with it. The compose file bind-mounts ./rustfs/data at /data. That directory is git-ignored, so a fresh clone does not have it, and the Docker daemon creates it owned by root:root with mode 0755. The rustfs/rustfs image runs as the non-root user rustfs (uid 10001), cannot write to /data, and exits with Permission denied. restart: unless-stopped then restarts it in a loop. Storage is on by default (STORAGE_ENABLED=true in .env.template), so the API's StorageService.onModuleInit sends HeadBucket and then CreateBucket to an unreachable rustfs. That rejects during Nest init, so the API never starts listening. An admin who follows the deployment tutorial (git clone, ./scripts/generate-env.sh, docker compose up -d) gets a 502 on every API route and has no indication that file storage is the cause.
Where
docker-compose.yaml:130-144:
rustfs:
image: rustfs/rustfs:${RUSTFS_VERSION:-latest}
# ...
volumes:
- ./rustfs/data:/dataapps/api/src/storage/storage.service.ts:93-103:
async onModuleInit(): Promise<void> {
// ...
try {
await s3.send(new HeadBucketCommand({ Bucket: bucket }));
} catch {
await s3.send(new CreateBucketCommand({ Bucket: bucket }));
}
}The image runs as uid=10001(rustfs), and its /entrypoint.sh only chowns a data directory it creates itself. A bind-mounted /data already exists, so the entrypoint never changes its owner, and a non-root process could not change it anyway.
Reproduce
- On a Linux host, clone the repository, run
./scripts/generate-env.sh, and make sure./rustfs/datadoes not exist. - Run
docker compose up -d. - Run
docker compose logs rustfsanddocker compose ps.
The same failure reproduces without compose. Give the container a root-owned /data:
docker run --rm --tmpfs /data:uid=0,gid=0,mode=755 -e RUSTFS_VOLUMES=/data rustfs/rustfs:latest
# [FATAL] Server runtime failed: Io error: Permission denied (os error 13)With --tmpfs /data:uid=10001,gid=10001,mode=750, the same image starts and /health returns 200.
Actual: rustfs restarts in a loop, logging Permission denied (os error 13). The API cannot finish starting, so the web app shows server errors.
Expected: a fresh docker compose up -d brings up a working stack with storage enabled.
Tests
No unit or Playwright test covers this. The e2e suite starts each app's dev:test with storage off and never runs the compose stack. Verify the fix by running docker compose up -d on a Linux host from a clone with no ./rustfs/data. docker compose ps should then show rustfs as healthy, and curl http://localhost:$APP_PORT/api/v1/setup should return 200.
Suggested fix
Add a one-shot rustfs-init service with a pinned busybox image. It mounts ./rustfs/data:/data and runs chown 10001:10001 /data. Make rustfs depend on it with condition: service_completed_successfully. While there, make the API's depends_on: rustfs use condition: service_healthy, since the healthcheck already exists. Pin RUSTFS_VERSION in .env.template to the version the stack was tested with instead of latest, so the image's user cannot change under an existing deployment.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e). Therustfsservice (docker-compose.yaml:130-154) and.env.template:27have not changed since this was filed.This is not limited to fresh clones. Every Linux deployment that followed the 2.0.0 upgrade steps (
CHANGELOG.md:164-171) got the same root-owned./rustfs/data. Those that kept storage off (CHANGELOG.md:126) still start, withrustfsrestarting in the background.mongobind-mounts its directories the same way (docker-compose.yaml:123-125) without breaking, because its entrypoint starts as root, runsfind -L /data/configdb /data/db \! -user mongodb -exec chown mongodb '{}' +, and only then drops tomongodb.Changes to the suggested fix
image:asrustfs,user: '0:0',entrypoint: ['chown', '-R', 'rustfs:rustfs', '/data']. The runtime stage is Alpine with coreutils, andrustfs:rustfsresolves from the image's own/etc/passwd, so the owner stays right if upstream renumbers the user again (it moved from 1000 to 10001 in November 2025). Make it recursive, as mongo's is, so a./rustfs/datathat root restored from a backup is fixed too.profilesofrustfs(docker-compose.yaml:151-154). A service with noprofilesis always enabled, so it would run on a gateway-only host and create./rustfs/datathere. With fewer profiles,rustfswould depend on a service Compose has not enabled. Leaverestartunset:unless-stoppedrestarts a container whatever its exit code, so the one-shot would loop.condition: service_healthyworks, because the image installscurlfor the healthcheck atdocker-compose.yaml:145-150. The cost: an unhealthyrustfsthen failsdocker compose up -deven withSTORAGE_ENABLED=false, unless the admin applies the override indocs/en/5-reference/5.6-docker.md:63-71..env.template:27and the:-latestfallback atdocker-compose.yaml:131reaches new installs only. Every.envgenerated since a988993 setsRUSTFS_VERSION=latestexplicitly. A note in the commit body does not reach those admins either:scripts/changelog.tscopies only the subject andBREAKING CHANGEnotes into the release notes. To pin existing deployments too, write the tag intodocker-compose.yamland dropRUSTFS_VERSIONfrom.env.template. An admin who wants another tag setsimage:indocker-compose.override.yaml, as5.6-docker.md:40already recommends for local changes. Since this was filed,latesthas moved to 1.0.2 (2026-10-16), 1.0.3 (10-17) and 1.0.4 (10-25). Pin 1.0.4 or later, so that no deployment that already pulledlatestgoes backwards../rustfs/data, which the 2.0.0 notes tell admins to back up (CHANGELOG.md:166,:177).Also in this change
docs/en/5-reference/5.6-docker.md: add the init service to the services table (:12-20) and the profiles table (:30-33, including "all seven services"). Also move it into thestorageprofile in the override at:63-71, or it keeps running on stacks without storage and creates./rustfs/data.fix(api)commit that also helps Development and deployment tutorials leave file storage enabled but unconfigured, so pnpm dev crashes and deployed file uploads target localhost #1787: the barecatchatapps/api/src/storage/storage.service.ts:100turns everyHeadBucketfailure (DNS, connection refused, bad keys) into aCreateBucketcall, and the error that then stops Nest does not mention storage. CallCreateBucketonly onNotFoundfrom@aws-sdk/client-s3, and rethrow anything else as an error namingSTORAGE_ENDPOINT, with the original ascause. The existing test atapps/api/src/storage/__tests__/storage.service.test.ts:159rejects with a plainnew Error('NotFound'), so it has to switch to the SDK class.Tests
No unit or e2e test can reach the compose change, and root
AGENTS.md:49-51exempts onlyscripts/. The API change has the same e2e gap: the api'sdev:testsetsNODE_ENV=test, whichonModuleInitskips (storage.service.ts:94). Both need an explicit exception, as #1792 and #1794 do, unless CI gains a compose job. Today it never starts the stack (release.yaml:28only reads its config). A job onubuntu-latestthat runsgenerate-env.shanddocker compose up -d, then polls/api/v1/setup, would fail onmainnow.In addition to the manual check in the issue, run
up:./rustfs/datathat already holds the bucket;--profile nogateway;./rustfs/dataappears;rustfsis already restart-looping;rustfsnow waits for the chown to succeed, so file sharing that refuseschownwould take storage down.Land this with #1792 if the gateway takes the compose route. The same pattern works there with the gateway image and
chown -R node:node /app/sqlite. It needs-Rbecause the gateway has run as root since 7c40422, so an existinggateway.dbis root-owned.Triage note written with Claude Code. Code references point at
main@d86740e.caoyadong888-wq commentedon Oct 26, 2026
This matches the classic “Docker creates the bind-mount parent as
root:rootbefore the container starts” failure mode on Linux.docker-compose.yamlbind-mounts./rustfs/data→/data, and therustfs/rustfsimage runs as non-root (uid 10001). On a fresh clone that directory does not exist (gitignored), so the daemon creates it asroot:root0755. The process then cannot writeRUSTFS_VOLUMES=/data, exits withPermission denied, andrestart: unless-stoppedcrash-loops it. WithSTORAGE_ENABLED=true(default in.env.template), the API’sStorageService.onModuleInitthen fails talking to an unhealthyrustfs, which is why every API route shows up as 502 with little signal in the app logs.Immediate recovery (no image rebuild)
On the host, before (or after stopping) the stack:
Then bring the API back once rustfs is healthy:
Sanity checks worth pasting back if it still loops:
Compose-side options (pick one)
./rustfs/data:/datawithrustfs_data:/dataand declarevolumes: { rustfs_data: {} }.user: "0:0"service thatchown -R 10001:10001 /databefore rustfs starts (same pattern as many linuxserver images).mkdir -p rustfs/data && sudo chown 10001:10001 rustfs/databefore the firstdocker compose up -d, plus a note that SELinux hosts may also need:Zon the bind.Until (1) or (2) lands, (3) + the recovery commands above unblock fresh Linux installs without disabling storage.
Paid help + https://github.com/caoyadong888-wq/linux-docker-troubleshoot-minilab