Skip to content

Railway Deployment

Railway Platform

Railway gives Git-triggered builds, IPv6 private networking, and automatic TLS for public services. LeafLock’s frontend runs publicly while the backend stays private.

Services created inside one Railway project:

  • leaflock-backend (private service, no domain)
  • leaflock-frontend (public domain)
  • PostgreSQL plugin (managed database)
  • Redis plugin (managed cache + rate limiter)

Set strong values before first deploy. Railway injects them at build and runtime.

openssl rand -base64 64 # JWT_SECRET
openssl rand -base64 32 # SERVER_ENCRYPTION_KEY
openssl rand -base64 32 # DEFAULT_ADMIN_PASSWORD
# Railway UI → Backend → Variables
JWT_SECRET=<value>
SERVER_ENCRYPTION_KEY=<value>
DEFAULT_ADMIN_EMAIL=ops@yourdomain.com
DEFAULT_ADMIN_PASSWORD=<value>
ENABLE_REGISTRATION=false
  • Root directory: /backend
  • Deployment type: Dockerfile (auto-detected)
  • Disable public domain (private-only)
DATABASE_URL=$RAILWAY_POSTGRES_URL
POSTGRES_PASSWORD=$RAILWAY_POSTGRES_PASSWORD
REDIS_URL=$RAILWAY_REDIS_URL
REDIS_PASSWORD=$RAILWAY_REDIS_PASSWORD
JWT_SECRET=<from secrets>
SERVER_ENCRYPTION_KEY=<from secrets>
APP_ENV=production
CORS_ORIGINS=https://leaflock-frontend-production.up.railway.app
ENABLE_METRICS=true
ENABLE_DEFAULT_ADMIN=true

The Go server binds to [::]:8080 (backend/server/listener.go) so Railway IPv6 networking works out of the box.

  • Root directory: /frontend
  • Port: 80
  • Assign Railway public domain (e.g., leaflock-frontend-production.up.railway.app)
BACKEND_INTERNAL_URL=http://leaflock-backend.railway.internal:8080
VITE_API_URL=/api/v1
PORT=80
NODE_ENV=production

Caddy (frontend/docker-entrypoint.sh) resolves the .railway.internal hostname on every request, preventing stale IPv6 records.

railway init
railway link
# Deploy backend first (runs migrations, seeds admin/templates)
railway up --service leaflock-backend
# Watch logs until "HTTP server starting on [::]:8080"
# Then deploy frontend
railway up --service leaflock-frontend

Verify health via the public domain:

curl https://leaflock-frontend-production.up.railway.app/health
curl https://leaflock-frontend-production.up.railway.app/api/v1/health/ready

Commit a railway.json (optional) to codify services:

{
"$schema": "https://railway.app/config.v2024.05.31.schema.json",
"project": {
"name": "leaflock",
"services": [
{
"name": "leaflock-backend",
"path": "./backend",
"plugins": [{ "name": "postgres" }, { "name": "redis" }],
"variables": {
"APP_ENV": "production",
"ENABLE_METRICS": "true"
}
},
{
"name": "leaflock-frontend",
"path": "./frontend",
"variables": {
"VITE_API_URL": "/api/v1",
"BACKEND_INTERNAL_URL": "http://leaflock-backend.railway.internal:8080",
"PORT": "80"
}
}
]
}
}

Railway applies plugin variables automatically; only set secrets that are not provided by the plugins.

  • Add domain under frontend → Settings → Domains.
  • Point DNS CNAME to the Railway hostname.
  • Update backend CORS_ORIGINS to include the new domain.
  • Toggle ENABLE_REGISTRATION based on whether sign-ups should be public.
  • 502 from frontend → backend domain mismatch. Ensure BACKEND_INTERNAL_URL points to leaflock-backend.railway.internal:8080.
  • Backend crash loops → secrets missing; check Railway logs for FATAL messages from config.LoadConfig.
  • WebSocket disconnects → Railway load balancer requires sticky sessions; enable them in service settings.
  • Slow first request → backend warms caches on boot (templates, admin, allowlist). Wait for readiness to report status=ready.
  • Secrets generated and stored in Railway variables
  • Backend service healthy (railway logs leaflock-backend)
  • Frontend health endpoint returns 200
  • Admin login works with the configured credentials
  • Metrics available at /metrics if enabled
  • Custom domain (if used) resolves over HTTPS