docs/DOCKER_SETUP.md
This guide covers running Woods in a Dockerized Rails application — extraction, MCP server configuration, and troubleshooting.
Woods has a split architecture: extraction requires a booted Rails environment (runs inside the container), but the two MCP servers have different runtime needs.
HOST CONTAINER
───────────────────────────── ──────────────────────
Index Server (27 tools) Rails App
reads JSON from disk bundle exec rake woods:extract
no Rails needed writes to tmp/woods/
▲ │
└──── volume mount ◀──────────────────────┘
Console Server — two modes:
Embedded (9 tools) rake woods:console
MCP client spawns via boots Rails, runs MCP in-process
docker exec -i ────────────────────▶ Tier 1 read-only tools only
Bridge (31 tools)
woods-console-mcp on host bridge.rb inside container
connects via docker exec -i ────────▶ evaluates queries in Rails console
all 4 tiers rolled-back transactions
Why the split? The Index Server reads static JSON files — it doesn't need Rails, ActiveRecord, or any of your app's dependencies. Running it on the host avoids container overhead and makes the extraction output available to any MCP client. The Console Server queries live application state, so it must run inside (or connect to) the Rails environment.
# Gemfile
group :development do
gem 'woods'
end
docker compose exec app bundle install
docker compose exec app bundle exec rails generate woods:install
This creates config/initializers/woods.rb with default configuration.
docker compose exec app bundle exec rails db:migrate
Edit config/initializers/woods.rb inside the container (or on the host if the app directory is volume-mounted):
Woods.configure do |config|
config.output_dir = Rails.root.join('tmp/woods')
end
Run extraction inside the container:
# Full extraction
docker compose exec app bundle exec rake woods:extract
# Incremental (changed files only)
docker compose exec app bundle exec rake woods:incremental
# Framework/gem sources only
docker compose exec app bundle exec rake woods:extract_framework
The extraction output must be accessible on the host for the Index Server to read it. Your docker-compose.yml should volume-mount the Rails app directory (or at least the output directory):
services:
app:
volumes:
- .:/app # Full app mount — output lands at ./tmp/woods/
# OR mount just the output:
# - ./tmp/woods:/app/tmp/woods
After extraction, confirm the output is visible from the host:
ls tmp/woods/manifest.json
If this file doesn't exist on the host, your volume mount isn't configured correctly.
When configuring paths, use the host path for the Index Server and the container path for rake tasks:
| Context | Path | Example |
|---|---|---|
| Rake tasks (inside container) | Container path | /app/tmp/woods |
| Index Server (on host) | Host path | ./tmp/woods or /home/dev/my-app/tmp/woods |
.mcp.json Index Server arg | Host path | Same as above |
The Index Server runs on the host — it reads JSON files, not Rails. Point it at the volume-mounted extraction output using the host path.
woods-mcp-start ./tmp/woods
.mcp.json configuration{
"mcpServers": {
"woods": {
"command": "woods-mcp-start",
"args": ["./tmp/woods"]
}
}
}
The woods-mcp-start wrapper validates the index directory, checks for manifest.json, ensures dependencies are installed, and restarts on failure. Use it instead of woods-mcp directly.
Common mistake: Using the container path (
/app/tmp/woods) in.mcp.json. The Index Server runs on the host — it needs the host-side path to the volume-mounted output.
The Console Server queries live Rails state. There are two modes with different trade-offs.
| Embedded | Bridge | |
|---|---|---|
| Where it runs | Inside container via docker exec -i | woods-console-mcp on host, bridge inside container |
| Config needed | None (just .mcp.json) | console.yml + .mcp.json |
| Tools available | 9 (Tier 1 — read-only) | 31 (all 4 tiers) |
| Setup complexity | Minimal | Moderate |
| Best for | Quick setup, basic queries | Full diagnostics, analytics, guarded operations |
The MCP client spawns docker exec -i directly. The container boots Rails and runs the MCP server in-process. Only Tier 1 read-only tools are available (count, sample, find, pluck, aggregate, association_count, schema, recent, status).
{
"mcpServers": {
"woods-console": {
"command": "docker",
"args": [
"compose", "exec", "-i", "app",
"bundle", "exec", "rake", "woods:console"
]
}
}
}
The
-iflag is required. Without it, stdin is not attached and the MCP protocol cannot communicate with the server.
If you use docker exec (not docker compose exec), provide the exact container name:
{
"mcpServers": {
"woods-console": {
"command": "docker",
"args": [
"exec", "-i", "my_app_web_1",
"bundle", "exec", "rake", "woods:console"
]
}
}
}
The woods-console-mcp binary runs on the host and connects to a bridge process inside the container via docker exec -i. This enables all 4 tool tiers: read-only, domain-aware, analytics, and guarded operations.
Step 1: Create console.yml
# ~/.woods/console.yml
mode: docker
container: my_app_web_1
Find your container name with:
docker ps --format '{{.Names}}'
For Docker Compose, names follow the pattern <project>-<service>-<number> (e.g., my_app-app-1).
Step 2: Configure .mcp.json
{
"mcpServers": {
"woods-console": {
"command": "woods-console-mcp"
}
}
}
The bridge reads ~/.woods/console.yml by default. To use a different path:
{
"mcpServers": {
"woods-console": {
"command": "woods-console-mcp",
"env": {
"CODEBASE_CONSOLE_CONFIG": "/path/to/console.yml"
}
}
}
}
.mcp.json ExampleBoth servers configured together for a Docker environment:
{
"mcpServers": {
"woods": {
"command": "woods-mcp-start",
"args": ["./tmp/woods"]
},
"woods-console": {
"command": "docker",
"args": [
"compose", "exec", "-i", "app",
"bundle", "exec", "rake", "woods:console"
]
}
}
}
This uses the embedded console (9 tools). To use the bridge (31 tools), replace the codebase-console entry:
{
"mcpServers": {
"woods": {
"command": "woods-mcp-start",
"args": ["./tmp/woods"]
},
"woods-console": {
"command": "woods-console-mcp"
}
}
}
Which tasks need Docker and which don't:
| Task | Needs Rails? | Run via |
|---|---|---|
woods:extract | Yes | docker compose exec app bundle exec rake ... |
woods:incremental | Yes | docker compose exec app bundle exec rake ... |
woods:extract_framework | Yes | docker compose exec app bundle exec rake ... |
woods:embed | Yes | docker compose exec app bundle exec rake ... |
woods:embed_incremental | Yes | docker compose exec app bundle exec rake ... |
woods:console | Yes | docker compose exec app bundle exec rake ... |
woods:flow[EntryPoint] | Yes | docker compose exec app bundle exec rake ... |
woods:notion_sync | Yes | docker compose exec app bundle exec rake ... |
woods:validate | No | Host or container |
woods:stats | No | Host or container |
woods:clean | No | Host or container |
Docker Compose generates container names using the pattern <project>-<service>-<number>:
# List all running containers
docker ps --format '{{.Names}}'
# Filter for your app service
docker ps --format '{{.Names}}' | grep app
The project name defaults to the directory name of the docker-compose.yml file. Override it with COMPOSE_PROJECT_NAME or the name: key in docker-compose.yml.
Symptom: ls tmp/woods/manifest.json fails on the host after extraction.
Fix: Ensure your docker-compose.yml volume-mounts the app directory:
volumes:
- .:/app
Then re-run extraction.
Symptom: The Index or Console server doesn't respond.
Check:
docker ps-i flag is present in the args.mcp.json is the host path, not the container path-i flag on docker execSymptom: Console server starts but immediately exits, or MCP client reports "broken pipe."
Fix: Add -i to keep stdin open:
"args": ["compose", "exec", "-i", "app", ...]
Symptom: Error response from daemon: No such container: ...
Fix: Check the actual name with docker ps --format '{{.Names}}' and update your .mcp.json or console.yml.
Symptom: Index Server reports "No manifest.json" even though extraction succeeded.
Fix: The Index Server runs on the host. Use the host-side path:
# Wrong (container path):
"args": ["/app/tmp/woods"]
# Right (host path):
"args": ["./tmp/woods"]
Symptom: MCP client shows JSON parse errors.
Fix: The woods:console rake task redirects stdout to stderr before Rails boots. If you still see issues, check for puts or print calls in your initializers that run before the task captures stdout.
Expected behavior. The embedded console (Option 1) only supports 9 Tier 1 tools. Switch to the bridge (Option 2) for the full 31 tools.
See CONSOLE_MCP_SETUP.md for detailed console server documentation.