Skip to main content
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:
1

Copy Setup Script

# From your local machine
scp deploy/setup.sh root@your-server-ip:/tmp/
2

SSH into Server

ssh root@your-server-ip
3

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
4

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 sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf
sudo systemctl restart redis
sudo systemctl enable redis
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
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.

3. Configure Environment Variables

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):
.env
# 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

5. Configure Nginx

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

7. Configure Gunicorn Service

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:
1

Backup Database

Creates timestamped database backup (deploy.sh:52)
2

Update Dependencies

Installs updated Python packages (deploy.sh:66)
3

Pull Latest Code

Updates from git repository (deploy.sh:76)
4

Run Migrations

Applies database migrations (deploy.sh:83)
5

Collect Static Files

Uploads static files to S3/R2 (deploy.sh:87)
6

Django Checks

Runs deployment validation (deploy.sh:91)
7

Restart Gunicorn

Restarts application server (deploy.sh:98)
8

Reload Nginx

Reloads reverse proxy (deploy.sh:107)
9

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.

Static and Media Files

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
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

sudo systemctl status gunicorn
sudo journalctl -u gunicorn -n 50
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

sudo systemctl status gunicorn

# Check if listening on port 8000
sudo netstat -tlnp | grep 8000
# or
sudo ss -tlnp | grep 8000
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

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

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
# 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