Files
kennethreitz 2ee4b57d77 Clean up dependencies and move docs to root directory
- Reduced dependencies from 13 to 5 packages (66% reduction)
- Removed unused packages: pydantic, fastapi, uvicorn, pygments, boto3, pillow, background, markdown
- Kept essential packages: flask, gunicorn, gevent, mistune, pyyaml
- Moved docs/ from data/docs/ to root directory for better organization
- Updated all internal documentation links to reflect new location

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 04:54:57 -04:00

13 KiB

Deployment Guide

Taking your TufteCMS site from development to production

TufteCMS is designed to be deployed easily across various platforms while maintaining performance and reliability in production environments.

Production Readiness: TufteCMS is experimental software. While functional, it may have undiscovered bugs or security considerations. Test thoroughly and monitor closely in production environments.

This guide assumes you have a working site from getting started, organized content from content structure, and any desired customizations from the customization guide.

Production Considerations

Environment Configuration

Create a production configuration class:

# config.py
import os
from pathlib import Path

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-change-in-production'
    DEBUG = False
    TESTING = False
    
    # Cache settings
    CACHE_TYPE = 'simple'  # or 'redis' for production
    CACHE_DEFAULT_TIMEOUT = 3600
    
    # Content settings
    DATA_DIR = Path('data')
    
class DevelopmentConfig(Config):
    DEBUG = True

class ProductionConfig(Config):
    SECRET_KEY = os.environ.get('SECRET_KEY')
    if not SECRET_KEY:
        raise ValueError("SECRET_KEY environment variable must be set")
    
    # Production optimizations
    SEND_FILE_MAX_AGE_DEFAULT = 31536000  # 1 year for static files

WSGI Application

Create a WSGI entry point:

# wsgi.py
from tuftecms import create_app
from config import ProductionConfig

app = create_app(ProductionConfig)

if __name__ == "__main__":
    app.run()

Requirements

Create production requirements:

# requirements.txt
flask>=2.0.0
markdown>=3.4.0
python-frontmatter>=1.0.0
gunicorn>=20.1.0  # WSGI server
gevent>=21.0.0    # Async support

Or with uv:

# pyproject.toml
[project]
dependencies = [
    "flask>=2.0.0",
    "markdown>=3.4.0", 
    "python-frontmatter>=1.0.0",
    "gunicorn>=20.1.0",
    "gevent>=21.0.0"
]

Platform Deployment

Traditional VPS/Server

Using Gunicorn + Nginx

  1. Install dependencies:
# Install system packages
sudo apt update
sudo apt install nginx python3 python3-pip

# Install Python dependencies
pip install -r requirements.txt
  1. Configure Gunicorn:
# gunicorn_config.py
bind = "127.0.0.1:8000"
workers = 4
worker_class = "gevent"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
timeout = 30
keepalive = 2
  1. Create systemd service:
# /etc/systemd/system/tuftecms.service
[Unit]
Description=TufteCMS
After=network.target

[Service]
Type=notify
User=www-data
Group=www-data
RuntimeDirectory=tuftecms
WorkingDirectory=/var/www/tuftecms
Environment=FLASK_ENV=production
ExecStart=/var/www/tuftecms/venv/bin/gunicorn -c gunicorn_config.py wsgi:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target
  1. Configure Nginx:
# /etc/nginx/sites-available/tuftecms
server {
    listen 80;
    server_name your-domain.com;
    
    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;
    
    # SSL configuration
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    
    # Security headers
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    
    # Static files with long cache
    location /static/ {
        alias /var/www/tuftecms/tuftecms/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        gzip_static on;
    }
    
    # Application proxy
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Timeout settings
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Platform-as-a-Service

Heroku

  1. Create Procfile:
web: gunicorn -c gunicorn_config.py wsgi:app
  1. Runtime specification:
# runtime.txt
python-3.11
  1. Environment variables:
heroku config:set SECRET_KEY=your-secret-key
heroku config:set FLASK_ENV=production

Railway

  1. Create railway.toml:
[build]
builder = "NIXPACKS"

[deploy]
startCommand = "gunicorn -c gunicorn_config.py wsgi:app"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
  1. Environment variables via Railway dashboard or CLI:
railway variables set SECRET_KEY=your-secret-key
railway variables set FLASK_ENV=production

Fly.io

  1. Create fly.toml:
app = "your-tuftecms-app"

[build]
  builder = "paketobuildpacks/builder:base"
  buildpacks = ["gcr.io/paketo-buildpacks/python"]

[env]
  FLASK_ENV = "production"
  PORT = "8080"

[[services]]
  http_checks = []
  internal_port = 8080
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20

  [[services.ports]]
    handlers = ["http"]
    port = 80
    force_https = true

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

Static Site Generation

For maximum performance, pre-generate static HTML:

# generate_static.py
from tuftecms import create_app
from pathlib import Path
import os

def generate_static_site():
    """Generate static HTML files from TufteCMS content."""
    app = create_app()
    
    with app.app_context():
        # Get all content URLs
        from tuftecms.core.cache import get_blog_cache
        blog_data = get_blog_cache()
        
        output_dir = Path('static_build')
        output_dir.mkdir(exist_ok=True)
        
        # Generate homepage
        with app.test_client() as client:
            response = client.get('/')
            (output_dir / 'index.html').write_text(response.get_data(as_text=True))
            
            # Generate all content pages
            for post in blog_data['posts']:
                response = client.get(post['url'])
                if response.status_code == 200:
                    post_dir = output_dir / post['url'].strip('/')
                    post_dir.mkdir(parents=True, exist_ok=True)
                    (post_dir / 'index.html').write_text(response.get_data(as_text=True))
            
            # Generate index pages
            for endpoint in ['/archive', '/sidenotes', '/connections', '/search']:
                response = client.get(endpoint)
                if response.status_code == 200:
                    page_dir = output_dir / endpoint.strip('/')
                    page_dir.mkdir(parents=True, exist_ok=True)
                    (page_dir / 'index.html').write_text(response.get_data(as_text=True))

if __name__ == '__main__':
    generate_static_site()

Deploy static files to:

  • Netlify: Drag and drop static_build folder
  • Vercel: vercel --prod static_build
  • GitHub Pages: Push to gh-pages branch
  • S3 + CloudFront: Sync with aws s3 sync

Performance Optimization

Content Delivery

  1. Static asset optimization:
# In production config
SEND_FILE_MAX_AGE_DEFAULT = 31536000  # 1 year

# Compress assets
import gzip
from pathlib import Path

def compress_static_files():
    """Pre-compress static files for nginx gzip_static."""
    static_dir = Path('tuftecms/static')
    for file_path in static_dir.rglob('*'):
        if file_path.suffix in ['.css', '.js', '.html', '.svg']:
            with open(file_path, 'rb') as f_in:
                with gzip.open(f"{file_path}.gz", 'wb') as f_out:
                    f_out.writelines(f_in)
  1. CDN configuration:
# For CloudFlare, AWS CloudFront, etc.
CDN_DOMAIN = 'https://cdn.your-domain.com'

@app.template_filter('cdn_url')
def cdn_url(filename):
    """Convert static URLs to CDN URLs."""
    if app.config.get('USE_CDN'):
        return f"{CDN_DOMAIN}/static/{filename}"
    return url_for('static', filename=filename)

Caching Strategy

  1. Redis caching for production:
# Production config
import redis

CACHE_TYPE = 'redis'
CACHE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')

# Initialize Redis cache
redis_client = redis.from_url(CACHE_REDIS_URL)
  1. Cache warming on deployment:
# In your deployment script
python -c "
from tuftecms import create_app
from tuftecms.app import warm_caches
app = create_app()
with app.app_context():
    warm_caches()
"

Database Considerations

TufteCMS is file-based, but for larger sites consider:

# Optional: SQLite for search indexing
import sqlite3
from pathlib import Path

def create_search_index():
    """Create full-text search database."""
    db_path = Path('search_index.db')
    conn = sqlite3.connect(db_path)
    
    conn.execute('''
        CREATE VIRTUAL TABLE IF NOT EXISTS content_fts 
        USING fts5(title, content, url, tags)
    ''')
    
    # Populate from content cache
    # Implementation depends on your needs
    
    conn.close()

Monitoring and Maintenance

Health Checks

# In blueprints/main.py
@main_bp.route('/health')
def health_check():
    """Comprehensive health check."""
    try:
        from ..core.cache import get_blog_cache
        blog_data = get_blog_cache()
        
        return jsonify({
            'status': 'healthy',
            'timestamp': datetime.now().isoformat(),
            'content_stats': blog_data.get('stats', {}),
            'version': '1.0.0'
        })
    except Exception as e:
        return jsonify({
            'status': 'unhealthy', 
            'error': str(e),
            'timestamp': datetime.now().isoformat()
        }), 500

Logging

# In app.py
import logging
from logging.handlers import RotatingFileHandler

def configure_logging(app):
    """Configure production logging."""
    if not app.debug:
        # File handler
        file_handler = RotatingFileHandler(
            'logs/tuftecms.log', 
            maxBytes=10240000, 
            backupCount=10
        )
        file_handler.setFormatter(logging.Formatter(
            '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
        ))
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)
        
        app.logger.setLevel(logging.INFO)
        app.logger.info('TufteCMS startup')

Backup Strategy

#!/bin/bash
# backup.sh - Content backup script

DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/tuftecms"
CONTENT_DIR="/var/www/tuftecms/data"

# Create backup directory
mkdir -p "$BACKUP_DIR"

# Backup content with compression
tar -czf "$BACKUP_DIR/content_$DATE.tar.gz" -C "$CONTENT_DIR" .

# Keep only last 30 backups
find "$BACKUP_DIR" -name "content_*.tar.gz" -type f -mtime +30 -delete

# Optional: Upload to S3, rsync to remote server, etc.
# aws s3 cp "$BACKUP_DIR/content_$DATE.tar.gz" s3://your-backup-bucket/

SSL/TLS Configuration

Let's Encrypt with Certbot

# Install certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d your-domain.com

# Auto-renewal (add to cron)
0 12 * * * /usr/bin/certbot renew --quiet

Security Headers

Add comprehensive security headers:

# Security headers in nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';";

Post-Deployment

Once your site is live:

  • Monitor content growth - Use insights from content structure to organize new material effectively
  • Enhance with sidenotes - Apply techniques from the sidenotes guide to add depth to your writing
  • Iterate on design - Use the customization guide to refine your site's appearance and functionality
  • Return to documentation - Explore the documentation index for advanced features and optimizations

Deployment is where your contemplative digital garden meets the world. Choose platforms and configurations that align with your values - prioritize reader experience over metrics, privacy over tracking, sustainability over scale.