Files
kennethreitz.org/docs/deployment.md
T
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

539 lines
13 KiB
Markdown

# 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](/docs/getting-started), organized content from [content structure](/docs/content-structure), and any desired customizations from the [customization guide](/docs/customization).
## Production Considerations
### Environment Configuration
Create a production configuration class:
```python
# 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:
```python
# wsgi.py
from tuftecms import create_app
from config import ProductionConfig
app = create_app(ProductionConfig)
if __name__ == "__main__":
app.run()
```
### Requirements
Create production requirements:
```txt
# 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`:
```toml
# 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:**
```bash
# Install system packages
sudo apt update
sudo apt install nginx python3 python3-pip
# Install Python dependencies
pip install -r requirements.txt
```
2. **Configure Gunicorn:**
```python
# 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
```
3. **Create systemd service:**
```ini
# /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
```
4. **Configure Nginx:**
```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
```
2. **Runtime specification:**
```
# runtime.txt
python-3.11
```
3. **Environment variables:**
```bash
heroku config:set SECRET_KEY=your-secret-key
heroku config:set FLASK_ENV=production
```
#### Railway
1. **Create railway.toml:**
```toml
[build]
builder = "NIXPACKS"
[deploy]
startCommand = "gunicorn -c gunicorn_config.py wsgi:app"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
```
2. **Environment variables via Railway dashboard or CLI:**
```bash
railway variables set SECRET_KEY=your-secret-key
railway variables set FLASK_ENV=production
```
#### Fly.io
1. **Create fly.toml:**
```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:
```python
# 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:**
```python
# 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)
```
2. **CDN configuration:**
```python
# 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:**
```python
# 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)
```
2. **Cache warming on deployment:**
```bash
# 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:
```python
# 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
```python
# 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
```python
# 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
```bash
#!/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
```bash
# 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:
```nginx
# 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](/docs/content-structure) to organize new material effectively
- **Enhance with sidenotes** - Apply techniques from the [sidenotes guide](/docs/sidenotes) to add depth to your writing
- **Iterate on design** - Use the [customization guide](/docs/customization) to refine your site's appearance and functionality
- **Return to documentation** - Explore the [documentation index](/docs) 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.*