Backend Deployment to Railway
This guide provides step-by-step instructions for deploying the DutyCall Laravel backend to Railway with MySQL database.
Overview
Deploy the DutyCall Laravel backend to Railway with automatic CI/CD from GitHub, managed MySQL database, and SSL-enabled public domain.
Time to Deploy: ~45 minutes (first time) Difficulty: Intermediate Prerequisites: Git, GitHub account, Railway account, Twilio account, Google Cloud OAuth
What Railway Provides:
- 🚀 Automatic deployment from GitHub
- 🗄️ Managed MySQL database
- 🔒 Free SSL certificates
- 📊 Built-in monitoring and logs
- 💰 Free tier: $5/month credit + 30 days trial
Prerequisites
Required Accounts
1. Railway Account
- Sign up: https://railway.app
- Connect GitHub account for auto-deployment
- Free tier includes: $5/month credit, 30 days trial
2. GitHub Account
- Repository must be pushed to GitHub
- Railway deploys automatically on push to main branch
3. Twilio Account (for voice features)
- Account SID and Auth Token
- API Key and API Secret
- TwiML App SID
- Phone number
4. Google Cloud Console (for OAuth)
- OAuth Client ID and Secret
- Authorized redirect URIs configured
Required Credentials Checklist
Gather these credentials before starting:
# Twilio Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_PHONE_NUMBER=+1xxxxxxxxxx
TWILIO_API_KEY=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_TWIML_APP_SID=APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Google OAuth Credentials
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxx
- Twilio: Console → Account → API Keys & Tokens
- Google: Cloud Console → APIs & Services → Credentials
Step 1: Prepare Your Repository
1.1 Verify Backend Structure
Ensure your backend directory contains:
backend/
├── Dockerfile # ← Required
├── .dockerignore # ← Required
├── composer.json
├── composer.lock
├── artisan
├── app/
├── config/
├── database/
└── routes/
1.2 Create Dockerfile
File: backend/Dockerfile
FROM php:8.3-cli
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
libzip-dev \
zip \
unzip
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /app
# Copy application files
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Expose port
EXPOSE 8080
# Start command (runs migrations then starts server)
CMD php artisan migrate --force || true && php artisan serve --host=0.0.0.0 --port=${PORT:-8080}
Key Points:
- Uses PHP 8.3 CLI
- Installs all required extensions
- Runs migrations automatically on startup
- Serves on port 8080 (Railway standard)
1.3 Create .dockerignore
File: backend/.dockerignore
.git
.env
.env.*
node_modules
vendor
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*
.phpunit.result.cache
Purpose: Exclude unnecessary files from Docker build (faster builds, smaller images)
1.4 Push to GitHub
cd /path/to/dutycall
git add backend/Dockerfile backend/.dockerignore
git commit -m "Add Railway deployment configuration"
git push origin main
Step 2: Create Railway Project
2.1 Login to Railway
- Navigate to https://railway.app
- Click "Start a New Project"
- Select "Deploy from GitHub repo"
2.2 Connect GitHub Repository
- Click "Deploy from GitHub repo"
- Authorize Railway to access your GitHub account
- Select your repository (e.g.,
yourname/dutycall) - Railway detects the repository automatically
2.3 Configure Service Root Directory
- Railway creates a service automatically
- Click on the service card
- Navigate to Settings tab
- Set Root Directory to:
backend - Railway will detect and use your Dockerfile
Why set root directory?
- If your repo has both frontend and backend, Railway needs to know where the backend code is
- Setting root to
backendtells Railway to build from that directory
Step 3: Add MySQL Database
3.1 Add Database to Project
- In Railway dashboard, click "+ New"
- Select "Database"
- Choose "Add MySQL"
- Railway provisions MySQL automatically (~1 minute)
3.2 Link Database to Backend
Railway auto-creates these environment variables in the MySQL service:
MYSQLHOST- Database hostMYSQLPORT- Database port (usually 3306)MYSQLDATABASE- Database nameMYSQLUSER- Database usernameMYSQLPASSWORD- Database password
You'll reference these in Step 4 using Railway's variable syntax: ${{MySQL.MYSQLHOST}}
Step 4: Configure Environment Variables
4.1 Generate Laravel APP_KEY
Run locally to generate an application key:
cd /path/to/backend
php artisan key:generate --show
Output:
base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Copy this entire string (including base64: prefix).
4.2 Add Variables via Railway Dashboard
Navigate to your backend service → Variables tab → Click "New Variable"
Add each variable below:
App Configuration
APP_NAME=DutyCall
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Important:
APP_DEBUG=falsein production (security)APP_KEYmust be the output fromphp artisan key:generate --show
Database Configuration
DB_CONNECTION=mysql
DB_HOST=${{MySQL.MYSQLHOST}}
DB_PORT=${{MySQL.MYSQLPORT}}
DB_DATABASE=${{MySQL.MYSQLDATABASE}}
DB_USERNAME=${{MySQL.MYSQLUSER}}
DB_PASSWORD=${{MySQL.MYSQLPASSWORD}}
Railway Variable References:
${{MySQL.MYSQLHOST}}automatically references the MySQL service'sMYSQLHOSTvariable- Railway updates these automatically if database credentials change
Twilio Configuration
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_PHONE_NUMBER=+1xxxxxxxxxx
TWILIO_API_KEY=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_TWIML_APP_SID=APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_EDGE=ashburn
Get these from: Twilio Console → Account → API Keys & Tokens
Google OAuth Configuration
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxx
Get these from: Google Cloud Console → APIs & Services → Credentials
Laravel Drivers & Session
SESSION_DRIVER=database
QUEUE_CONNECTION=database
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
LOG_CHANNEL=stack
LOG_LEVEL=debug
Why these settings?
SESSION_DRIVER=database- Store sessions in MySQL (required for Sanctum)QUEUE_CONNECTION=database- Use database for background jobsLOG_LEVEL=debug- Verbose logging for troubleshooting
You'll add more variables in Step 6 after Railway generates your public domain:
APP_URLFRONTEND_URLSANCTUM_STATEFUL_DOMAINSSESSION_DOMAINGOOGLE_CALLBACK_URL
Step 5: Deploy Backend
5.1 Trigger Deployment
Railway automatically deploys when:
- ✅ You push to GitHub (auto-deploy enabled by default)
- ✅ You change environment variables
- ✅ You click "Redeploy" button
First deployment starts immediately after adding environment variables.
5.2 Monitor Deployment
- Navigate to Deployments tab in your service
- Click on the active deployment (status: Building or Running)
- Click "View Logs"
- Wait for successful startup message
Expected deployment time: 3-5 minutes
5.3 Check Deployment Logs
Look for these success indicators in logs:
✅ Building Docker image
✅ Installing Composer dependencies
✅ Running migrations: php artisan migrate --force
✅ Server started: INFO Server running on [http://0.0.0.0:8080]
If deployment fails, check logs for errors and see Troubleshooting section.
Step 6: Generate Public Domain
6.1 Create Railway Domain
- Navigate to your backend service
- Go to Settings tab
- Scroll to Networking section
- Under "Public Networking", enter port:
8080 - Click "Generate Domain"
Railway provides a URL like:
https://dutycall-backend-production.up.railway.app
Copy this URL - you'll need it for the next steps.
6.2 Update Environment Variables with Domain
Add these new variables using your Railway domain:
APP_URL=https://dutycall-backend-production.up.railway.app
FRONTEND_URL=https://your-frontend-url.vercel.app
GOOGLE_CALLBACK_URL=https://dutycall-backend-production.up.railway.app/auth/google/callback
SANCTUM_STATEFUL_DOMAINS=dutycall-backend-production.up.railway.app,your-frontend-url.vercel.app
SESSION_DOMAIN=dutycall-backend-production.up.railway.app
Notes:
- Replace
dutycall-backend-production.up.railway.appwith your actual Railway domain - Update
FRONTEND_URLafter deploying frontend (Step 6.3) - No
https://inSANCTUM_STATEFUL_DOMAINSorSESSION_DOMAIN
6.3 Update After Frontend Deployment
After deploying your frontend to Vercel:
- Update
FRONTEND_URLwith your Vercel URL - Update
SANCTUM_STATEFUL_DOMAINSto include both backend and frontend domains - Railway will automatically redeploy
Step 7: Update Google OAuth
7.1 Add Railway URL to Google Console
- Go to: https://console.cloud.google.com/apis/credentials
- Find your OAuth 2.0 Client ID
- Click Edit
- Add to Authorized JavaScript origins:
https://dutycall-backend-production.up.railway.app - Add to Authorized redirect URIs:
https://dutycall-backend-production.up.railway.app/auth/google/callback - Click "Save"
Why this is required:
- Google only allows OAuth callbacks from whitelisted URLs
- Must match
GOOGLE_CALLBACK_URLenvironment variable exactly
Step 8: Update Twilio Webhooks
8.1 Configure TwiML App
- Go to Twilio Console: https://console.twilio.com
- Navigate to: Voice → TwiML → TwiML Apps
- Select your TwiML App
- Set Voice Request URL:
https://dutycall-backend-production.up.railway.app/api/twilio/agent-dial-queue - Set method to HTTP POST
- Click "Save"
8.2 Configure Phone Number
- Navigate to: Phone Numbers → Active Numbers
- Select your DutyCall phone number
- Scroll to Voice Configuration
- Set A Call Comes In webhook:
https://dutycall-backend-production.up.railway.app/api/twilio/inbound - Set method to HTTP POST
- Click "Save"
Why this is required:
- Twilio sends incoming call events to your backend
- Backend processes calls and returns TwiML instructions
Step 9: Verify Deployment
9.1 Test Backend Health
curl https://dutycall-backend-production.up.railway.app/
Expected response: HTML 404 page (this means Laravel is running correctly)
Why 404 is good:
- Laravel doesn't have a route for
/(root) - Seeing Laravel's 404 page confirms the server is running
9.2 Check Database Connection
View deployment logs to verify database connection:
# If you installed Railway CLI:
railway logs
# Or check in Railway dashboard:
# Service → Deployments → Latest → View Logs
Look for:
Migration table created successfully.
Migrating: 2024_01_01_000000_create_users_table
Migrated: 2024_01_01_000000_create_users_table
9.3 Create Test Users (Optional)
Run seeder to create test accounts:
# Using Railway CLI:
railway run php artisan db:seed --class=RoleTestUsersSeeder
# Or via Railway dashboard:
# Service → Settings → Run Command
Creates test accounts:
super@dutycall.net/password(super_admin)admin@dutycall.net/password(account_admin)manager@dutycall.net/password(dept_manager)agent@dutycall.net/password(agent)
These test accounts should be removed or passwords changed before production use.
Troubleshooting
Build Fails: Missing PHP Extensions
Error:
ext-zip * -> it is missing from your system
Cause: Dockerfile doesn't include required PHP extension
Solution: Ensure Dockerfile includes all extensions:
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
Server Returns 502 Bad Gateway
Possible Causes:
- Database not connected
- Missing
APP_KEY - Server failed to start
Debugging Steps:
- Check Railway logs: Service → Deployments → View Logs
- Verify database variables are linked:
DB_HOST=${{MySQL.MYSQLHOST}} - Ensure
APP_KEYis set and starts withbase64: - Check for migration errors in logs
Common Fix:
# Redeploy with fresh build
railway redeploy
Migration Errors: Table Already Exists
Error:
SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'users' already exists
Cause: Migrations already ran (database has existing tables)
Solution: This is expected behavior. The Dockerfile command handles this:
CMD php artisan migrate --force || true && php artisan serve ...
The || true allows the server to start even if migrations fail (tables already exist).
Session/CSRF Token Errors
Error:
Session store not set on request
CSRF token mismatch
Causes:
SESSION_DRIVERnot set todatabaseSANCTUM_STATEFUL_DOMAINSdoesn't include frontend domain- CORS not configured
Solutions:
- Verify
SESSION_DRIVER=database - Check
SANCTUM_STATEFUL_DOMAINSincludes both backend and frontend domains (nohttps://) - Ensure migrations ran (
sessionstable exists) - Update
config/cors.phpto allow frontend origin
Google OAuth Fails
Error:
redirect_uri_mismatch
Cause: Google Console callback URL doesn't match GOOGLE_CALLBACK_URL
Solution:
- Verify
GOOGLE_CALLBACK_URLin Railway variables - Check Google Console → Credentials → OAuth 2.0 Client → Authorized redirect URIs
- URLs must match exactly (including
https://)
Twilio Webhooks Not Receiving Calls
Error: Calls come in but backend doesn't receive webhook
Debugging:
- Check Railway logs during a test call
- Verify Twilio webhook URL is correct in Twilio Console
- Test webhook URL:
curl https://your-backend.railway.app/api/twilio/inbound - Check Twilio debugger: https://console.twilio.com/debugger
Common Issue: Webhook URL has typo or uses HTTP instead of HTTPS
Railway CLI (Optional)
Install Railway CLI
macOS:
brew install railway
Linux/WSL:
npm install -g @railway/cli
Windows:
npm install -g @railway/cli
Authenticate
railway login
Opens browser for authentication.
Link Project
cd /path/to/backend
railway link
Select: Workspace → Project → Environment → Service
Useful Commands
View logs:
railway logs
railway logs --tail 100 # Last 100 lines
railway logs --follow # Real-time
Check deployment status:
railway status
List environment variables:
railway variables --kv
Set variable:
railway variables --set "APP_DEBUG=false"
Run artisan commands:
railway run php artisan tinker
railway run php artisan migrate
railway run php artisan db:seed
Open Railway dashboard:
railway open
Redeployment
Auto-Deploy (Recommended)
Railway automatically redeploys when you push to GitHub:
git add .
git commit -m "Update backend features"
git push origin main
Railway auto-deploy:
- Detects push to main branch
- Rebuilds Docker image
- Runs migrations
- Deploys new version (~3-5 minutes)
Manual Redeploy
Via Dashboard:
- Go to Railway dashboard
- Click on your service
- Navigate to Deployments tab
- Click "Redeploy" on latest deployment
Via CLI:
railway redeploy
Zero-Downtime Deployments
Railway provides zero-downtime deployments by default:
- New version builds
- Health check passes
- Traffic switches to new version
- Old version terminates
Environment Variable Reference
Complete List of Required Variables
# App Configuration
APP_NAME=DutyCall
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_URL=https://dutycall-backend-production.up.railway.app
# Database (auto-linked from MySQL service)
DB_CONNECTION=mysql
DB_HOST=${{MySQL.MYSQLHOST}}
DB_PORT=${{MySQL.MYSQLPORT}}
DB_DATABASE=${{MySQL.MYSQLDATABASE}}
DB_USERNAME=${{MySQL.MYSQLUSER}}
DB_PASSWORD=${{MySQL.MYSQLPASSWORD}}
# Twilio
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_PHONE_NUMBER=+1xxxxxxxxxx
TWILIO_API_KEY=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_TWIML_APP_SID=APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_EDGE=ashburn
# Google OAuth
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxx
GOOGLE_CALLBACK_URL=https://dutycall-backend-production.up.railway.app/auth/google/callback
# Session & CORS
SESSION_DRIVER=database
SANCTUM_STATEFUL_DOMAINS=dutycall-backend-production.up.railway.app,your-frontend.vercel.app
SESSION_DOMAIN=dutycall-backend-production.up.railway.app
FRONTEND_URL=https://your-frontend.vercel.app
# Laravel Drivers
QUEUE_CONNECTION=database
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
LOG_CHANNEL=stack
LOG_LEVEL=debug
Variable Format Guide
Railway Service References:
DB_HOST=${{MySQL.MYSQLHOST}}
# Format: ${{ServiceName.VARIABLE_NAME}}
Domain Variables (no protocol):
SANCTUM_STATEFUL_DOMAINS=backend.railway.app,frontend.vercel.app
SESSION_DOMAIN=backend.railway.app
# No https:// prefix
URL Variables (with protocol):
APP_URL=https://backend.railway.app
FRONTEND_URL=https://frontend.vercel.app
# Include https:// prefix
Cost Tracking
Railway Pricing
Free Tier:
- $5/month credit
- 30 days trial (no credit card required)
- Includes 1 MySQL database
- Up to 500MB storage
- Up to 1GB transfer
After Free Tier:
- Backend Service: ~$10-15/month
- MySQL Database: ~$5-10/month
- Total: ~$15-25/month
Billing is usage-based:
- CPU usage
- Memory usage
- Disk storage
- Network transfer
Cost Optimization Tips:
- Use
APP_DEBUG=false(reduces log storage) - Optimize database queries
- Use caching (
CACHE_DRIVER=redisfor production)
Twilio Costs
Monthly Costs:
- Phone Number: ~$1.15/month
- Incoming calls: ~$0.0085/minute
- Outgoing calls: ~$0.013/minute
- SMS (if used): ~$0.0075/message
Example Monthly Cost:
- Phone number: $1.15
- 1,000 inbound minutes: $8.50
- 500 outbound minutes: $6.50
- Total: ~$16/month
Cost Tracking:
- Monitor in Twilio Console → Usage
- Set billing alerts in Twilio account
- Review monthly invoices
Security Checklist
Before going to production, verify:
-
APP_DEBUG=false(prevents sensitive error display) -
APP_KEYis strong and unique (generated viaphp artisan key:generate) - Database credentials are Railway-managed (not hardcoded)
- HTTPS is enabled (Railway provides free SSL)
- CORS configured for production frontend domain only
- OAuth callback URLs whitelisted in Google Console
- Twilio webhook URLs use HTTPS (not HTTP)
- Environment variables never committed to Git (.env in .gitignore)
- Test user accounts removed or passwords changed
- File permissions secure (storage/ writable only)
Additional Security:
- Enable Railway's built-in DDoS protection
- Set up rate limiting in Laravel (
throttlemiddleware) - Use Twilio signature validation for webhooks
- Implement API authentication (Sanctum tokens)
Monitoring & Maintenance
View Logs
Via Railway CLI:
railway logs --tail 100 # Last 100 lines
railway logs --follow # Real-time logs
Via Dashboard: Service → Deployments → Latest → View Logs
Check Deployment Status
railway status
Output:
Service: backend
Status: Running
URL: https://dutycall-backend-production.up.railway.app
Last Deploy: 5 minutes ago
Database Monitoring
View MySQL metrics in Railway: Service → MySQL → Metrics
Monitor:
- CPU usage
- Memory usage
- Disk usage
- Active connections
Database Backup
Railway automatic backups:
- Daily snapshots (retained 7 days)
- Point-in-time recovery available
Manual backup:
railway run php artisan backup:run
Update Dependencies
When updating Laravel or Composer packages:
# Update locally
composer update
# Commit updated lock file
git add composer.lock
git commit -m "Update Laravel dependencies"
git push origin main
# Railway auto-redeploys with new dependencies
Health Checks
Create a health check endpoint in Laravel:
Route: routes/api.php
Route::get('/health', function () {
return response()->json([
'status' => 'healthy',
'database' => DB::connection()->getDatabaseName(),
'timestamp' => now()->toISOString()
]);
});
Test:
curl https://dutycall-backend-production.up.railway.app/api/health
Next Steps
After successful backend deployment:
- Deploy Frontend → Frontend deployment guide (coming soon)
- Configure Custom Domain → Custom domain setup guide (coming soon)
- End-to-End Testing → Production testing guide (coming soon)
- Set Up Monitoring → Monitoring guide (coming soon)
Immediate Action Items:
- Update
FRONTEND_URLafter deploying frontend - Test Google OAuth login flow
- Test Twilio inbound call flow
- Remove or secure test user accounts
Additional Resources
Railway Documentation
Laravel Documentation
Docker Resources
Third-Party Services
Support
If you encounter deployment issues:
- Check Railway logs first:
railway logs - Review environment variables:
railway variables --kv - Consult troubleshooting section above
- Test locally with same environment variables
- Contact development team with:
- Error message from logs
- Steps to reproduce
- Environment configuration (sanitized)
Common Support Channels:
- Railway Discord: https://discord.gg/railway
- DutyCall GitHub Issues: (your repository issues page)
- Development team Slack/email
Deployment successful? 🎉 Your DutyCall backend is now live on Railway! Next, deploy the frontend to complete the full-stack deployment.