Skip to content

Railway

Follow these steps to deploy LeafLock on Railway’s managed platform with full IPv6 support and private networking.

🚅 Railway Platform Benefits

Modern Cloud Infrastructure:

  • IPv6-first private networking
  • Automatic HTTPS certificates
  • Git-based deployments
  • Built-in service discovery
  • WireGuard-based private mesh network

Perfect for LeafLock:

  • Supports multi-service applications
  • Environment variable management
  • Automatic container orchestration
  • Built-in observability

📊 Railway Account

  • Active Railway account
  • GitHub repository access
  • Payment method (if needed)

💾 Repository Access

  • Fork or access to LeafLock repository
  • Understand project structure
  • Environment configuration ready

🌐 IPv6-First Networking

Railway Implementation:

  • All services get IPv6 addresses in private mesh
  • WireGuard VPN for secure inter-service communication
  • Public IPv4/IPv6 endpoints for external access
  • Automatic service discovery via internal hostnames

LeafLock Compatibility:

  • Backend implements IPv6 dual-stack listening
  • Frontend auto-detects Railway service discovery
  • Universal service discovery across deployment platforms

Railway provides internal hostnames for service-to-service communication:

Terminal window
# Public endpoints
frontend: https://leaflock-frontend-production.up.railway.app
backend: https://leaflock-backend-production.up.railway.app
# Private network (IPv6)
frontend: leaflock-frontend.railway.internal
backend: motivated-energy.railway.internal # Railway-generated name
  1. Create Railway Project

    • Go to railway.app
    • Click “New Project”
    • Select “Deploy from GitHub repo”
    • Choose your LeafLock repository
  2. Multi-Service Detection

    • Railway automatically detects services
    • Creates separate services for backend/frontend
    • Configures build contexts correctly

⚡ Backend Service Setup

Service Settings:

  • Name: leaflock-backend
  • Root Directory: /backend
  • Build Command: (auto-detected from Dockerfile)
  • Start Command: (handled by Dockerfile)

Environment Variables (Backend):

Terminal window
# Database Configuration
POSTGRES_PASSWORD=<64-char-secure-password>
# Pull the connection string directly from the Railway Postgres plugin
DATABASE_URL=<RAILWAY_POSTGRES_URL>
PGHOST=<RAILWAY_POSTGRES_HOST>
PGUSER=<RAILWAY_POSTGRES_USER>
PGPASSWORD=<RAILWAY_POSTGRES_PASSWORD>
PGDATABASE=<RAILWAY_POSTGRES_DATABASE>
# Redis Configuration
REDIS_PASSWORD=<32-char-secure-password>
# Use the value injected by the Redis plugin
REDIS_URL=<RAILWAY_REDIS_URL>
# Security Keys (Critical)
JWT_SECRET=<64-character-base64-secret>
SERVER_ENCRYPTION_KEY=<32-character-base64-key>
# Default Admin (set to your own values!)
ENABLE_DEFAULT_ADMIN=true
DEFAULT_ADMIN_EMAIL=ops@your-domain.com
DEFAULT_ADMIN_PASSWORD=<VeryStrongPassword>
# CORS for Railway (include public + preview domains)
CORS_ORIGINS=https://leaflock-frontend-production.up.railway.app,https://leaflock-frontend-main.up.railway.app
# Runtime flags
APP_ENV=production
PORT=8080
ENABLE_REGISTRATION=false # Secure default - must explicitly enable
ENABLE_METRICS=true

Important: Railway treats service variables as both build-time and runtime values. Set these before the first deploy so that migrations, admin bootstrap, and Vite builds use the correct configuration. Also, never leave placeholder hosts like postgres.railway.internal—copy the exact values shown in Railway’s “Variables” panel (for example pg-pleasant-haze-12345.up.railway.app). LeafLock now reads both DATABASE_URL and the plugin’s PG* variables, so attaching the Postgres add-on and using its defaults is enough.

📦 IaC Template

You can codify the entire stack using Railway’s configuration file (railway.json). Define both services, attach the Postgres/Redis plugins, and seed mandatory secrets up-front.

// railway.json (excerpt)
{
"$schema": "https://railway.app/config.v2024.05.31.schema.json",
"project": {
"name": "leaflock",
"services": [
{
"name": "leaflock-backend",
"path": "./backend",
"dockerfilePath": "backend/Dockerfile",
"plugins": [
{ "name": "postgres" },
{ "name": "redis" }
],
"variables": {
"APP_ENV": "production",
"ENABLE_DEFAULT_ADMIN": "true",
"ENABLE_METRICS": "true",
"DEFAULT_ADMIN_EMAIL": "ops@your-domain.com",
"DEFAULT_ADMIN_PASSWORD": "<change-me>"
}
},
{
"name": "leaflock-frontend",
"path": "./frontend",
"dockerfilePath": "frontend/Dockerfile",
"variables": {
"VITE_API_URL": "https://leaflock-backend-production.up.railway.app",
"BACKEND_INTERNAL_URL": "http://leaflock-backend.railway.internal:8080",
"PORT": "80"
}
}
]
}
}

Railway resolves plugin connection variables (e.g. PGHOST, PGPASSWORD, REDIS_URL) automatically at deploy time, so once the template is applied those secrets are injected without any imperative steps. Populate real admin credentials and rotate them through version control or your secrets store.

⚛️ Frontend Service Setup

Service Settings:

  • Name: leaflock-frontend
  • Root Directory: /frontend
  • Build Command: (auto-detected)
  • Port: 80 (auto-detected)

Environment Variables (Frontend):

Terminal window
# Internal proxy target (private mesh)
BACKEND_INTERNAL_URL=http://leaflock-backend.railway.internal:8080
# URL embedded in the SPA (public endpoint)
VITE_API_URL=https://leaflock-backend-production.up.railway.app
# Optional tuning
VITE_ENABLE_ADMIN_PANEL=false
PORT=80
NODE_ENV=production

The frontend entrypoint now auto-detects Railway’s service discovery. Supplying both BACKEND_INTERNAL_URL (private HTTP) and VITE_API_URL (public HTTPS) ensures:

  • Caddy proxies /api traffic across Railway’s private mesh, avoiding CORS issues
  • The built SPA calls the public backend URL when the browser makes direct requests
  • Horizontal scaling works automatically—every replica resolves the latest backend hostname via Caddy’s dynamic DNS
  1. Add PostgreSQL Service

    • Click “Add Service” → “Database” → “PostgreSQL”
    • Railway automatically configures connection
    • Get connection details from service variables
  2. Configuration

    Terminal window
    # Auto-generated by Railway
    DATABASE_URL=postgresql://postgres:password@host:port/railway
    POSTGRES_PASSWORD=auto-generated-password
  1. Add Redis Service

    • Click “Add Service” → “Database” → “Redis”
    • Railway provides connection details
    • Configure password if needed
  2. Configuration

    Terminal window
    # Railway Redis connection
    REDIS_URL=redis.railway.internal:6379
    REDIS_PASSWORD=your-redis-password

LeafLock’s backend automatically detects Railway’s IPv6 environment:

// backend/main.go - IPv6 dual-stack listening
func listenWithIPv6Fallback(app *fiber.App, port string) error {
// Try IPv6 first (Railway's preferred method)
ipv6Addr := fmt.Sprintf("[::]:%s", port)
if err := app.Listen(ipv6Addr); err != nil {
// Fallback to IPv4 if IPv6 fails
ipv4Addr := fmt.Sprintf("0.0.0.0:%s", port)
return app.Listen(ipv4Addr)
}
return nil
}

Logging Output on Railway:

🌐 HTTP server starting on [::]:8080 (IPv6 dual-stack)
✅ Server binding successful on IPv6
🔗 Service accessible via Railway private network

Caddy Reverse Proxy (Critical for Railway)

Section titled “Caddy Reverse Proxy (Critical for Railway)”
FIXED

LeafLock now uses Caddy instead of NGINX to avoid 502 Bad Gateway errors on Railway.

🔴 Why Not NGINX?

The Problem with NGINX on Railway:

  • NGINX caches DNS lookups at startup
  • Railway’s IPv6 private network assigns dynamic IPs on each deployment
  • After deploy, NGINX uses stale IP addresses → 502 Bad Gateway errors

Caddy Solution:

  • ✅ Re-resolves DNS on every request (no caching)
  • ✅ IPv6 native support
  • ✅ Railway’s official recommendation for reverse proxies
  • ✅ Simpler configuration
  • ✅ Automatic X-Forwarded headers

Caddy Configuration:

:{$PORT} {
# Health check endpoint
handle /health {
respond "OK" 200
}
# Proxy API requests to backend
# Caddy automatically does dynamic DNS resolution
handle /api/* {
reverse_proxy {$BACKEND_INTERNAL_URL} {
header_up Host {upstream_hostport}
}
}
# Serve static files
handle {
root * /usr/share/caddy
try_files {path} {path}/ /index.html
file_server
}
# Security headers (automatic)
header {
-Server
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
}
# Compression and logging
encode gzip zstd
log { output stdout }
}

Key Benefits:

  • No resolver directive needed (Caddy does this automatically)
  • No manual X-Forwarded-* headers (Caddy sets these by default)
  • Dynamic DNS resolution on every request (fixes 502 issue)
  • Faster than NGINX for proxy workloads

Note: If upgrading from an older LeafLock version with NGINX, just pull the latest code and redeploy. Railway will automatically use Caddy. No configuration changes needed!

For troubleshooting 502 errors, see: Railway Troubleshooting Guide

The frontend container automatically discovers the backend service. At startup, Caddy:

  • Uses explicit BACKEND_INTERNAL_URL or VITE_API_URL if provided
  • Scans Railway’s injected RAILWAY_SERVICE_* variables to locate the backend
  • Falls back to RAILWAY_PRIVATE_DOMAIN, then RAILWAY_PUBLIC_DOMAIN
  • Normalizes IPv6 addresses for Railway’s private mesh
Terminal window
# Frontend startup logs
Railway internal backend detected
🔗 Using backend: http://motivated-energy.railway.internal:8080
🚀 Caddy server starting on port 80
Dynamic DNS resolution enabled

Because Caddy re-resolves DNS on every request, scaling either service just works—no stale IP issues.

Custom Admin Credentials: keeping the defaults will fail on Railway after the first boot (the bootstrap refuses reused credentials). Always set DEFAULT_ADMIN_EMAIL and DEFAULT_ADMIN_PASSWORD to fresh values before deploying.

  1. Deploy backend first

    Terminal window
    railway up --service backend --root backend

    Wait for migrations to finish and confirm the logs show HTTP server starting....

  2. Deploy frontend

    Terminal window
    railway up --service frontend --root frontend

    The entrypoint log prints the backend URL it detected—verify it matches your private domain.

  3. Smoke-test the stack

    Terminal window
    railway logs backend
    railway logs frontend
    curl https://leaflock-backend-production.up.railway.app/api/v1/health
  4. Scale if needed

    • Backend: enable horizontal auto-scaling in Railway settings or increase numReplicas
    • Frontend: stateless Caddy can scale immediately; every replica auto-discovers the backend via dynamic DNS
    • Postgres/Redis: consider managed tiers for production workloads
Terminal window
# Database
POSTGRES_PASSWORD=KjRZ9xWxZ8HTYmafctWSe0NjgPGbYS_secure_64char
DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres.railway.internal:5432/railway?sslmode=require
# Redis
REDIS_PASSWORD=k1KZ0FNtLc3Fe0b9mYyTCpN2H8vYGJq_secure_32char
REDIS_URL=redis.railway.internal:6379
# Security (Critical!)
JWT_SECRET=DF9PX1buVwyK8dl0LNoOmJPQl3TKjQbfCLF6nZGdJSPNKA3FV5OdJKnTczRv9KW6m5N8nZHsGcKtZJDQpYvXog==
SERVER_ENCRYPTION_KEY=xzkuu7roGO8zsGtPtuYwpT9wY5L3nDfXGbHJMKs3Zzo=
# CORS for Railway
CORS_ORIGINS=https://leaflock-frontend-production.up.railway.app
Terminal window
# Backend Communication
BACKEND_INTERNAL_URL=http://motivated-energy.railway.internal:8080
VITE_API_URL=https://leaflock-backend-production.up.railway.app
# Build Configuration
VITE_ENABLE_ADMIN_PANEL=false
NODE_ENV=production
PORT=3000

🌐 Custom Domain Configuration

  1. Add Custom Domain in Railway

    • Go to your frontend service
    • Click “Settings” → “Domains”
    • Add your custom domain (e.g., leaflock.yourdomain.com)
  2. DNS Configuration

    # DNS Records
    CNAME leaflock.yourdomain.com -> leaflock-frontend-production.up.railway.app
  3. Update Environment Variables

    Terminal window
    # Backend CORS
    CORS_ORIGINS=https://leaflock.yourdomain.com,https://leaflock-frontend-production.up.railway.app
    # Frontend API URL
    VITE_API_URL=https://leaflock-backend-production.up.railway.app

Railway automatically provides SSL certificates for:

  • .up.railway.app subdomains
  • ✅ Custom domains (free Let’s Encrypt certificates)
  • ✅ Automatic renewal

🚀 Deployment Steps

  1. Push to Repository

    Terminal window
    git push origin main
  2. Railway Auto-Deploy

    • Railway detects changes
    • Builds backend and frontend services
    • Deploys to production environment
  3. Monitor Deployment

    • Check build logs in Railway dashboard
    • Verify all services are healthy
    • Test health endpoints
Terminal window
# Frontend health check
curl https://leaflock-frontend-production.up.railway.app/
# Backend health check
curl https://leaflock-backend-production.up.railway.app/api/v1/health
# Expected response
{
"status": "healthy",
"database": "connected",
"redis": "connected",
"version": "1.0.0"
}

🔌 Service Connection Issues

Symptoms:

  • Frontend shows “No available server”
  • Backend connection timeouts

Solutions:

  1. Verify BACKEND_INTERNAL_URL uses Railway internal hostname
  2. Check IPv6 binding logs in backend service
  3. Ensure services are in same Railway project
  4. Verify environment variables are set correctly

🗄️ Database Connection Errors

Symptoms:

  • Backend logs show database connection failures
  • Authentication errors

Solutions:

  1. Check DATABASE_URL format and credentials
  2. Verify PostgreSQL service is running
  3. Ensure sslmode=require for production
  4. Check database service logs

🌐 IPv6 Network Debugging

Verify IPv6 Connectivity:

Terminal window
# Check backend logs for IPv6 binding
Railway logs: "🌐 HTTP server starting on [::]:8080"
# Test internal service discovery
curl -6 http://[ipv6-address]:8080/api/v1/health
# Verify Railway private network
ping6 motivated-energy.railway.internal

Debug Service Discovery:

Terminal window
# Frontend service discovery logs
Railway logs: "✅ Railway internal backend detected"
Railway logs: "🔗 Using backend: http://motivated-energy.railway.internal:8080"
Terminal window
# Debug environment variables in Railway
echo $BACKEND_INTERNAL_URL
echo $VITE_API_URL
echo $CORS_ORIGINS
# Check for missing variables
# JWT_SECRET, SERVER_ENCRYPTION_KEY are critical
# DATABASE_URL, REDIS_URL for data connections

🏎️ Build Performance

  • Use Railway’s build cache effectively
  • Optimize Docker image layers
  • Minimize build context size
  • Use multi-stage builds

🌐 Network Performance

  • Leverage Railway’s private network
  • Use IPv6 for inter-service communication
  • Enable HTTP/2 for public endpoints
  • Configure proper connection pooling
Terminal window
# Railway service configuration
# Set appropriate CPU/memory limits
# Monitor usage in Railway dashboard
# Scale services based on traffic

🔒 Security Checklist

  • Secure Environment Variables

    • Use Railway’s encrypted environment variable storage
    • Rotate secrets regularly
    • Never commit secrets to repository
  • Network Security

    • Use private network for inter-service communication
    • Configure CORS origins properly
    • Enable HTTPS for all public endpoints
  • Database Security

    • Use strong database passwords
    • Enable SSL mode for database connections
    • Regularly backup database
  • Application Security

    • Keep dependencies updated
    • Monitor for security vulnerabilities
    • Use Railway’s security scanning features
Terminal window
# Generate secure secrets for Railway
openssl rand -base64 64 | tr -d '\n' # JWT_SECRET
openssl rand -base64 32 | tr -d '\n' # SERVER_ENCRYPTION_KEY
openssl rand -base64 32 | tr -d '\n' # POSTGRES_PASSWORD
openssl rand -base64 32 | tr -d '\n' # REDIS_PASSWORD

Store every generated value in your team password manager (or another encrypted vault) so you can restore services after rotation or disaster recovery.

📊 Monitoring Features

Built-in Monitoring:

  • Service health and uptime
  • Resource usage (CPU, memory, network)
  • Build and deployment history
  • Real-time logs and metrics

Custom Monitoring:

  • Health check endpoints (/api/v1/health)
  • Prometheus metrics (if enabled)
  • Application-specific logging
Terminal window
# Regular maintenance checklist
1. Monitor service resource usage
2. Review application logs for errors
3. Check database performance metrics
4. Update dependencies regularly
5. Rotate secrets quarterly
6. Monitor storage usage
7. Review and update CORS origins
Terminal window
# Map Docker Compose to Railway
# docker-compose.yml services → Railway services
# .env variables → Railway environment variables
# Volume mounts → Railway persistent storage

Railway simplifies the Kubernetes complexity:

  • Services → Railway services
  • ConfigMaps → Environment variables
  • Secrets → Railway encrypted variables
  • Ingress → Railway domains and SSL

🚅 Git-Based Deployments

  • Automatic deployments on git push
  • Branch-based environments
  • Rollback to previous deployments
  • Deploy previews for pull requests

🔄 Zero-Downtime Deployments

  • Blue-green deployment strategy
  • Health check-based traffic switching
  • Automatic rollback on failures
  • Service dependency management

Railway IPv6 Compatible

LeafLock is fully optimized for Railway’s modern IPv6 infrastructure with automatic service discovery and zero-configuration networking.