Skip to content

Instantly share code, notes, and snippets.

@adamghill
Last active November 13, 2022 16:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save adamghill/9c11687eb87e98951adb11c4dacdb709 to your computer and use it in GitHub Desktop.
Save adamghill/9c11687eb87e98951adb11c4dacdb709 to your computer and use it in GitHub Desktop.
Settings, files, and a checklist to deploy a Django app to Heroku with nginx + gunicorn + postgres + redis and using `poetry` for dependencies.
  • GitHub for code
  • Heroku pipeline with one production app
    • Heroku Postgres
    • Heroku Redis
    • Linked to GitHub repo
    • Add environment variables for ENVIRONMENT=live, DISABLE_COLLECTSTATIC=1, and SECRET_KEY
    • Add pgbouncer and nginx buildpacks
  • Cloudflare for SSL and CNAME pointing to Heroku app domain
    • Force SSL with a Cloudflare rule
  • Namecheap for the domain
    • Point nameservers to Cloudflare
  • Sentry for error logging
  • Panelbear and Plausible for privacy-focused analytics
  • UptimeRobot to get alerts for when the site goes down
def when_ready(server):
# touch app-initialized when ready
open("/tmp/app-initialized", "w").close()
bind = "unix:///tmp/nginx.socket"
workers = 3
rem this lives in the `bin` directory (i.e. bin/post_compile)
#!/usr/bin/env bash
set -eo pipefail
indent() {
sed "s/^/ /"
}
puts-step() {
echo "-----> $@"
}
/app/.heroku/python/bin/python -m pip install --upgrade pip
puts-step "Installing dependencies with poetry..."
poetry config virtualenvs.create false 2>&1 | indent
poetry install --no-dev 2>&1 | indent
puts-step "Collect static files (part 1)..."
python manage.py collectstatic --noinput 2>&1 | indent
puts-step "Compress files..."
python manage.py compress --force 2>&1 | indent
puts-step "Collect static files (part 2)..."
python manage.py collectstatic --noinput 2>&1 | indent
web: python manage.py collectstatic --noinput && bin/start-pgbouncer bin/start-nginx gunicorn -c gunicorn.conf.py project.wsgi
python-3.7.12
import os
from pathlib import Path
ENVIRONMENT = os.getenv("ENVIRONMENT", "dev")
# Regular dev settings go here
if ENVIRONMENT == "live":
import dj_database_url
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)
DEBUG = False
COMPRESS_OFFLINE = True
ALLOWED_HOSTS += [
"ADD-DOMAINS-HERE"
]
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": os.environ.get("REDIS_URL"),
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
}
}
DATABASES["default"] = dj_database_url.config(conn_max_age=600)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"console": {
"format": "[%(asctime)s][%(levelname)s] %(name)s "
"%(filename)s:%(funcName)s:%(lineno)d | %(message)s",
"datefmt": "%H:%M:%S",
},
"verbose": {
"format": (
"%(asctime)s [%(process)d] [%(levelname)s] "
+ "pathname=%(pathname)s lineno=%(lineno)s "
+ "funcname=%(funcName)s %(message)s"
),
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"simple": {"format": "%(levelname)s %(message)s"},
},
"handlers": {
"null": {"level": "DEBUG", "class": "logging.NullHandler"},
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"": {"handlers": ["console"], "level": "INFO", "propagate": False},
"testlogger": {"handlers": ["console"], "level": "INFO"},
"deploylogs": {"level": "WARNING", "propagate": True},
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment