What We Learned
Railway’s automatic build detection (Railpack) assumes a single service per repository. When deploying multiple services from a monorepo, Railpack fails to detect any language and errors with “no supported languages found.” We discovered a simple but powerful pattern: create a root-level Dockerfile that acts as a proxy, navigating to the correct service subdirectory and building only that service.The Problem
Railway’s Railpack scans a repository’s root directory for build indicators:go.mod→ Go projectpackage.json→ Node.js projectDockerfile→ Use Docker build- etc.
The Solution: Root Dockerfile Proxy
Create a singleDockerfile at the monorepo root that:
- Copies the entire monorepo
- Navigates (
WORKDIR) to the specific service subdirectory - Builds only that service
- Produces a minimal image with only that service’s binary
- ✅ Railpack detects
Dockerfileat root → triggers Docker build - ✅ Docker context includes entire monorepo (no submodule issues)
- ✅ WORKDIR navigation isolates build to one service
- ✅ Final image contains only that service’s binary (minimal)
- ✅ Same pattern reusable for all services (just change WORKDIR)
Key Insights
1. One Dockerfile Per Service, Deployed Via One Root Dockerfile
Instead of maintaining three Dockerfiles (one per service), we maintain:- One generic root Dockerfile (shared template)
- Three service-specific
railway.tomlfiles (small configs)
2. Railway Configuration Is Minimal
Each service only needsSERVICE/railway.toml:
3. Environment Variables Bridge the Deployment Gap
Services deployed to Railway need to know each other’s public URLs:4. Build Failures Are Easy to Diagnose
When deploy fails, the error message is unambiguous:- ❌ “Railpack could not determine language” → Missing root Dockerfile
- ❌ “go: github.com/sibling/package not found” → Cross-repo import issue
- ❌ “health check timeout” → Service doesn’t listen or health endpoint missing
5. Multi-Stage Builds Are Essential
Without multi-stage builds, final Docker images are enormous (500MB+). With multi-stage:Metrics
| Metric | Before | After |
|---|---|---|
| Deploy detection | Railpack tries 8+ languages, fails | Immediate Dockerfile detection |
| Dockerfile maintenance | 3 files (one per service) | 1 file (root) + 3 configs (service-specific) |
| Image size per service | 500MB+ | 20-50MB |
| Deploy time | 10-15 min (build + push + start) | 2-5 min |
| Bandwidth (CD pipeline) | 1.5GB per deploy | 100-200MB per deploy |
Troubleshooting Patterns Discovered
-
Health Check Timeout → Service listens on wrong port or wrong path
- Fix: Add env var for PORT, default to 8080 in code
-
Service Can’t Reach Other Services → Hard-coded localhost URLs
- Fix: Use public HTTPS URLs from Railway domains, not internal networking
-
Build Fails Mysteriously → Working directory issue
- Fix: Use
WORKDIRnavigation carefully; verifygo.modpath in logs
- Fix: Use
-
Two Services Deploy to Same Port → Config collision
- Fix: Each service gets unique internal port (8080, 8090, 3000, etc.)
Unexpected Benefits
- No Monorepo Lock-In: Services can be extracted to separate repos later (just remove the root Dockerfile)
- Faster Iteration: Railway redeploy for one service takes 5 min, not 15 (doesn’t rebuild everything)
- Clear Ownership: Each team owns their service directory + their
railway.toml - Easy Onboarding: New engineers see clear structure: “engine/ builds as Docker image, olly/ builds as Docker image”
Comparison: Other Approaches (Why This Won **)“
| Approach | Pros | Cons |
|---|---|---|
| Root Dockerfile Proxy (ours) | Simple, clean, Railway-native | Copies entire monorepo to Docker context |
| Git submodules | Services stay independent | Git complexity, slow clones |
| Separate repos | Fully independent | Microservices operational overhead |
| Monorepo subdirectory config (railway.json) | Config-first | Railway doesn’t reliably read it |
| Custom build scripts | Flexible | Maintenance burden, harder to debug |
What’s Next for Teams Adopting This
- Create root Dockerfile with service template
- Add service-specific
railway.tomlfiles - Set up environment variables in Railway UI (map inter-service URLs)
- Document the pattern in team wiki (copy our checklist)
- Add to CI/CD: enforce
Dockerfileexists at root before merging
Authored by: OpenCode (Claude Haiku 4-5)
Session Date: 2026-03-30
Context: Nestr Engine, Olly, and Web deployment to Railway
Confidence: High (3/3 services successfully deployed and healthy)