Skip to content

Give the suite somewhere to run that is not the deploy host - #120

Open
openipc-ai wants to merge 2 commits into
masterfrom
chore/dev-container
Open

Give the suite somewhere to run that is not the deploy host#120
openipc-ai wants to merge 2 commits into
masterfrom
chore/dev-container

Conversation

@openipc-ai

Copy link
Copy Markdown
Collaborator

First of five PRs rebasing the June 11 relaunch work onto current master. This
one carries no relaunch content — it exists so the four that follow can be
verified locally instead of by pushing and waiting.

What and why

The Gemfile pins Ruby 3.1.7 and the app needs MariaDB, libvips and libheif.
Almost no development machine has that combination, so verification has been
happening in CI or on the deploy host.

compose.yaml + docker/Dockerfile.dev are a dev/test stack, deliberately
separate from the root Dockerfile, which is the production build and should
not grow development concerns. The image carries only the OS libraries the gems
bind to; the tree is bind-mounted and gems live in a volume, so a source edit
never rebuilds anything. Its build context is ./docker, so the daemon is not
handed node_modules on every build.

Three things it has to get right, all found by running it:

  • libvips/libheif from the OS, matching the production runtime stage. The
    vips gem is gone from the Gemfile and must stay gone.
  • Yarn 4 via corepack, because yarn.lock carries the Berry __metadata
    header that a v1 yarn cannot read.
  • RUBYOPT=--disable-error_highlight. Ruby 3.1 activates its bundled
    error_highlight 0.3.0 before Bundler runs, while the development group asks
    for >= 0.4.0, so bin/rails dies in config/boot.rb. Neither the production
    image nor CI hits this — both exclude the development group. This container is
    the only place that installs it, and it has to, since rubocop and i18n-tasks
    live there. Upgrading RubyGems does not help; gem update --system 3.4.22
    was tried and the conflict is unchanged.

Two fixes that fell out

rubocop-performance joins the Gemfile. .rubocop.yml has had
require: rubocop-performance all along, so rubocop could not start at all
without it.

development.rb gets two settings that only ever mattered off-localhost:

  • LAN hosts, written /.*\.local(:\d+)?\z/HostAuthorization matches a
    Regexp against the Host header including the port while Strings and
    IPAddrs are matched port-stripped, so the obvious spelling 403s every request
    to a host:3010.
  • i18n.fallbacks = true, which production already sets. Without it development
    is the only environment that renders "translation missing", so a gap in ru or
    zh looks broken here and invisible there.

Verification

Run in the container, not asserted from reading:

check result
bin/rails test 231 runs, 873 assertions, 0 failures
rubocop runs at all; 742 offences / 111 files (pre-existing baseline)
config/environments/development.rb 4 offences → 3 (lost a stray blank line)
i18n-tasks missing / unused 0 / 102

Note for reviewers: the numbers in bd6fb59's message ("15 offences", "4 missing")
were scoped to that change's touched files. The whole-repo baselines above are
what to measure future changes against, and are now recorded in CLAUDE.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi

The Gemfile pins Ruby 3.1.7 and the app needs MariaDB, libvips and libheif.
Almost no development machine has that combination, so verification has been
happening in CI or on the deploy host -- which means the loop between writing a
change and finding out it fails is a push and a wait.

This adds a dev/test stack: compose.yaml plus docker/Dockerfile.dev, deliberately
separate from the root Dockerfile, which is the production build and should not
grow development concerns. The image carries only the OS libraries the gems bind
to; the tree is bind-mounted and gems live in a volume, so a source edit never
rebuilds anything. Its build context is ./docker rather than the repository root,
so the daemon is not handed node_modules on every build.

Three things it has to get right, all of them found by running it:

  - libvips and libheif come from the OS packages, matching the production
    runtime stage. The 'vips' gem is gone from the Gemfile and must stay gone.
  - Yarn 4, via corepack, because yarn.lock carries the Berry __metadata header
    that a v1 yarn cannot read.
  - RUBYOPT=--disable-error_highlight. Ruby 3.1 activates its own bundled
    error_highlight 0.3.0 before Bundler runs, and the development group asks
    for >= 0.4.0, so bin/rails dies in config/boot.rb with "You have already
    activated error_highlight 0.3.0". Neither the production image nor CI hits
    this because both exclude the development group; this container is the only
    place that installs it, and it has to, since rubocop and i18n-tasks are
    there. Upgrading RubyGems does not help -- gem update --system 3.4.22 was
    tried and the conflict is unchanged.

rubocop-performance joins the Gemfile. .rubocop.yml has had
`require: rubocop-performance` all along, so rubocop could not start at all
without it; that went unnoticed for the same reason the rest of this did.

development.rb gets two settings that only ever mattered off-localhost. LAN
hosts, so the dev server can be opened from a phone -- written
/.*\.local(:\d+)?\z/ because HostAuthorization matches a Regexp against the Host
header including the port, while Strings and IPAddrs are matched port-stripped,
so the obvious spelling 403s every request to a host:3010. And
i18n.fallbacks = true, which production already sets: without it development is
the only environment that renders "translation missing", so a gap in ru or zh
looks broken here and invisible there.

Verified in the container: 231 runs, 873 assertions, 0 failures. rubocop runs
(742 offences over 111 files, the pre-existing baseline); development.rb goes
from 4 offences to 3, having lost a stray blank line next to the new stanza.
i18n-tasks reports 0 missing and 102 unused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add containerized Rails development and test stack

✨ Enhancement 🐞 Bug fix ⚙️ Configuration changes 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds reproducible Rails development and testing with Ruby, MariaDB, Node, and native libraries.
• Persists dependencies and bind-mounts source for fast iteration without changing production
 images.
• Fixes RuboCop startup, LAN host access, and development translation fallback behavior.
Diagram

graph TD
  DEV["Developer"] --> COMPOSE["Compose Stack"] --> WEB["Rails Web"] --> DB[("MariaDB")]
  IMAGE["Dev Image"] --> WEB
  SOURCE["Source Tree"] --> WEB
  VOLUMES[("Named Volumes")] --> WEB
  SETTINGS["Dev Settings"] --> WEB
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reuse a production Dockerfile target
  • ➕ Reduces duplicated operating-system package declarations
  • ➕ Keeps Ruby and Node setup in one build definition
  • ➖ Couples development tooling to production image evolution
  • ➖ Complicates bind-mounted source and development-gem installation
  • ➖ Raises the risk of shipping development concerns
2. Adopt an editor devcontainer
  • ➕ Provides one-click IDE onboarding
  • ➕ Can standardize extensions and editor settings
  • ➖ Couples the workflow to devcontainer-aware tooling
  • ➖ Still requires Compose or equivalent database orchestration
  • ➖ Adds another configuration layer over the same runtime

Recommendation: Keep the dedicated Compose stack and development Dockerfile. It preserves production-image isolation while reproducing the exact Ruby, MariaDB, Yarn, and native-library environment; an editor devcontainer can later wrap this stack without replacing it.

Files changed (6) +182 / -1

Bug fix (1) +3 / -0
GemfileDeclare the configured RuboCop performance extension +3/-0

Declare the configured RuboCop performance extension

• Adds 'rubocop-performance' to the development bundle so the existing RuboCop configuration can load successfully.

Gemfile

Documentation (1) +5 / -1
CLAUDE.mdDocument containerized verification commands and lint baseline +5/-1

Document containerized verification commands and lint baseline

• Adds Docker Compose usage for tests and development tools, clarifies separation from the production image, and records the existing whole-repository RuboCop baseline.

CLAUDE.md

Other (4) +174 / -0
compose.yamlOrchestrate local Rails and MariaDB services +71/-0

Orchestrate local Rails and MariaDB services

• Defines a MariaDB 11.8 service and a development web service with health-gated startup, TCP database configuration, port 3010 exposure, bind-mounted source, and persistent dependency volumes.

compose.yaml

development.rbAlign remote development behavior with production +17/-0

Align remote development behavior with production

• Allows named '.local' LAN hosts with optional ports and enables I18n fallback behavior so untranslated locale keys render consistently with production.

config/environments/development.rb

Dockerfile.devBuild the dedicated Rails development runtime +74/-0

Build the dedicated Rails development runtime

• Creates a Ruby 3.1.7 development image with Node 20, Yarn 4 via Corepack, MariaDB build tooling, and system libvips/libheif support. It persists Bundler artifacts externally and disables Ruby's startup 'error_highlight' activation to avoid the development dependency conflict.

docker/Dockerfile.dev

db-init.sqlGrant access to parallel Rails test databases +12/-0

Grant access to parallel Rails test databases

• Creates the base test database with the expected collation and grants the application user access to the escaped 'openipc_' database namespace used by parallel Minitest workers.

docker/db-init.sql

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 25, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Root owns generated host files ✓ Resolved 🐞 Bug ☼ Reliability
Description
The web image never switches away from the base image's root user while the repository is
bind-mounted at /app, so Rails generators, logs, and Yarn asset builds create root-owned files in
the host checkout. Subsequent host-side tools can then fail to edit or overwrite those files until
ownership is repaired manually.
Code

compose.yaml[59]

+      - .:/app
Evidence
compose.yaml mounts the host checkout at /app and runs Rails plus documented Yarn build commands
there, while docker/Dockerfile.dev ends after WORKDIR without creating or selecting a user. The
repository's production image explicitly creates an unprivileged account, fixes writable-directory
ownership, and selects it, demonstrating that user selection is otherwise required.

compose.yaml[50-63]
docker/Dockerfile.dev[69-74]
Dockerfile[109-115]
package.json[17-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The development container runs as root while writing into the bind-mounted repository, leaving generated source, logs, and assets owned by root on Linux hosts.
## Issue Context
The production Dockerfile explicitly creates and selects an unprivileged user, but the development Dockerfile ends without a `USER` directive. Account for writable named volumes when introducing user mapping.
## Fix Focus Areas
- compose.yaml[58-63]
- docker/Dockerfile.dev[69-74]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread compose.yaml
The stack as first written ran as root, so everything it put into the
bind-mounted tree -- app/assets/builds, log/, tmp/, the output of
yarn build:fonts -- landed on the host owned by root. Using it for one afternoon
left public/fonts/ owned by nobody:nogroup and a git checkout that could not
remove it: "warning: unable to unlink", twenty times.

The image now creates a user and runs as it, and chowns /bundle to it so bundler
can still install into the volume. The ids come from DEV_UID/DEV_GID build args
defaulting to 1000, which is the usual first user on a Linux desktop; anyone
else writes them into a .env beside compose.yaml, which Compose reads on its own
and which is now gitignored.

The node_modules named volume goes. A named volume is created root-owned and
Docker seeds it from the image, so an empty one over a path the image does not
have stays root-owned and yarn fails on the first link step with EACCES. /bundle
does not have this problem precisely because the image creates and chowns it
before the volume is populated from it. node_modules lives in the bind mount
instead, where it belongs to the developer and is already gitignored.

Migrating an existing checkout needs one command, since the old root-owned files
resist the new user:

  docker compose run --rm --user 0:0 web chown -R "$(id -u):$(id -g)" \
    /app/.yarn /app/node_modules /app/app/assets/builds /app/log /app/tmp

Verified: container reports uid 1002, a file it touches in the tree reads
ai:ai on the host and deletes without sudo, and the suite is unchanged at
231 runs, 873 assertions, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant