3. Preparing for production
The Music Time Machine works on your laptop because your laptop has the right Python, the right packages, the right .env, and a writable directory for the SQLite file. Railway has none of those guarantees by default. This section bridges that gap: a security checklist for the things that change between dev and prod, a Config class that fails fast when a secret is missing, a .env.example template a fresh clone can copy, a requirements.txt Railway will install, and a Gunicorn start command that tells Railway how to run the app. Thirty minutes of preparation that prevents the three failure modes a first deploy will hit: missing secrets, hardcoded localhost URLs, and database resets.
Production security checklist
Security practices that seem optional in development become mandatory in production. Follow these six rules:
- Never Commit Secrets to Git. Your
SPOTIFY_CLIENT_SECRETandSECRET_KEYmust never appear in your repository. Use environment variables. Add.envto.gitignore. If you've already committed secrets, they're in Git history forever. Rotate them immediately. - Use Strong Secret Keys. Flask's
SECRET_KEYprotects session data. Use cryptographically secure random strings, not predictable values like "my-secret-key". Generate with:python -c "import secrets; print(secrets.token_hex(32))". This produces a 64-character hex string suitable for production. - Disable Debug Mode. Flask's debug mode exposes your code, environment variables, and internal stack traces to anyone who triggers an error. Set
FLASK_DEBUG=FalseorENVIRONMENT=productionin production. Check your Config class enforces this. - Require HTTPS for OAuth Callbacks. Spotify and most OAuth providers require HTTPS redirect URIs in production. HTTP works locally but fails in production. Platforms like Railway provide HTTPS automatically. Update your Spotify developer dashboard with
https://URLs before deploying. - Validate All Environment Variables. Applications should refuse to start if required environment variables are missing. Your Config class should validate
SECRET_KEY,SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRET, andSPOTIFY_REDIRECT_URIon initialization. Fail fast with clear error messages. - Use Environment-Based Configuration. The same codebase should work in development and production by reading environment variables. Never hardcode localhost URLs, file paths, or credentials. Use
os.getenv()with sensible defaults for development but required values for production.
The pattern is so predictable it has its own subgenre: a developer pushes AWS credentials to a public GitHub repository, automated scanner bots find the credentials within minutes, and the attacker spins up EC2 instances for cryptocurrency mining before the developer has noticed the leak. Bills in the thousands of dollars per hour are routine.
Git history is permanent. Even if you delete a file containing secrets, it remains in commit history accessible to anyone who clones your repository. Use environment variables from day one, not as an afterthought.
Implement production configuration
Create a configuration module that adapts to different environments automatically. This pattern works across Flask, Django, FastAPI, and most Python web frameworks.
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
class Config:
"""Production-ready configuration with validation."""
def __init__(self):
self.environment = os.getenv('ENVIRONMENT', 'development')
self._validate_and_load()
def _validate_and_load(self):
"""Load and validate all configuration values."""
# Secret key - required in all environments
self.secret_key = os.getenv('SECRET_KEY')
if not self.secret_key:
raise ValueError(
"SECRET_KEY environment variable must be set. "
"Generate with: python -c 'import secrets; print(secrets.token_hex(32))'"
)
# Spotify credentials - required
self.spotify_client_id = os.getenv('SPOTIFY_CLIENT_ID')
self.spotify_client_secret = os.getenv('SPOTIFY_CLIENT_SECRET')
self.spotify_redirect_uri = os.getenv('SPOTIFY_REDIRECT_URI')
if not all([self.spotify_client_id, self.spotify_client_secret, self.spotify_redirect_uri]):
raise ValueError(
"Spotify credentials incomplete. Required: "
"SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REDIRECT_URI"
)
# Database path - environment-specific
if self.environment == 'production':
# Production: use persistent volume
self.database_path = os.getenv('DATABASE_PATH', '/data/music_time_machine.db')
self.debug = False
else:
# Development: use local directory
self.database_path = os.getenv('DATABASE_PATH', 'music_time_machine.db')
self.debug = True
# Ensure database directory exists
db_dir = Path(self.database_path).parent
db_dir.mkdir(parents=True, exist_ok=True)
@property
def is_production(self):
"""Check if running in production environment."""
return self.environment == 'production'
# Create global config instance
config = Config()
What this configuration provides
Fail-fast validation: The application won't start if required variables are missing. This prevents silent failures where your app runs but breaks unpredictably.
Environment detection: The same code adapts to development (local SQLite, debug mode) and production (persistent volume, no debug) by checking the ENVIRONMENT variable.
Clear error messages: When configuration is missing, errors tell you exactly what's wrong and how to fix it. No cryptic exceptions deep in the application.
Secure defaults: Production defaults to secure settings (debug disabled, HTTPS required). Development defaults to convenient settings (local paths, debug enabled).
Use this configuration in your Flask app:
from flask import Flask
from config import config
app = Flask(__name__)
app.secret_key = config.secret_key
app.debug = config.debug
# Use config values throughout your app
DATABASE_PATH = config.database_path
SPOTIFY_CLIENT_ID = config.spotify_client_id
SPOTIFY_CLIENT_SECRET = config.spotify_client_secret
SPOTIFY_REDIRECT_URI = config.spotify_redirect_uri
if __name__ == '__main__':
if config.is_production:
# Production: use Gunicorn (via Procfile or Railway Start Command)
print("Production mode: Use 'gunicorn app:app --bind 0.0.0.0:$PORT' to start")
else:
# Development: use Flask development server
app.run(host='127.0.0.1', port=5000, debug=True)
Create the .env.example template
Document required environment variables in a .env.example file. This template shows other developers (and future you) what configuration the application needs without exposing actual secrets.
# Environment Configuration
# Copy this file to .env and fill in your actual values
# Never commit .env to version control
# Environment type (development or production)
ENVIRONMENT=development
# Flask Secret Key
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=your-secret-key-here
# Spotify API Credentials
# Get from: https://developer.spotify.com/dashboard
SPOTIFY_CLIENT_ID=your-client-id-here
SPOTIFY_CLIENT_SECRET=your-client-secret-here
# Spotify OAuth Redirect URI
# Development: http://127.0.0.1:5000/callback
# Production: https://your-app.up.railway.app/callback
SPOTIFY_REDIRECT_URI=http://127.0.0.1:5000/callback
# Database Configuration
# Development: local path (music_time_machine.db)
# Production: persistent volume path (/data/music_time_machine.db)
DATABASE_PATH=music_time_machine.db
Verify your .gitignore excludes secrets:
# Environment variables with secrets
.env
# Python
__pycache__/
*.py[cod]
*$py.class
venv/
env/
ENV/
# Database
*.db
*.sqlite3
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
Test your .gitignore
Before committing, verify secrets don't appear in staged files: git status should NOT show .env. If it does, run git rm --cached .env to unstage it, then commit your .gitignore changes.
After adding .gitignore, create your actual .env file: cp .env.example .env, then fill in real values. The .env file stays local and never gets committed.
The load_dotenv() call in config.py reads this local file during development. Railway does not use your local .env; it injects the same variable names from the Variables tab in production.
Generate requirements.txt
Railway needs to know which packages your application requires. The requirements.txt file lists every Python package with exact versions to ensure production matches your development environment.
Generate from your virtual environment:
pip freeze > requirements.txt
This creates a file listing every installed package. For the Music Time Machine, your requirements should include:
Flask==3.0.0
Flask-WTF==1.2.1
spotipy>=2.26.0
gunicorn==21.2.0
python-dotenv==1.0.0
requests==2.31.0
Add Gunicorn for production: Flask's development server (flask run) is not designed for production. Gunicorn handles concurrent requests, process management, and graceful restarts. Add it explicitly even if you're not using it locally. python-dotenv is new in this chapter (the load_dotenv() call you added to config.py needs it), so install it now if you don't already have it. If Flask-WTF or any other package from earlier chapters is missing from your freeze, install it before freezing again:
pip install gunicorn python-dotenv Flask-WTF
pip freeze > requirements.txt
Why exact versions matter
Flask==3.0.0 installs exactly version 3.0.0. Without the version pin, Railway might install Flask 3.1.0 or 4.0.0, which could have breaking changes. Your code works locally with 3.0.0 but might break in production with a different version.
Pin versions in production. Use pip freeze to capture exactly what you've tested locally. This eliminates "works on my machine" problems caused by package version mismatches.
Create a Procfile
Railway can accept a Start Command in the service settings, but putting the same command in a Procfile keeps the deployment instruction in your repository. If Railway does not detect the file automatically, paste the command below into the service's Start Command field.
web: gunicorn app:app --bind 0.0.0.0:$PORT
What this means:
web:defines the HTTP process for Procfile-based platformsgunicornis the production WSGI serverapp:appmeans "in the file app.py, find the variable named app" (your Flask instance)--bind 0.0.0.0:$PORTtells Gunicorn to listen on the port Railway assigns at runtime
Save this as a file named Procfile (no extension) in your project root directory, alongside app.py and requirements.txt.
Gunicorn Configuration Options
Default configuration works for most applications. If you need customization, add flags:
--workers 2sets the number of worker processes; start with one or two for a small portfolio app, then scale only if metrics show you need it--timeout 120increases request timeout from 30 to 120 seconds (useful for slow API calls)--bind 0.0.0.0:$PORTbinds to Railway's assigned port explicitly
Commit production configuration
With configuration complete, commit your changes and push to GitHub. Railway deploys automatically when you push to your repository's main branch.
git add config.py .env.example .gitignore requirements.txt Procfile
git commit -m "Add production configuration for deployment"
git push origin main
Before pushing, verify:
git statusdoes NOT show.env(actual secrets stay local).env.exampleexists and contains template values onlyrequirements.txtincludes all runtime dependencies includingFlask-WTF,python-dotenv, andgunicornProcfilehas no file extensionconfig.pyvalidates environment variables on initialization
Your codebase is now ready for production deployment. The next section walks through Railway setup, environment configuration, and going live.