Deploy FootyCollect directly on an Ubuntu or Debian VPS using Nginx as a reverse proxy, Gunicorn as the application server, and systemd for service management.
Overview
Bare metal deployment installs FootyCollect directly on your server without containerization:
Nginx : Reverse proxy and SSL termination
Gunicorn : WSGI application server
Systemd : Service management and process supervision
PostgreSQL : Database server
Redis : Cache and Celery broker
Supervisor : Celery worker management (optional)
This guide is based on the deployment scripts in deploy/ including setup.sh, deploy.sh, nginx.conf, and gunicorn.service.
Prerequisites
Ubuntu 20.04+ or Debian 11+ server
Root or sudo access
Domain name with DNS pointing to your server
Minimum 2GB RAM, 20GB disk space
Initial Server Setup
Automated Setup Script
The repository includes an automated setup script (deploy/setup.sh:1) that configures your server:
Copy Setup Script
# From your local machine
scp deploy/setup.sh root@your-server-ip:/tmp/
Run Setup Script
chmod +x /tmp/setup.sh
/tmp/setup.sh
This script will:
Update system packages
Install Python 3.12, PostgreSQL, Redis, Nginx
Install certbot for SSL certificates
Create footycollect user
Configure PostgreSQL database
Setup firewall (UFW)
Configure fail2ban
Setup log rotation
Set Database Password
The setup script creates a PostgreSQL user with a default password. Change it immediately:
sudo -u postgres psql -c "ALTER USER footycollect WITH PASSWORD 'your-secure-password';"
Manual Setup (Alternative)
If you prefer manual setup or need to customize the installation:
sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y \
python3.12 \
python3.12-venv \
python3-pip \
postgresql \
postgresql-contrib \
redis-server \
nginx \
certbot \
python3-certbot-nginx \
git \
curl \
build-essential \
libpq-dev \
python3-dev \
supervisor \
ufw \
fail2ban
sudo useradd -m -s /bin/bash footycollect
sudo mkdir -p /var/www/footycollect
sudo chown footycollect:footycollect /var/www/footycollect
sudo -u postgres psql << EOF
CREATE USER footycollect WITH PASSWORD 'your-secure-password';
CREATE DATABASE footycollect_db OWNER footycollect;
ALTER USER footycollect CREATEDB;
\q
EOF
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Application Deployment
1. Clone Repository
Switch to the application user and clone the repository:
# Switch to application user
sudo su - footycollect
# Clone repository
cd /var/www
git clone https://github.com/sunr4y/FootyCollect.git footycollect
cd footycollect
2. Setup Virtual Environment
# Create virtual environment with Python 3.12
python3.12 -m venv venv
# Activate virtual environment
source venv/bin/activate
# Upgrade pip
pip install --upgrade pip
# Install production dependencies
pip install -r requirements/production.txt
The virtual environment must be created with Python 3.12 as specified in setup.sh:25.
Copy the environment template and configure production settings:
# Copy template
cp deploy/env.example .env
# Edit environment variables
nano .env
Required variables (from deploy/env.example:1):
# Django Settings
DJANGO_SECRET_KEY = your-secret-key-here
DJANGO_DEBUG = False
DJANGO_ALLOWED_HOSTS = yourdomain.com,www.yourdomain.com
DJANGO_ADMIN_URL = admin/
# Database
DATABASE_URL = postgresql://footycollect:your-db-password@localhost:5432/footycollect_db
# Redis
REDIS_URL = redis://localhost:6379/0
# Security
DJANGO_SECURE_SSL_REDIRECT = True
DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS = True
DJANGO_SECURE_HSTS_PRELOAD = True
# Email (SendGrid)
SENDGRID_API_KEY = your-sendgrid-api-key
DJANGO_DEFAULT_FROM_EMAIL = noreply@yourdomain.com
# Sentry (Error Tracking)
SENTRY_DSN = your-sentry-dsn
SENTRY_ENVIRONMENT = production
# Storage Backend (aws or r2)
STORAGE_BACKEND = aws
DJANGO_AWS_ACCESS_KEY_ID = your-access-key
DJANGO_AWS_SECRET_ACCESS_KEY = your-secret-key
DJANGO_AWS_STORAGE_BUCKET_NAME = your-bucket
DJANGO_AWS_S3_REGION_NAME = us-east-1
Generate a secure SECRET_KEY :python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
See the environment setup guide for all available variables.
4. Setup Database
Run migrations and create a superuser:
# Activate virtual environment
source venv/bin/activate
# Run migrations
python manage.py migrate
# Create superuser
python manage.py createsuperuser
# Collect static files to S3/R2
python manage.py collectstatic --noinput
Copy and configure the Nginx reverse proxy (deploy/nginx.conf:1):
# Copy nginx configuration
sudo cp deploy/nginx.conf /etc/nginx/sites-available/footycollect
# Edit configuration - replace footycollect.pro with your domain
sudo nano /etc/nginx/sites-available/footycollect
Update the domain in nginx.conf:
# Replace all instances of:
server_name footycollect.pro www.footycollect.pro;
# With your domain:
server_name yourdomain.com www.yourdomain.com;
Enable the site:
# Create symbolic link
sudo ln -s /etc/nginx/sites-available/footycollect /etc/nginx/sites-enabled/
# Remove default site
sudo rm /etc/nginx/sites-enabled/default
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
The Nginx configuration includes:
HTTP to HTTPS redirect (nginx.conf:6)
Security headers (nginx.conf:40)
Gunicorn proxy on 127.0.0.1:8000 (nginx.conf:62)
Static/media file serving (nginx.conf:77)
Health check endpoints (nginx.conf:93)
6. Setup SSL Certificates
Obtain free SSL certificates from Let’s Encrypt:
# Install certbot (if not already installed via setup.sh)
sudo apt-get install certbot python3-certbot-nginx
# Obtain certificate (replace with your domain)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Follow prompts and agree to terms
Certbot will:
Obtain SSL certificates
Update Nginx configuration automatically
Setup auto-renewal via systemd timer
Certificates auto-renew via certbot.timer. Verify: sudo systemctl status certbot.timer
Setup systemd service for Gunicorn (deploy/gunicorn.service:1):
# Copy service file
sudo cp deploy/gunicorn.service /etc/systemd/system/
# Edit if needed (verify paths match)
sudo nano /etc/systemd/system/gunicorn.service
The service file configures:
[Unit]
Description =FootyCollect Gunicorn daemon
After =network.target postgresql.service redis.service
[Service]
User =www-data
Group =www-data
WorkingDirectory =/var/www/footycollect
Environment = "PATH=/var/www/footycollect/venv/bin"
EnvironmentFile =/var/www/footycollect/.env
ExecStart =/var/www/footycollect/venv/bin/gunicorn \
--access-logfile - \
--workers 3 \
--bind 127.0.0.1:8000 \
--timeout 120 \
--keep-alive 5 \
--max-requests 1000 \
--max-requests-jitter 50 \
--log-level info \
config.wsgi:application
[Install]
WantedBy =multi-user.target
Gunicorn configuration (gunicorn.service:11):
Workers : 3 (adjust based on CPU cores: (2 * cores) + 1)
Bind : 127.0.0.1:8000 (Nginx proxies to this)
Timeout : 120 seconds
Max requests : 1000 (workers restart after to prevent memory leaks)
Enable and start the service:
# Reload systemd
sudo systemctl daemon-reload
# Enable on boot
sudo systemctl enable gunicorn
# Start service
sudo systemctl start gunicorn
# Check status
sudo systemctl status gunicorn
8. Verify Deployment
Check that everything is running:
# Check Gunicorn logs
sudo journalctl -u gunicorn -f
# Check Nginx logs
sudo tail -f /var/log/nginx/footycollect_error.log
# Test health endpoints
curl http://localhost:8000/health/
curl http://localhost:8000/ready/
# Test via Nginx (HTTPS)
curl https://yourdomain.com/health/
# Run Django deployment checks
source /var/www/footycollect/venv/bin/activate
python /var/www/footycollect/manage.py check --deploy
Deployment Updates
Use the automated deployment script (deploy/deploy.sh:1) for updates:
# SSH into server
ssh footycollect@your-server-ip
# Navigate to project
cd /var/www/footycollect
# Run deployment script
./deploy/deploy.sh
The deployment script (deploy/deploy.sh:45) performs:
Backup Database
Creates timestamped database backup (deploy.sh:52)
Update Dependencies
Installs updated Python packages (deploy.sh:66)
Pull Latest Code
Updates from git repository (deploy.sh:76)
Run Migrations
Applies database migrations (deploy.sh:83)
Collect Static Files
Uploads static files to S3/R2 (deploy.sh:87)
Django Checks
Runs deployment validation (deploy.sh:91)
Restart Gunicorn
Restarts application server (deploy.sh:98)
Reload Nginx
Reloads reverse proxy (deploy.sh:107)
Health Check
Verifies application is running (deploy.sh:132)
Database backups are stored in /var/www/footycollect/backups/ and automatically rotated (keeps last 7 backups).
Service Management
Gunicorn Service
# Start
sudo systemctl start gunicorn
# Stop
sudo systemctl stop gunicorn
# Restart
sudo systemctl restart gunicorn
# Status
sudo systemctl status gunicorn
# Enable on boot
sudo systemctl enable gunicorn
# View logs
sudo journalctl -u gunicorn -f
sudo journalctl -u gunicorn -n 100
Nginx Service
# Reload (no downtime)
sudo systemctl reload nginx
# Restart (brief downtime)
sudo systemctl restart nginx
# Test configuration
sudo nginx -t
# View logs
sudo tail -f /var/log/nginx/footycollect_access.log
sudo tail -f /var/log/nginx/footycollect_error.log
PostgreSQL
# Status
sudo systemctl status postgresql
# Restart
sudo systemctl restart postgresql
# Connect to database
sudo -u postgres psql -d footycollect_db -U footycollect
Redis
# Status
sudo systemctl status redis
# Restart
sudo systemctl restart redis
# CLI
redis-cli
Celery Workers (Background Tasks)
Setup Celery for background tasks (image downloads, scheduled tasks):
Using Supervisor
Create supervisor configuration:
sudo nano /etc/supervisor/conf.d/footycollect-celery.conf
[program:footycollect-celery]
command =/var/www/footycollect/venv/bin/celery -A config.celery_app worker -- loglevel =info
directory =/var/www/footycollect
user =footycollect
numprocs =1
stdout_logfile =/var/log/celery/worker.log
stderr_logfile =/var/log/celery/worker.log
autostart =true
autorestart =true
startsecs =10
stopwaitsecs =600
stopasgroup =true
priority =998
[program:footycollect-celery-beat]
command =/var/www/footycollect/venv/bin/celery -A config.celery_app beat -- loglevel =info
directory =/var/www/footycollect
user =footycollect
numprocs =1
stdout_logfile =/var/log/celery/beat.log
stderr_logfile =/var/log/celery/beat.log
autostart =true
autorestart =true
startsecs =10
priority =999
Create log directory and start:
# Create log directory
sudo mkdir -p /var/log/celery
sudo chown footycollect:footycollect /var/log/celery
# Reload supervisor
sudo supervisorctl reread
sudo supervisorctl update
# Start workers
sudo supervisorctl start footycollect-celery
sudo supervisorctl start footycollect-celery-beat
# Check status
sudo supervisorctl status
Database Backups
Manual Backup
# Backup database
sudo -u postgres pg_dump footycollect_db > /var/www/footycollect/backups/db_backup_ $( date +%Y%m%d_%H%M%S ) .sql
# Backup with compression
sudo -u postgres pg_dump footycollect_db | gzip > /var/www/footycollect/backups/db_backup_ $( date +%Y%m%d_%H%M%S ) .sql.gz
Automated Backups (Cron)
Setup daily backups via cron:
# Edit crontab for postgres user
sudo crontab -e -u postgres
# Add daily backup at 2 AM
0 2 * * * pg_dump footycollect_db | gzip > /var/www/footycollect/backups/db_backup_ $( date + \% Y \% m \% d ) .sql.gz
# Cleanup old backups (keep 30 days)
0 3 * * * find /var/www/footycollect/backups/ -name "db_backup_*.sql.gz" -mtime +30 -delete
Restore Backup
# Stop application
sudo systemctl stop gunicorn
# Restore database
sudo -u postgres psql footycollect_db < /var/www/footycollect/backups/db_backup_20260302.sql
# Or restore compressed backup
gunzip < /var/www/footycollect/backups/db_backup_20260302.sql.gz | sudo -u postgres psql footycollect_db
# Start application
sudo systemctl start gunicorn
Always create a fresh backup before restoring from an old backup.
Production serves static/media from S3 or R2 (configured in .env).
Collect Static Files
cd /var/www/footycollect
source venv/bin/activate
# Upload to S3/R2
python manage.py collectstatic --noinput
# Clear and re-upload
python manage.py collectstatic --noinput --clear
# Verify configuration
python manage.py verify_staticfiles
Local Static Files (Not Recommended)
For testing only, serve files locally via Nginx (already configured in nginx.conf:77):
location /static/ {
alias /var/www/footycollect/staticfiles/;
expires 30d ;
add_header Cache-Control "public, immutable" ;
}
location /media/ {
alias /var/www/footycollect/media/;
expires 7d ;
add_header Cache-Control "public" ;
}
Collect to local directory:
# Set STORAGE_BACKEND=local in .env temporarily
python manage.py collectstatic --noinput
# Ensure permissions
sudo chown -R www-data:www-data /var/www/footycollect/staticfiles/
sudo chown -R www-data:www-data /var/www/footycollect/media/
Use S3/R2 for production. Local storage doesn’t scale and complicates multi-server deployments.
Monitoring and Logs
Application Logs
# Gunicorn logs (via systemd)
sudo journalctl -u gunicorn -f
sudo journalctl -u gunicorn --since "1 hour ago"
# Nginx access logs
sudo tail -f /var/log/nginx/footycollect_access.log
# Nginx error logs
sudo tail -f /var/log/nginx/footycollect_error.log
# Celery logs (if using supervisor)
sudo tail -f /var/log/celery/worker.log
sudo tail -f /var/log/celery/beat.log
Log Rotation
Log rotation is configured by setup.sh:93:
/etc/logrotate.d/footycollect
/var/www/footycollect/logs/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 footycollect footycollect
sharedscripts
}
Troubleshooting
Gunicorn Won’t Start
Check service status and logs
sudo systemctl status gunicorn
sudo journalctl -u gunicorn -n 50
Verify virtual environment
ls -la /var/www/footycollect/venv/bin/gunicorn
# Test manually
cd /var/www/footycollect
source venv/bin/activate
gunicorn --bind 127.0.0.1:8000 config.wsgi:application
ls -la /var/www/footycollect
# Should be owned by footycollect or www-data
sudo chown -R www-data:www-data /var/www/footycollect
# Check .env exists and is readable
ls -la /var/www/footycollect/.env
# Verify DATABASE_URL
grep DATABASE_URL /var/www/footycollect/.env
Nginx 502 Bad Gateway
Check Gunicorn is running
sudo systemctl status gunicorn
# Check if listening on port 8000
sudo netstat -tlnp | grep 8000
# or
sudo ss -tlnp | grep 8000
Check Nginx configuration
sudo nginx -t
# Verify proxy_pass
grep proxy_pass /etc/nginx/sites-available/footycollect
# Should be: http://127.0.0.1:8000
sudo tail -f /var/log/nginx/footycollect_error.log
Database Connection Errors
Check PostgreSQL is running
sudo systemctl status postgresql
# Test connection
sudo -u postgres psql -d footycollect_db -U footycollect
grep DATABASE_URL /var/www/footycollect/.env
# Should match: postgresql://footycollect:password@localhost:5432/footycollect_db
sudo tail -f /var/log/postgresql/postgresql- * -main.log
Static Files Not Loading
Check S3/R2 configuration
cd /var/www/footycollect
source venv/bin/activate
python manage.py verify_staticfiles
# Check last collectstatic run in deployment logs
grep collectstatic /var/www/footycollect/logs/ * .log
# Re-run collectstatic
python manage.py collectstatic --noinput
Check storage credentials
# Verify AWS/R2 credentials in .env
grep -E 'AWS|CLOUDFLARE' /var/www/footycollect/.env
# Run deployment checks
python manage.py check --deploy
SSL Certificate Issues
# Check certbot logs
sudo tail -f /var/log/letsencrypt/letsencrypt.log
# Verify DNS
dig yourdomain.com
# Test certificate renewal
sudo certbot renew --dry-run
# Ensure port 80 is open for ACME challenge
sudo ufw status
sudo ufw allow 80/tcp
# Test from outside
curl http://yourdomain.com/.well-known/acme-challenge/test
Security Hardening
Firewall Rules
# Check current rules
sudo ufw status verbose
# Allow only necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable
SSH Security
# Disable root login
sudo nano /etc/ssh/sshd_config
# Set: PermitRootLogin no
# Use SSH keys only
# Set: PasswordAuthentication no
# Restart SSH
sudo systemctl restart sshd
Fail2ban Configuration
# Check fail2ban status
sudo fail2ban-client status
# Check Nginx jail
sudo fail2ban-client status nginx-http-auth
# View banned IPs
sudo fail2ban-client status sshd
Automatic Security Updates
# Install unattended-upgrades
sudo apt-get install unattended-upgrades
# Enable automatic updates
sudo dpkg-reconfigure --priority=low unattended-upgrades
Next Steps