7. Production deployment

The app works locally on PostgreSQL; concurrent writes pass; the dashboard renders. The remaining step is pointing the Railway-hosted Flask app at the Railway PostgreSQL we set up in section 3, running the same schema and migration scripts against the production database, and verifying the live URL behaves like the local one. Rollback is one config change away.

Two things distinguish this deploy from the local work. The credentials come from Railway's dashboard rather than from a local .env, so we list every variable the app needs in one place; and we run the schema and migration scripts from the laptop against Railway's PostgreSQL before the new app code ever serves a request, so the database is populated by the time traffic arrives.

Environment variables on Railway

Railway exposes environment variables via its dashboard; the app reads them through os.getenv(), so no code changes are needed. Set the following on the Railway project (PostgreSQL addon supplies the database values; Spotify Developer dashboard supplies the OAuth values):

Variable Example Value Source
DB_HOST monorail.proxy.rlwy.net Railway PostgreSQL addon
DB_NAME railway Railway PostgreSQL addon
DB_USER postgres Railway PostgreSQL addon
DB_PASSWORD your_railway_password Railway PostgreSQL addon
DB_PORT 12345 Railway PostgreSQL addon
DATABASE_URL postgresql://postgres:<password>@monorail.proxy.rlwy.net:12345/railway Railway PostgreSQL addon
FLASK_SECRET_KEY Random 32-character string Generate with python -c "import secrets; print(secrets.token_hex(32))"
SPOTIPY_CLIENT_ID Your Spotify app client ID Spotify Developer Dashboard
SPOTIPY_CLIENT_SECRET Your Spotify app client secret Spotify Developer Dashboard
SPOTIPY_REDIRECT_URI https://your-app.railway.app/callback Your Railway app URL
Do not reuse the development secret key

Generate a fresh FLASK_SECRET_KEY for production: python -c "import secrets; print(secrets.token_hex(32))". The secret key signs session cookies; a compromised dev key reused in production lets anyone who's seen your repository forge sessions and impersonate users.

Running the schema and migration against Railway

Run the same create_schema.py and migrate_to_postgresql.py from the previous sections, this time with the production environment loaded. Doing this from your laptop (rather than from inside the Railway-hosted Flask process) keeps the migration step independent of the app deploy and means the production database is already populated by the time the new code starts serving requests:

Terminal
# Step 1: Load production environment variables
set -a
source .env.production
set +a

# Step 2: Create schema on production database
python create_schema.py

# Step 3: Migrate data to production database
python migrate_to_postgresql.py

# Step 4: Verify migration
export PGPASSWORD="$DB_PASSWORD"
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT COUNT(*) FROM tracks"
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT COUNT(*) FROM snapshots"

Both row counts coming back equal to the SQLite source confirms the production database is in the same state as the local one. With that done, the Flask app deploy is safe; it will connect to a populated database.

Pre-deploy checklist

Six things must be true before clicking deploy on Railway:

  • Database ready. PostgreSQL provisioned on Railway, schema applied, tracks and snapshots row counts matching SQLite, indexes present (check with \di in psql).
  • Environment variables set. All ten variables from the table above are configured in Railway's dashboard, with a fresh production FLASK_SECRET_KEY and the SPOTIPY_REDIRECT_URI pointing at the Railway app URL.
  • App code on PostgreSQL. Every route uses get_db_connection(), every query uses %s, every write calls conn.commit(), and the pool is initialised in app.py's startup path.
  • Dependencies updated. requirements.txt lists psycopg2-binary alongside the existing Flask dependencies; sqlite3 imports are removed from anything that gets deployed.
  • Spotify OAuth configured. The Spotify developer dashboard has the Railway URL registered as a redirect URI; OAuth scopes match what the app requests.
  • Rollback plan. The legacy SQLite-backed deploy is still buildable from the previous git tag, so reverting is a deploy of an older commit rather than an emergency rewrite.

Post-deploy smoke test

Once Railway reports the deploy as healthy, run through the critical path immediately, not the next morning when the deployment details have faded:

  1. Visit home page: Application loads without errors. No 500 responses.
  2. OAuth flow: Click "Login with Spotify," authorize, get redirected back successfully.
  3. Create playlist: Use your application's playlist creation feature. Verify playlist appears in Spotify.
  4. Dashboard renders: Visit /dashboard. Charts display without JavaScript errors. Data comes from PostgreSQL.
  5. Concurrent users: Have 2-3 friends use the application simultaneously. No "database is locked" errors.
  6. Check Railway logs: No PostgreSQL connection errors. No Python exceptions in logs.

Six green checks and the migration is done as a deployment exercise. The most common Railway-specific failures are predictable: a misspelled environment variable (check the Railway dashboard against the table above), a missing psycopg2-binary in requirements.txt (add it and redeploy), or the pool's maxconn=10 being too low for Railway's tier (raise it). Logs in the Railway dashboard surface all three immediately.

With the deploy stable, the last page in the chapter looks at three PostgreSQL features the new schema unlocks: full-text search across tracks, advanced JSONB queries against raw_json, and using EXPLAIN ANALYZE to confirm the dashboard queries hit their indexes. See section 8.