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_SECRET and SECRET_KEY must never appear in your repository. Use environment variables. Add .env to .gitignore. If you've already committed secrets, they're in Git history forever. Rotate them immediately.
  • Use Strong Secret Keys. Flask's SECRET_KEY protects 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=False or ENVIRONMENT=production in 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, and SPOTIFY_REDIRECT_URI on 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.
Security Breach Example

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.

config.py
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:

app.py
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.

.env.example
# 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:

.gitignore
# 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:

Terminal
pip freeze > requirements.txt

This creates a file listing every installed package. For the Music Time Machine, your requirements should include:

requirements.txt
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:

Terminal
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.

Procfile
web: gunicorn app:app --bind 0.0.0.0:$PORT

What this means:

  • web: defines the HTTP process for Procfile-based platforms
  • gunicorn is the production WSGI server
  • app:app means "in the file app.py, find the variable named app" (your Flask instance)
  • --bind 0.0.0.0:$PORT tells 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 2 sets 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 120 increases request timeout from 30 to 120 seconds (useful for slow API calls)
  • --bind 0.0.0.0:$PORT binds 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.

Terminal
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 status does NOT show .env (actual secrets stay local)
  • .env.example exists and contains template values only
  • requirements.txt includes all runtime dependencies including Flask-WTF, python-dotenv, and gunicorn
  • Procfile has no file extension
  • config.py validates environment variables on initialization

Your codebase is now ready for production deployment. The next section walks through Railway setup, environment configuration, and going live.