diff --git a/.claude/skills/dev-env/SKILL.md b/.claude/skills/dev-env/SKILL.md index bf01122..7b6af22 100644 --- a/.claude/skills/dev-env/SKILL.md +++ b/.claude/skills/dev-env/SKILL.md @@ -7,23 +7,24 @@ description: Boot, test, or run Rails commands for inbox-web through Docker. Use The native `bundle` install on this machine does not match `Gemfile.lock`, so `bin/rails` and `bundle exec` fail directly on the host. Run everything through the dev container -defined by `Dockerfile.dev` + `compose.yaml`. +defined by `Dockerfile.dev` + `compose.dev.yaml`. That file is intentionally NOT a default +Compose name (production's `docker-compose.yml` keeps that slot), so always pass `-f compose.dev.yaml`. ## First time / after Gemfile changes ```bash -docker compose build web # or: docker build -f Dockerfile.dev -t inbox-web-dev . +docker compose -f compose.dev.yaml build web # or: docker build -f Dockerfile.dev -t inbox-web-dev . ``` ## Common commands ```bash -docker compose up # web server → http://localhost:3000 -docker compose run --rm web bundle exec rspec # full suite -docker compose run --rm web bundle exec rspec spec/lib/unified_storage_service_spec.rb -docker compose run --rm web bin/rubocop -docker compose run --rm web bin/rails console -docker compose run --rm web bin/rails db:prepare +docker compose -f compose.dev.yaml up # web server → http://localhost:3000 +docker compose -f compose.dev.yaml run --rm web bundle exec rspec # full suite +docker compose -f compose.dev.yaml run --rm web bundle exec rspec spec/lib/unified_storage_service_spec.rb +docker compose -f compose.dev.yaml run --rm web bin/rubocop +docker compose -f compose.dev.yaml run --rm web bin/rails console +docker compose -f compose.dev.yaml run --rm web bin/rails db:prepare ``` ## One-off scripts / reproductions @@ -44,6 +45,6 @@ docker run --rm -v "$PWD":/rails -v /tmp/script.rb:/tmp/script.rb \ - The dev SQLite db lives at `storage/development.sqlite3` (a Docker volume, not the host tree) — an empty host `storage/` is normal, not a sign of lost data. - `StorageSetting` uses an encrypted column, so `ACTIVE_RECORD_ENCRYPTION_*` keys must be - set in the environment (compose.yaml provides dev defaults). + set in the environment (compose.dev.yaml provides dev defaults). - Test env uses the `:test` Disk ActiveStorage service; dev/prod use `:unified`. See `CLAUDE.md` → "File storage" before changing storage code. diff --git a/CLAUDE.md b/CLAUDE.md index 18302f8..f6fcb02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,14 +77,18 @@ Raspberry Pi — no external services required. Native `bundle` on this machine is currently broken (Gemfile.lock pins gems that aren't installed for the active Ruby). **Use Docker for anything that boots Rails:** +The dev stack lives in `compose.dev.yaml`. It is **not** a default Compose filename +(that's deliberate — `docker-compose.yml` is the production stack and must stay the +default), so always pass it with `-f`: + ```bash -docker compose up # web → http://localhost:3000 -docker compose run --rm web bundle exec rspec # full test suite -docker compose run --rm web bin/rubocop # style -docker compose run --rm web bin/rails console +docker compose -f compose.dev.yaml up # web → http://localhost:3000 +docker compose -f compose.dev.yaml run --rm web bundle exec rspec # full test suite +docker compose -f compose.dev.yaml run --rm web bin/rubocop # style +docker compose -f compose.dev.yaml run --rm web bin/rails console ``` -`compose.yaml` bind-mounts the source (edits are live) and keeps gems + `storage/` +`compose.dev.yaml` bind-mounts the source (edits are live) and keeps gems + `storage/` (SQLite db + uploads) in named volumes. See `Dockerfile.dev`. If the native toolchain is fixed, the same commands work without the `docker compose run` diff --git a/Dockerfile.dev b/Dockerfile.dev index 9d4d996..2e21f0d 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,8 +1,8 @@ # Development-only image. # Production deploy uses ./Dockerfile (consumed by Dokku). # -# Run via: docker compose up -# See: compose.yaml +# Run via: docker compose -f compose.dev.yaml up +# See: compose.dev.yaml # # syntax = docker/dockerfile:1 @@ -32,7 +32,7 @@ RUN apt-get update -qq && \ RUN gem install bundler # Pre-install gems for faster cold starts. The source tree is bind-mounted -# at runtime (see compose.yaml), and /usr/local/bundle is a named volume, +# at runtime (see compose.dev.yaml), and /usr/local/bundle is a named volume, # so gems persist across rebuilds and survive bare `compose down`. COPY Gemfile Gemfile.lock ./ RUN bundle config set --local without "" && \ @@ -47,5 +47,5 @@ ENV RAILS_ENV=development \ EXPOSE 3000 -# Default command — overridden by compose.yaml's `command:` to chain db:prepare. +# Default command — overridden by compose.dev.yaml's `command:` to chain db:prepare. CMD ["bin/rails", "server", "-b", "0.0.0.0"] diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..c51665f --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,45 @@ +# Local development stack. Production deploy uses Dokku via ./Dockerfile. +# +# docker compose up # web on http://localhost:3000 +# docker compose run --rm web bin/rails console +# docker compose run --rm web bundle exec rspec +# +# The source tree is bind-mounted, so host edits are live. Gems live in a +# named volume so they survive `compose down` and aren't reinstalled on every +# boot. App data (SQLite db + uploaded files) lives under storage/ — also a +# named volume so it persists across rebuilds. + +services: + web: + build: + context: . + dockerfile: Dockerfile.dev + # db:prepare is idempotent — creates/migrates the SQLite db on first boot. + command: bash -c "bin/rails db:prepare && bin/rails server -b 0.0.0.0" + ports: + - "3000:3000" + volumes: + - .:/rails + - bundle:/usr/local/bundle + - storage:/rails/storage + environment: + RAILS_ENV: development + # ActiveRecord encryption keys — StorageSetting#config_encrypted is encrypted. + # Override with real values (bin/rails db:encryption:init) for persistent data. + ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: ${ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY:-devkeydevkeydevkeydevkeydevkey01} + ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: ${ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY:-devkeydevkeydevkeydevkeydevkey02} + ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: ${ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT:-devkeydevkeydevkeydevkeydevkey03} + # Cloud storage OAuth (optional — configure via Settings > Storage). + # Null values pass the host env through and stay UNSET when not provided — + # don't use ${VAR:-} here, an empty string makes OAuthManager#env_credential + # treat the var as configured. + DROPBOX_CLIENT_ID: + DROPBOX_CLIENT_SECRET: + GOOGLE_DRIVE_CLIENT_ID: + GOOGLE_DRIVE_CLIENT_SECRET: + ONEDRIVE_CLIENT_ID: + ONEDRIVE_CLIENT_SECRET: + +volumes: + bundle: + storage: