Debugging a Full-Stack Application: From Docker Build Errors to Successful Railway Deployment
The Beginning: A Cryptic Import Error
My journey began with what seemed like a simple deployment issue. The Railway logs were showing an import error that didn't make sense:
ModuleNotFoundError: No module named 'langchain_classic'
This was puzzling because I had recently updated all my imports to use the newer langchain packages. My first instinct was to verify the actual state of my local files rather than assuming anything.
Investigation Phase: Trust but Verify
I started by checking the current imports in my document_qa.py file:
grep -n "from langchain" src/document_qa.py
The output confirmed my suspicions - the local file was correct:
5:from langchain_text_splitters import RecursiveCharacterTextSplitter
6:from langchain_openai import AzureOpenAIEmbeddings, AzureChatOpenAI
7:from langchain_community.vectorstores.azuresearch import AzureSearch
8:from langchain.chains import RetrievalQA
9:from langchain_core.prompts import ChatPromptTemplate
No langchain_classic imports anywhere. This meant the issue was likely with cached Docker layers or the deployment process itself.
The First Fix: Rebuilding Without Cache
I decided to rebuild the Docker image completely fresh:
docker build --no-cache -t docqa-test .
The build succeeded after 192.6 seconds. The --no-cache flag was crucial here - it forced Docker to rebuild every layer from scratch, ensuring my updated code was actually being used.
Testing Locally: Discovering the Missing Frontend
After resolving a port conflict (found using docker ps -a | grep 8000), I ran the container:
docker run --rm -p 8000:8000 --env-file .env docqa-test
The backend started successfully:
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000
Testing the health endpoint confirmed the backend was working:
curl http://localhost:8000/health
{"status":"healthy","qa_system_initialized":true}
However, when I checked for available endpoints:
curl -s http://localhost:8000/openapi.json | python3 -c "import json,sys; d=json.load(sys.stdin); [print(p) for p in d.get('paths',{}).keys()]"
Output:
/
/query
/health
The query endpoint worked fine:
curl -X POST http://localhost:8000/query -H "Content-Type: application/json" -d '{"question":"What is Azure OpenAI?"}'
But something was missing - there was no frontend being served. Checking the Dockerfile revealed the issue:
cat Dockerfile
The Dockerfile only built the Python backend. The frontend folder existed with a React app, but it wasn't being built or served.
The Second Fix: Multi-Stage Docker Build
I created a new multi-stage Dockerfile to include the frontend:
# Dockerfile.complete
# Stage 1: Build frontend
FROM node:18-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Python backend with frontend
FROM python:3.11-slim
WORKDIR /app
# ... rest of the configuration
COPY --from=frontend-build /app/frontend/build ./frontend/build
Building this new image took 93.4 seconds and included the frontend build process.
The Third Fix: Route Conflict Resolution
Even with the frontend built, accessing the root URL still returned the API response instead of the React app:
curl -s http://localhost:8000/ | head -10
{"status":"healthy","message":"Document Q&A API"}
The problem was a route conflict. The backend had a root endpoint that was taking precedence over the static file serving. I added static file serving to the backend API:
# Serve React frontend
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
frontend_build = "frontend/build"
if os.path.exists(frontend_build):
app.mount("/static", StaticFiles(directory=f"{frontend_build}/static"), name="static")
@app.get("/{path:path}")
async def serve_react_app(path: str):
file_path = os.path.join(frontend_build, path)
if os.path.exists(file_path) and os.path.isfile(file_path):
return FileResponse(file_path)
return FileResponse(os.path.join(frontend_build, "index.html"))
Then I had to remove the conflicting root API endpoint:
sed -i '/@app.get("\/")$/,/return {"status": "healthy", "message": "Document Q&A API"}/d' backend/api.py
After rebuilding with these changes, the frontend finally served correctly:
curl -s http://localhost:8000/ | head -10
<!doctype html><html lang="en"><head><meta charset="utf-8"/>...
Deploying to Railway: A Fresh Start
With everything working locally, I moved to Railway deployment. To eliminate any cache issues, I completely unlinked the existing project:
railway unlink
Then created a fresh project:
railway init
# Selected: enterprise-document-qa
After deployment with railway up --detach, I linked the service and set the environment variables:
railway link
cat .env | grep -v '^#' | grep '=' | while IFS='=' read -r key value; do echo "--set \"$key=$value\""; done | xargs railway variables
The deployment succeeded, and Railway provided the public URL:
railway domain
# Output: https://enterprise-document-qa-production.up.railway.app
Reflection: Lessons Learned
This debugging journey taught me several valuable lessons:
Never assume - always verify: When the error pointed to import issues, I didn't assume my local files were wrong. I checked them first.
Cache can be the enemy: The
--no-cacheflag in Docker builds is essential when debugging deployment issues.System integration matters: Having a working backend isn't enough - I needed to ensure the frontend was properly built and served.
Route conflicts are subtle: The root API endpoint blocking static file serving was a non-obvious issue that required careful investigation.
Fresh deployments solve mysteries: Sometimes starting fresh with Railway (unlink and reinit) is faster than debugging cached configurations.
CLI Commands Reference for Future Debugging
Here's my toolkit of commands that proved invaluable during this debugging session:
Docker Investigation
# Check running containers and ports
docker ps -a | grep 8000
# Build without cache
docker build --no-cache -t image-name .
# Run container with environment variables
docker run --rm -d -p 8000:8000 --env-file .env container-name
# Check container logs
docker logs container-name 2>&1 | tail -20
# Stop container
docker stop container-name
File and Code Investigation
# Search for specific imports
grep -n "from langchain" src/document_qa.py
# Check file content with line numbers
head -30 filename.py
# Check for specific patterns
grep -A 5 -B 5 "pattern" filename
# List directory contents with details
ls -la directory/
API Testing
# Test health endpoint
curl http://localhost:8000/health
# Get API documentation
curl http://localhost:8000/docs
# List all API endpoints
curl -s http://localhost:8000/openapi.json | python3 -c "import json,sys; d=json.load(sys.stdin); [print(p) for p in d.get('paths',{}).keys()]"
# Test POST endpoint
curl -X POST http://localhost:8000/endpoint -H "Content-Type: application/json" -d '{"key":"value"}'
# Check HTML response
curl -s http://localhost:8000/ | head -10
Railway Deployment
# Check CLI version
railway --version
# Unlink existing project
railway unlink
# Login to Railway
railway login --browserless
# Initialize new project
railway init
# Link to service
railway link
# Deploy
railway up --detach
# Set environment variables
railway variables --set "KEY=value"
# Check status
railway status
# Get deployment URL
railway domain
This experience reinforced my belief that systematic debugging - moving from local verification to containerization to deployment - is the most efficient path to resolution. Each step built upon the previous one, and maintaining clear command outputs helped me track my progress and identify patterns in the issues I encountered.
If you enjoyed this article, you can also find it published on LinkedIn and Medium.