# Flask + PostgreSQL + Redis on Railway - Deployment Template

This is the **proven setup** that works for deploying Flask apps to Railway. Use this for future projects.

## Tech Stack

- **Backend**: Flask 3.0 + SQLAlchemy
- **Database**: PostgreSQL 15+
- **Session Store**: Redis (optional - falls back to filesystem)
- **Deployment**: Railway.app
- **Server**: Gunicorn

---

## Essential Files for Railway Deployment

### 1. `runtime.txt`
```
python-3.11.0
```

### 2. `requirements.txt`
```
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.5
Flask-Session==0.6.0
Flask-Limiter==3.5.0
psycopg2-binary==2.9.9
redis==5.0.1
requests==2.31.0
gunicorn==21.2.0
python-dotenv==1.0.0
Werkzeug==3.0.1
cachelib==0.10.2
```

### 3. `Procfile`
```
web: gunicorn run:app
```

**Important**: Don't use `release:` commands - they're unreliable on Railway. Use app startup instead.

### 4. `.gitignore`
```
venv/
.env
__pycache__/
*.pyc
.DS_Store
*.db
.vscode/
migrations/versions/*.pyc
```

### 5. `.env.example`
```
FLASK_SECRET_KEY=your-secret-key-here
FLASK_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
REDIS_URL=redis://localhost:6379
APP_DOMAIN=localhost:5000
```

---

## Config Pattern That Works

### `config.py`

```python
import os

class Config:
    """Base configuration."""
    
    SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'dev-secret-key'
    
    # Database
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    if SQLALCHEMY_DATABASE_URI and SQLALCHEMY_DATABASE_URI.startswith('postgres://'):
        # Fix for SQLAlchemy 1.4+
        SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI.replace('postgres://', 'postgresql://', 1)
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # Sessions - MAKE REDIS OPTIONAL
    SESSION_TYPE = 'filesystem'  # Default fallback
    SESSION_PERMANENT = True
    SESSION_USE_SIGNER = True
    SESSION_FILE_DIR = '/tmp/flask_session'
    SESSION_REDIS = None  # Set if Redis available

class ProductionConfig(Config):
    DEBUG = False
    TESTING = False

config = {
    'development': Config,
    'production': ProductionConfig,
    'default': ProductionConfig
}
```

---

## App Factory Pattern

### `app/__init__.py`

```python
import os
import redis
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_session import Session

from config import config

# Initialize extensions
db = SQLAlchemy()
migrate = Migrate()
sess = Session()

def create_app(config_name=None):
    """Application factory pattern."""
    
    if config_name is None:
        config_name = os.environ.get('FLASK_ENV', 'production')
    
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    # CRITICAL: Make Redis optional
    redis_url = os.environ.get('REDIS_URL')
    if redis_url:
        try:
            app.config['SESSION_REDIS'] = redis.from_url(redis_url)
            app.config['SESSION_TYPE'] = 'redis'
        except Exception as e:
            app.logger.warning(f'Redis failed: {e}. Using filesystem.')
            app.config['SESSION_TYPE'] = 'filesystem'
    else:
        app.logger.warning('No Redis. Using filesystem sessions.')
        app.config['SESSION_TYPE'] = 'filesystem'
    
    # Initialize extensions
    db.init_app(app)
    migrate.init_app(app, db)
    sess.init_app(app)
    
    # Register blueprints
    from app.routes import main_bp
    app.register_blueprint(main_bp)
    
    return app
```

---

## CRITICAL: Auto-Create Tables on Startup

### `run.py`

```python
import os
from dotenv import load_dotenv

load_dotenv()

from app import create_app, db

# Create Flask app
app = create_app()

# ⭐ KEY SOLUTION: Auto-create tables on startup
with app.app_context():
    try:
        db.create_all()
        print("✅ Database tables created successfully!")
    except Exception as e:
        print(f"⚠️ Table creation error: {e}")

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port, debug=app.config.get('DEBUG', False))
```

**Why This Works:**
- ✅ Runs every time app starts
- ✅ Creates tables automatically (idempotent - won't overwrite)
- ✅ No separate migration commands needed
- ✅ Works even if `release:` commands fail

---

## Railway Setup Steps

### 1. Create Railway Project

1. Go to https://railway.app
2. Click "New Project" → "Deploy from GitHub repo"
3. Select your repository

### 2. Add PostgreSQL

1. Click "+ New" → "Database" → "PostgreSQL"
2. Railway automatically sets `DATABASE_URL`

### 3. Add Redis (Optional but Recommended)

1. Click "+ New" → "Database" → "Redis"  
2. Railway automatically sets `REDIS_URL`

### 4. Set Environment Variables

In Railway service → Variables:

```
FLASK_SECRET_KEY=<generate-random-32-char-hex>
FLASK_ENV=production
APP_DOMAIN=your-app.railway.app
```

Generate secret key:
```bash
python -c "import secrets; print(secrets.token_hex(32))"
```

### 5. Add Custom Domain (Optional)

1. In Railway service → Settings → Domains
2. Add custom domain
3. Update DNS with provided CNAME

### 6. Deploy

Push to GitHub → Railway auto-deploys!

```bash
git add -A
git commit -m "Initial deployment"
git push origin main
```

---

## Verification Checklist

After deployment, check Railway logs for:

```
✅ Database tables created successfully!
[INFO] Starting gunicorn
[INFO] Listening at: http://0.0.0.0:8080
```

Then visit your app and test!

---

## Common Issues & Solutions

### Issue: Tables Not Creating

**Solution**: Use `db.create_all()` in run.py (not migrations)

```python
with app.app_context():
    db.create_all()
```

### Issue: Redis Connection Errors

**Solution**: Make Redis optional in config:

```python
redis_url = os.environ.get('REDIS_URL')
if redis_url:
    try:
        app.config['SESSION_REDIS'] = redis.from_url(redis_url)
    except:
        app.config['SESSION_TYPE'] = 'filesystem'
```

### Issue: 502 Bad Gateway

**Causes:**
- Missing `DATABASE_URL`
- App not binding to correct port
- Missing environment variables

**Fix**: Check Railway logs and verify all env vars are set.

### Issue: `release:` Commands Don't Run

**Solution**: Don't rely on them! Use app startup code instead (see run.py above).

---

## File Structure Template

```
your-project/
├── app/
│   ├── __init__.py          # App factory with db.init_app()
│   ├── models.py            # SQLAlchemy models
│   ├── routes.py            # Flask blueprints
│   └── templates/           # Jinja2 templates
├── config.py                # Config with Redis fallback
├── run.py                   # Entry point with db.create_all()
├── requirements.txt         # All dependencies
├── Procfile                 # web: gunicorn run:app
├── runtime.txt              # python-3.11.0
├── .env.example             # Template env file
├── .gitignore              # Exclude venv, .env, etc.
└── README.md               # Documentation
```

---

## Quick Deploy Commands

```bash
# 1. Create project
git init
git add -A
git commit -m "Initial commit"

# 2. Push to GitHub
git remote add origin https://github.com/user/repo.git
git push -u origin main

# 3. Deploy on Railway
# - Connect GitHub repo in Railway dashboard
# - Add PostgreSQL and Redis
# - Set FLASK_SECRET_KEY variable
# - Railway auto-deploys!

# 4. Verify
# Check logs for "✅ Database tables created successfully!"
```

---

## Key Takeaways

1. **Always use `db.create_all()` in run.py** - Don't rely on migrations for Railway
2. **Make Redis optional** - Filesystem sessions work fine as fallback
3. **Use `web:` in Procfile, not `release:`** - More reliable
4. **Fix postgres:// → postgresql://** - Required for SQLAlchemy 1.4+
5. **Load dotenv for local development** - Railway injects env vars automatically

---

## Testing Locally Before Deploy

```bash
# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Set up .env file
cp .env.example .env
# Edit .env with local PostgreSQL connection

# Run app
python run.py
```

Visit: http://localhost:5000

---

## Production Optimizations

1. **Add volume for uploads** (if storing files):
   - Railway → Service → Volumes
   - Mount path: `/app/uploads`

2. **Enable Redis** for better session management

3. **Set resource limits** in Railway settings

4. **Monitor logs** regularly for errors

5. **Set up health checks** (optional)

---

## Conclusion

This template avoids all the common Railway deployment issues:

✅ No migration failures  
✅ No missing psql commands  
✅ No local environment setup needed  
✅ Redis is optional  
✅ Tables auto-create  
✅ Works every time  

**Copy this pattern for all future Flask + Railway projects!**
