Skip to main content

Deployment Guide · Docker · Railway · 15 min read

Docker + Railway Deployment for LangChain/FastAPI

Ship your LangChain application to production in under 30 minutes. PostgreSQL, Redis, health checks, and production hardening—all covered.

dockerrailwaylangchainfastapipostgresqlredis

Prerequisites

You need three things before starting: a LangChain application with working FastAPI endpoints, a Railway account connected to your GitHub repository, and Docker installed locally for testing. That's it.

This guide assumes your FastAPI application uses LangChain for LLM orchestration, PostgreSQL for structured data, and Redis for caching and session management. If you're starting from scratch, grab our LangChain + FastAPI Starter Kit — it ships with all of this pre-wired.

Railway is the fastest path from container to cloud. It detects your Dockerfile, provisions PostgreSQL and Redis with a single click, and handles TLS termination automatically. No heroku.yml, no buildpacks, no nginx configs.

Project Structure

A deployable LangChain project has a predictable layout. Railway needs to find your Dockerfile at the repository root. Keep your application code modular—separation between routers, services, and models makes local testing with docker-compose identical to production.

Recommended Directory Layout
├── app/
│   ├── __init__.py
│   ├── main.py              # FastAPI app entrypoint
│   ├── routers/             # API route handlers
│   ├── services/            # LangChain logic
│   └── models/              # Pydantic schemas
├── Dockerfile               # Multi-stage production build
├── docker-compose.yml       # Local dev + testing
├── railway.toml             # Railway deployment config
├── .env.example             # Template for env vars
└── requirements.txt         # Python dependencies

The Dockerfile uses multi-stage builds to keep your production image lean. A base stage installs all dependencies, and a slim stage copies only the runtime artifacts. This slims your final image from 1.5GB to under 300MB.

Docker-Compose Setup

docker-compose.yml is your local production simulation. When your app works here, it works on Railway. Use identical images, identical environment variables, identical network configuration. The only difference: local URLs for databases instead of Railway-provided hosts.

PostgreSQL and Redis are non-negotiable for production LangChain. PostgreSQL stores user sessions, RAG document metadata, and application state. Redis handles LangChain callback events, rate limiting tokens, and temporary LLM response caching that speeds up repeated queries.

docker-compose.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: langchain-api
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/langchain
      - REDIS_URL=redis://redis:6379/0
      - ENVIRONMENT=development
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    image: pgvector/pgvector:pg16
    container_name: langchain-db
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=langchain
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: langchain-redis
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    command: redis-server --appendonly yes
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Notice the health check on the app service. It hits /health every 30 seconds. The depends_on condition ensures your app never starts until PostgreSQL is confirmed healthy. This prevents the cascade of connection errors you get when services start out of order.

The pgvector image matters. If your LangChain app does RAG with vector similarity search, you need pgvector enabled from the start. You cannot add the extension to an existing database without a migration—it's better to start with it.

Implementing the /health Endpoint

The /health endpoint is your deployment contract with Railway. Railway's native health checks poll this endpoint to determine if your container is ready to receive traffic. A returning 200 means healthy. A 503 means your app knows something is wrong and Railway should not route requests to it.

Implement health checks that actually check health. Not just a static 200 response—a real verification that PostgreSQL is reachable, Redis responds to PING, and your LangChain model loader has a valid connection. False health reports cause traffic to route to broken instances.

app/main.py — Health Endpoint
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
from contextlib import asynccontextmanager
from app.services.database import db
from app.services.cache import redis
from app.services.llm import llm

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: verify critical connections
    try:
        await db.connect()
        await redis.ping()
        await llm.initialize()
    except Exception as e:
        print(f"Startup failed: {e}")
    yield
    # Shutdown: clean up connections
    await db.disconnect()

app = FastAPI(title="LangChain API", lifespan=lifespan)

@app.get("/health")
async def health_check():
    checks = {
        "status": "healthy",
        "database": "unknown",
        "redis": "unknown",
        "llm": "unknown",
    }
    status_code = 200

    try:
        await db.execute("SELECT 1")
        checks["database"] = "up"
    except Exception as e:
        checks["database"] = f"down: {e}"
        status_code = 503

    try:
        await redis.ping()
        checks["redis"] = "up"
    except Exception as e:
        checks["redis"] = f"down: {e}"
        status_code = 503

    try:
        checks["llm"] = "up" if llm.is_initialized else "not_initialized"
    except Exception as e:
        checks["llm"] = f"down: {e}"
        status_code = 503

    checks["status"] = "healthy" if status_code == 200 else "degraded"

    return JSONResponse(content=checks, status_code=status_code)

This endpoint returns structured JSON, not just a boolean. When you're debugging a production incident at 2am, you need to know exactly which component failed. The JSON response tells you immediately whether it's the database, Redis, or the LLM service that's causing the issue.

Railway Deployment Steps

Railway deployment happens in five steps. Connect, configure, provision, deploy, verify. Each step is self-service—the entire process takes under 15 minutes for a properly containerized application.

Step 1: Connect Your Repository

Log into Railway and click 'New Project'. Select 'Deploy from GitHub repo' and authorize Railway to access your repository. Railway automatically detects your Dockerfile and language. If it doesn't, you can manually specify the build command and root directory.

Step 2: Add PostgreSQL

In your Railway project dashboard, click 'Add a Database' and select PostgreSQL. Railway provisions a persistent PostgreSQL instance with a connection string available as an environment variable. Copy the connection string—you'll use it to configure your app.

Step 3: Add Redis

Click 'Add a Database' again and select Redis. Railway provides a Redis URL in the same format as your local docker-compose setup. The transition from local to production is seamless because the URL format is identical.

Step 4: Configure Environment Variables

Go to the Variables tab in your Railway service. Add all required environment variables: DATABASE_URL (use the Railway-provided PostgreSQL connection string), REDIS_URL (use the Railway-provided Redis URL), ENVIRONMENT=production, JWT_SECRET, and your LLM provider API keys (OpenAI, Anthropic, etc.).

Step 5: Deploy

Push a commit to your main branch, or click 'Deploy' in the Railway dashboard. Railway builds your Docker image, provisions your services, and runs the container. Watch the build logs for any errors. A successful deployment shows a green 'Deployed' status.

Environment Variables

Environment variables are the only way your app communicates with its infrastructure. In production, your LangChain app has no hardcoded database URLs, no embedded API keys, no localhost references. Everything comes from the environment.

Create a .env.example file that documents every variable. This file lives in version control and shows future developers (including yourself in six months) what environment variables the application needs. Never commit the actual .env file—it contains secrets.

.env.example
# Application
ENVIRONMENT=production
LOG_LEVEL=INFO

# Database
DATABASE_URL=postgresql+asyncpg://user:password@host:5432/dbname

# Redis
REDIS_URL=redis://user:password@host:6379/0

# Authentication
JWT_SECRET=your-256-bit-secret-key-here
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30

# LLM Provider
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...

# LangChain Configuration
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=ls-...
LANGCHAIN_PROJECT=your-project-name

# Optional: Vector Store
USE_PGVECTOR=true

Rotate your JWT_SECRET regularly. In production, generate a new 256-bit key for each deployment environment. Store it in Railway's secrets panel—not in your code, not in your git history.

Production Considerations

Production is not just "set ENVIRONMENT=production." True production readiness means your app handles connection failures gracefully, scales across multiple instances, maintains availability during deployments, and gives you observability into what's happening under load.

Connection Pooling

Use asyncpg's connection pooling for PostgreSQL. Set a reasonable pool size (default 20) to prevent exhausting database connections under load. LangChain's async chains benefit enormously from pooled connections.

Graceful Shutdown

Handle SIGTERM in your FastAPI app. Railway sends SIGTERM before stopping a container—drain in-flight requests, close database connections, and exit cleanly within 30 seconds.

Structured Logging

Log in JSON format for machine parsing. Include trace IDs that correlate requests across services. Railway's log search works best with structured logs you can filter by level and metadata.

Rate Limiting

Implement rate limiting with Redis at the FastAPI middleware level. LangChain apps are expensive—protect your LLM API quota with per-user rate limits that feel fair to users but prevent abuse.

Startup Probes

Configure Railway's startup probe to give your app time to load LangChain models and connect to databases. Set start_period in your docker-compose healthcheck to 60 seconds for model-heavy apps.

Rolling Deployments

Railway supports zero-downtime deployments by default. Your new container starts, passes health checks, then Railway switches traffic. Ensure your /health endpoint accurately reflects readiness.

Ready to ship your LangChain app?

Don't start from scratch. Our LangChain + FastAPI Starter Kit ships with PostgreSQL, Redis, JWT auth, RAG pipeline, SSE streaming, and a complete Docker + Railway deployment configuration—production-ready from day one.

Get the Starter Kit →

Frequently Asked Questions

Why use Docker with Railway for LangChain apps?

Docker ensures your LangChain/FastAPI application runs identically in development and production. Railway eliminates infrastructure complexity—no server management, no nginx configuration, no manual process supervision. You get containerized consistency plus managed cloud deployment in minutes.

Can I use LangChain with serverless on Railway?

Railway's platform is built around persistent containers, not serverless functions. This makes it ideal for LangChain applications because LLM inference can be long-running and stateful. You get guaranteed resources and no cold starts that plague serverless deployments.

How do I handle environment variables securely?

Never commit .env files to version control. Use Railway's dashboard to set environment variables, or pass them via the railway.toml file for non-sensitive defaults. For production, store API keys in Railway's secret management system which encrypts them at rest.

What health check endpoint should I use?

Implement /health as a FastAPI route that checks database connectivity, Redis responsiveness, and returns a 200 status when healthy. Railway's native health checks hit this endpoint to determine if your container is ready to receive traffic.

How do I scale LangChain apps on Railway?

Railway handles horizontal scaling via replica count adjustments. For LangChain apps, ensure your vector database (ChromaDB/PGVector) can handle concurrent reads, and implement Redis for shared caching across instances. Stateless FastAPI endpoints scale effortlessly.

What PostgreSQL extension do I need for LangChain?

If you're using pgvector for vector storage alongside LangChain, enable the pgvector extension in your Railway PostgreSQL database. Run CREATE EXTENSION IF NOT EXISTS vector; during your database initialization script.

How do I debug issues in production on Railway?

Use Railway's built-in log viewer for real-time container logs. For deeper debugging, pipe structured JSON logs from your FastAPI app and use Railway's log search to filter by severity level or trace ID.