Skip to content

Developer Setup & Deployment Guide

Martin Paul Eve edited this page Nov 25, 2025 · 3 revisions

This guide covers:

  • Running the Django app locally (SQLite or Postgres)
  • Running in production with Gunicorn, Nginx, and PostgreSQL

If your project structure differs, adjust paths accordingly. The commands assume a Unix-like shell (macOS/Linux) and Python 3.11+.


1) Prerequisites

  • Python 3.11+
  • pip and virtualenv (or uv/poetry, if you prefer)
  • Git
  • Node & npm (only if the project has frontend builds; otherwise skip)
  • PostgreSQL 14+ (optional for local; required for prod)

2) Clone & Configure

git clone https://github.com/dqprogramming/intrepid.git
cd intrepid

Create a virtual environment:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

Install Python deps:

pip install -r requirements.txt

Configure a settings.py file inside the intrepid application. In addition to establishing all Django basics, such as DATABASES, you will need a number of custom settings. These include

# Settings for use with Mailgun
USE_MAILGUN = True
MAILGUN_ACCESS_KEY = "KEY_HERE"
MAILGUN_SERVER_NAME = "SERVER_HERE"
MAILGUN_REQUIRE_TLS = True
ENABLE_ENHANCED_MAILGUN_FEATURES = True  # Enables email tracking
FROM_EMAIL = "FROM@EMAIL.COM"


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static-assets")]

MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

SUMMERNOTE_THEME = "bs4"
SUMMERNOTE_CONFIG = {
    # Using SummernoteWidget - iframe mode, default
    "iframe": True,
    # You can put custom Summernote settings
    "summernote": {
        # As an example, using Summernote Air-mode
        "airMode": False,
        # Change editor size
        "width": "100%",
    },
    "toolbar": [
        ["style", ["style"]],
        ["font", ["bold", "italic", "underline", "clear"]],
        ["fontsize", ["fontsize"]],
        ["color", ["color"]],
        ["para", ["ul", "ol", "paragraph"]],
        ["height", ["height"]],
        ["table", ["table"]],
        ["insert", ["link", "picture", "hr", "video"]],
        ["view", ["fullscreen", "codeview"]],
        ["help", ["help"]],
    ],
}

BLEACH_DEFAULT_WIDGET = "django_summernote.widgets.SummernoteWidget"
BLEACH_STRIP_TAGS = True
BLEACH_ALLOWED_TAGS = [
    "h1",
    "h2",
    "h3",
    "h4",
    "h5",
    "p",
    "br" "b",
    "i",
    "u",
    "em",
    "ul",
    "li",
    "ol",
    "strong",
    "a",
    "table",
    "tr",
    "th",
    "td",
    "img",
    "button",
    "span",
    "hr",
    "iframe",
]
BLEACH_ALLOWED_ATTRIBUTES = ["href", "title", "style", "src", "alt"]

X_FRAME_OPTIONS = "SAMEORIGIN"

LOGIN_URL = "two_factor:login"
LOGIN_REDIRECT_URL = "profile"
LOGOUT_REDIRECT_URL = "two_factor:login"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

SITE_ID = 2

DATE_FORMAT = "Y-m-d"
DATETIME_FORMAT = "Y-m-d H:m:s"

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}

# MATOMO
MATOMO_SITE_URL = ""
MATOMO_SITE_ID = None

# AWS backup IAM
AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY"
AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"

AWS_DEFAULT_REGION = "eu-west-2"
BACKUP_BUCKET = "bucket_name"

# LANGUAGES
LANGUAGE_CODE = "en"
USE_I18N = True
gettext = lambda s: s
LANGUAGES = (
    ("de", gettext("German")),
    ("en", gettext("English")),
)
MODELTRANSLATION_DEFAULT_LANGUAGE = "en"
MODELTRANSLATION_LANGUAGES = ("en", "de")
MODELTRANSLATION_FALLBACK_LANGUAGES = ("en",)
MODELTRANSLATION_PREPOPULATE_LANGUAGE = "en"

3) Local Development (SQLite or Postgres)

Option A: Quick start with SQLite (recommended for first run)

Update settings to use SQLite in settings.py if not already:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

Run migrations & start:

python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

App is available at http://127.0.0.1:8000/

Option B: Local Postgres

Create DB & user:

psql -U postgres -h localhost <<'SQL'
CREATE USER intrepid WITH PASSWORD 'intrepid';
CREATE DATABASE intrepid OWNER intrepid;
GRANT ALL PRIVILEGES ON DATABASE intrepid TO intrepid;
SQL

Ensure your settings.py matches the DB credentials. Then:

python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

4) Static & Media Files (dev)

If static files are configured:

python manage.py collectstatic --noinput

For dev, Django will serve static automatically when DEBUG=True. In production, Nginx serves static and media.


5) Gunicorn + Nginx + Postgres (Production)

These steps assume Ubuntu 22.04 on a VM with a domain like app.example.com.

5.1 System packages

sudo apt update
sudo apt install -y python3.11-venv python3-pip postgresql postgresql-contrib nginx pkg-config build-essential libpq-dev

5.2 Project directory & user

sudo adduser --system --group --home /srv/intrepid intrepid
sudo mkdir -p /srv/intrepid/app
sudo chown -R intrepid:intrepid /srv/intrepid

5.3 Virtualenv & app install

sudo -u intrepid bash -lc '
cd /srv/intrepid/app
python3 -m venv .venv
source .venv/bin/activate
git clone https://github.com/dqprogramming/intrepid.git src
cd src
pip install --upgrade pip
pip install -r requirements.txt
'

5.4 PostgreSQL database (prod)

sudo -u postgres psql <<'SQL'
CREATE USER intrepid WITH PASSWORD 'super-strong-password';
CREATE DATABASE intrepid OWNER intrepid;
REVOKE CONNECT ON DATABASE intrepid FROM PUBLIC;
GRANT CONNECT ON DATABASE intrepid TO intrepid;
SQL

Optional: restrict schema permissions further as needed.

5.5 Django setup

sudo -u intrepid bash -lc '
cd /srv/intrepid/app/src
source ../.venv/bin/activate
python manage.py migrate --noinput
python manage.py collectstatic --noinput
'

5.6 Gunicorn systemd service

Create /etc/systemd/system/intrepid-gunicorn.service:

[Unit]
Description=Gunicorn for Intrepid
After=network.target

[Service]
User=intrepid
Group=intrepid
WorkingDirectory=/srv/intrepid/app/src
Environment="DJANGO_SETTINGS_MODULE=project.settings"
EnvironmentFile=/srv/intrepid/.env
ExecStart=/srv/intrepid/app/.venv/bin/gunicorn     project.wsgi:application     --bind unix:/run/intrepid.sock     --workers 3     --timeout 60     --access-logfile -     --error-logfile -
RuntimeDirectory=intrepid
RuntimeDirectoryMode=0755
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Start & enable:

sudo systemctl daemon-reload
sudo systemctl enable --now intrepid-gunicorn
sudo systemctl status intrepid-gunicorn

A Unix socket should appear at /run/intrepid.sock.

5.7 Nginx server block

Create /etc/nginx/sites-available/intrepid:

server {
    listen 80;
    server_name app.example.com;

    client_max_body_size 20M;

    location /static/ {
        alias /srv/intrepid/static/;
        access_log off;
        expires 30d;
    }

    location /media/ {
        alias /srv/intrepid/media/;
        access_log off;
        expires 30d;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/intrepid.sock;
        proxy_read_timeout 120;
        proxy_connect_timeout 5;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enable it:

sudo ln -s /etc/nginx/sites-available/intrepid /etc/nginx/sites-enabled/intrepid
sudo nginx -t
sudo systemctl reload nginx

5.8 HTTPS (Let's Encrypt)

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com --redirect --email you@example.com --agree-tos --non-interactive

Certificates auto-renew via systemd timers.


6) Process Management & Deploys

sudo -u intrepid bash -lc '
cd /srv/intrepid/app/src
git pull
source ../.venv/bin/activate
pip install -r requirements.txt
python manage.py migrate --noinput
python manage.py collectstatic --noinput
'
sudo systemctl restart intrepid-gunicorn

Logs:

  • Gunicorn: journalctl -u intrepid-gunicorn -f
  • Nginx: /var/log/nginx/access.log, /var/log/nginx/error.log

7) Health Checks & Security

  • Add a /health endpoint returning 200 OK
  • Set SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
  • Consider:
    • CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
    • SESSION_COOKIE_SECURE = True
    • CSRF_COOKIE_SECURE = True
    • SECURE_HSTS_SECONDS = 31536000 (after HTTPS verified)

8) Troubleshooting

  • 502 Bad Gateway → check Gunicorn service and /run/intrepid.sock
  • Static 404s → confirm collectstatic and paths in Nginx
  • DB errors → verify env vars and Postgres access
  • Migrations → run python manage.py showmigrations

9) TCP Alternative

If you prefer TCP over Unix sockets:

Gunicorn:

ExecStart=/srv/intrepid/app/.venv/bin/gunicorn project.wsgi:application --bind 127.0.0.1:8001 --workers 3

Nginx:

proxy_pass http://127.0.0.1:8001;

Restart Gunicorn and reload Nginx.


10) Quick Reference

  • python manage.py migrate
  • python manage.py createsuperuser
  • python manage.py collectstatic --noinput
  • python manage.py runserver 0.0.0.0:8000
  • python manage.py shell