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:
frontend: https://leaflock-frontend-production.up.railway.app
backend: https://leaflock-backend-production.up.railway.app
frontend: leaflock-frontend.railway.internal
backend: motivated-energy.railway.internal # Railway-generated name
Create Railway Project
Go to railway.app
Click “New Project”
Select “Deploy from GitHub repo”
Choose your LeafLock repository
Multi-Service Detection
Railway automatically detects services
Creates separate services for backend/frontend
Configures build contexts correctly
Add Services Manually
Click “Add Service” → “GitHub Repo”
Add backend service (root path: /backend
)
Add frontend service (root path: /frontend
)
Configure build settings per service
⚡ 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):
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_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
ENABLE_REGISTRATION = false # Secure default - must explicitly enable
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 " ,
"name" : " leaflock-backend " ,
"dockerfilePath" : " backend/Dockerfile " ,
"ENABLE_DEFAULT_ADMIN" : " true " ,
"ENABLE_METRICS" : " true " ,
"DEFAULT_ADMIN_EMAIL" : " ops@your-domain.com " ,
"DEFAULT_ADMIN_PASSWORD" : " <change-me> "
"name" : " leaflock-frontend " ,
"dockerfilePath" : " frontend/Dockerfile " ,
"VITE_API_URL" : " https://leaflock-backend-production.up.railway.app " ,
"BACKEND_INTERNAL_URL" : " http://leaflock-backend.railway.internal:8080 " ,
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):
# 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
VITE_ENABLE_ADMIN_PANEL = false
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
Add PostgreSQL Service
Click “Add Service” → “Database” → “PostgreSQL”
Railway automatically configures connection
Get connection details from service variables
Configuration
# Auto-generated by Railway
DATABASE_URL = postgresql://postgres:password@host:port/railway
POSTGRES_PASSWORD = auto-generated-password
# For external database (recommended for production)
DATABASE_URL = postgresql://user:password@your-db-host:5432/leaflock? sslmode = require
POSTGRES_PASSWORD = your-external-db-password
Add Redis Service
Click “Add Service” → “Database” → “Redis”
Railway provides connection details
Configure password if needed
Configuration
# Railway Redis connection
REDIS_URL = redis.railway.internal:6379
REDIS_PASSWORD = your-redis-password
REDIS_URL = your-redis-host:6379
REDIS_PASSWORD = your-external-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 )
Logging Output on Railway:
🌐 HTTP server starting on [::]:8080 (IPv6 dual-stack)
✅ Server binding successful on IPv6
🔗 Service accessible via Railway private network
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:
# Proxy API requests to backend
# Caddy automatically does dynamic DNS resolution
reverse_proxy {$BACKEND_INTERNAL_URL} {
header_up Host {upstream_hostport}
try_files {path} {path}/ /index.html
# Security headers (automatic)
X-Content-Type-Options "nosniff"
# Compression and logging
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
✅ 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.
Deploy backend first
railway up --service backend --root backend
Wait for migrations to finish and confirm the logs show HTTP server starting...
.
Deploy frontend
railway up --service frontend --root frontend
The entrypoint log prints the backend URL it detected—verify it matches your private domain.
Smoke-test the stack
curl https://leaflock-backend-production.up.railway.app/api/v1/health
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
POSTGRES_PASSWORD = KjRZ9xWxZ8HTYmafctWSe0NjgPGbYS_secure_64char
DATABASE_URL = postgresql://postgres: ${ POSTGRES_PASSWORD } @postgres.railway.internal:5432/railway? sslmode = require
REDIS_PASSWORD = k1KZ0FNtLc3Fe0b9mYyTCpN2H8vYGJq_secure_32char
REDIS_URL = redis.railway.internal:6379
JWT_SECRET = DF9PX1buVwyK8dl0LNoOmJPQl3TKjQbfCLF6nZGdJSPNKA3FV5OdJKnTczRv9KW6m5N8nZHsGcKtZJDQpYvXog = =
SERVER_ENCRYPTION_KEY = xzkuu7roGO8zsGtPtuYwpT9wY5L3nDfXGbHJMKs3Zzo =
CORS_ORIGINS = https://leaflock-frontend-production.up.railway.app
ENABLE_REGISTRATION = false # Now defaults to false for security
ENABLE_DEFAULT_ADMIN = true
DEFAULT_ADMIN_EMAIL = admin@leaflock.app
DEFAULT_ADMIN_PASSWORD = SecureAdminPassword2024!
BACKEND_INTERNAL_URL = http://motivated-energy.railway.internal:8080
VITE_API_URL = https://leaflock-backend-production.up.railway.app
VITE_ENABLE_ADMIN_PANEL = false
🌐 Custom Domain Configuration
Add Custom Domain in Railway
Go to your frontend service
Click “Settings” → “Domains”
Add your custom domain (e.g., leaflock.yourdomain.com
)
DNS Configuration
CNAME leaflock.yourdomain.com -> leaflock-frontend-production.up.railway.app
Update Environment Variables
CORS_ORIGINS = https://leaflock.yourdomain.com,https://leaflock-frontend-production.up.railway.app
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
Push to Repository
Railway Auto-Deploy
Railway detects changes
Builds backend and frontend services
Deploys to production environment
Monitor Deployment
Check build logs in Railway dashboard
Verify all services are healthy
Test health endpoints
curl https://leaflock-frontend-production.up.railway.app/
curl https://leaflock-backend-production.up.railway.app/api/v1/health
🔌 Service Connection Issues
Symptoms:
Frontend shows “No available server”
Backend connection timeouts
Solutions:
Verify BACKEND_INTERNAL_URL
uses Railway internal hostname
Check IPv6 binding logs in backend service
Ensure services are in same Railway project
Verify environment variables are set correctly
🗄️ Database Connection Errors
Symptoms:
Backend logs show database connection failures
Authentication errors
Solutions:
Check DATABASE_URL
format and credentials
Verify PostgreSQL service is running
Ensure sslmode=require
for production
Check database service logs
🌐 IPv6 Network Debugging
Verify IPv6 Connectivity:
# 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:
# Frontend service discovery logs
Railway logs: " ✅ Railway internal backend detected "
Railway logs: " 🔗 Using backend: http://motivated-energy.railway.internal:8080 "
# Debug environment variables in Railway
echo $BACKEND_INTERNAL_URL
# 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
# Railway service configuration
# Set appropriate CPU/memory limits
# Monitor usage in Railway dashboard
# Scale services based on traffic
🔒 Security Checklist
# 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
# 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
7. Review and update CORS origins
# Map Docker Compose to Railway
# docker-compose.yml services → Railway services
# .env variables → Railway environment variables
# Volume mounts → Railway persistent storage
# Export from existing database
pg_dump old_database > backup.sql
# Import to Railway PostgreSQL
psql $DATABASE_URL < backup.sql
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.