Skip to content

New Project With Django, Python and Heroku

Matt Harley edited this page Jul 10, 2015 · 45 revisions

DEPRECATED

this github repository does a much better job at explaining things: https://github.com/etianen/django-herokuapp

Prerequesites

  1. https://www.apple.com/au/macbook-pro/
  2. https://www.heroku.com/
  3. https://github.com/
  4. http://brew.sh/
  5. Python 2.7: brew install python
  6. http://postgresapp.com/ Make sure you can run the Postgres executables:
pg_config
# ....

Otherwise add /Applications/Postgres.app/Contents/Versions/{version}/bin/ to your path

  1. https://virtualenvwrapper.readthedocs.org/en/latest/
  2. https://toolbelt.heroku.com/
  3. https://github.com/kennethreitz/autoenv

Assumptions

${PROJECT} is the name of your project for all of the steps below

Starting a new project

mkvirtualenv ${PROJECT}
cdvirtualenv
git clone https://github.com/mattharley/${PROJECT}.git
cd ${PROJECT}

Warning: If you clone with https it is very difficult to then switch over to an SSH connection!! If you intend to use SSH then clone with SSH...

  • Install the [essentials](Web App Development Essentials) TODO - update to whitenoise instead of dj-static
easy_install readline  # only for OSX?
pip install Django dj-database-url dj-static django-debug-toolbar django-nose django-redis gunicorn ipdb ipython mock newrelic nose psycopg2 pytz python-dateutil redis requests static
pip freeze > requirements.txt

Remember: This is in the PROJECT location(i.e. <YourProjects/ProjectVirtualEnv/ProjectName>)

  • Create our project and app
django-admin startproject ${PROJECT}_project
django-admin startapp ${PROJECT}
  • I prefer to have the directly laid out like this...
mv ${PROJECT}_project ${PROJECT}_project_parent
mv ${PROJECT}_project_parent/* .
rm -rf ${PROJECT}_project_parent/
  • Create Heroku apps
heroku apps:create ${PROJECT}
heroku apps:create ${PROJECT}-test
git remote add heroku-test [email protected]:${PROJECT}-test.git
  • setup local environment variables in your .env file (for autoenv)
    Remember: This is in the PROJECT location(i.e. <YourProjects/ProjectVirtualEnv/ProjectName>)
# .env
# Switch debug on and also now we can refer to test at $T and production at $P
export DEBUG=True
export PROJECT=${PROJECT}
export T='--app='${PROJECT}'-test'
export P='--app='${PROJECT}
  • Activate environment variables
cd .. && cd ${PROJECT}
  • Awesome Heroku Settings Template First of all take the value of SECRET_KEY out of the original settings file - we don't want to check that into source control! Instead, we'll make it an environment variable (see below)

Here's what your settings file should look like:

# ${PROJECT}_project/settings.py
import sys
import os
import urlparse

from django.core.exceptions import ImproperlyConfigured

def get_env_variable(var_name, default=None):
    """ Get the environment variable or return exception """
    try:
        return os.environ[var_name]
    except KeyError:
        if default is not None:
            return default
        else:
            error_msg = "Set the %s environment variable" % var_name
            raise ImproperlyConfigured(error_msg)

PROJECT_PATH = os.path.abspath(os.path.dirname(__name__))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_env_variable("SECRET_KEY", "")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(get_env_variable("DEBUG", False))
if DEBUG:
    DEBUG_TOOLBAR_PATCH_SETTINGS = True
else:
    DEBUG_TOOLBAR_PATCH_SETTINGS = False

TEMPLATE_DEBUG = DEBUG

ALLOWED_HOSTS = [
    '127.0.0.1', 
    'localhost',
    '${PROJECT}.herokuapp.com',
    '${PROJECT}-test.herokuapp.com',
]


# Application definition

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_nose',
    '${PROJECT}',
    'debug_toolbar',
    'django.contrib.admin',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'debug_toolbar.middleware.DebugToolbarMiddleware',
)

ROOT_URLCONF = '${PROJECT}_project.urls'

WSGI_APPLICATION = '${PROJECT}_project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': '${PROJECT}',
        'USER': '',
        'PASSWORD': '',
        'HOST': 'localhost',    # Note on Ubuntu the presence of these two lines can cause problems
        'PORT': '',             # with passwordless logins. It's safe to remove them.
    }
}

HEROKU = bool(os.environ.get('DATABASE_URL'))

if HEROKU:
    import dj_database_url
    DATABASES['default'] = dj_database_url.config()
    DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
    DEBUG_TOOLBAR_PATCH_SETTINGS = False

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': 'cache.sqlite',
    },
}

redis_url = None

if os.environ.get('REDISCLOUD_URL'):
    redis_url = urlparse.urlparse(os.environ.get('REDISCLOUD_URL'))

    CACHES = {
        'default': {
            "BACKEND": "redis_cache.cache.RedisCache",
            'LOCATION': '{0.hostname}:{0.port}:0'.format(redis_url),
            'OPTIONS': {
                'PASSWORD': redis_url.password,
                'CLIENT_CLASS': 'redis_cache.client.DefaultClient',
            },
        },
    }

# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


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

STATIC_ROOT = 'static/'
STATIC_URL = '/static/'

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse'
        }
    },
    'formatters': {
        'verbose': {
            'format': '[%(levelname)s] [%(module)s %(funcName)s] [p%(process)d t%(thread)d] %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(message)s'
        },
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'formatter': 'verbose'
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        },
        'null': {
            'level': 'DEBUG',
            'class':'django.utils.log.NullHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['null'],  # Quiet by default!
            'propagate': False,
            'level':'DEBUG',
        },
        'analytics': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': True,
        },
        '${PROJECT}': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propogate': True,
        },
        'django.request': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propogate': True,
        }
    },
}

from django.core.signals import got_request_exception
import traceback
import logging
logger = logging.getLogger(__name__)


def exception_printer(sender, **kwargs):
    logger.error(''.join(traceback.format_exception(*sys.exc_info())))

got_request_exception.connect(exception_printer)

  1. Set the secret key as an environment variable.
echo -e "export SECRET_KEY='${YOUR_SECRET_KEY}'\n" >> .env
cd .. && cd ${PROJECT}
heroku config:set SECRET_KEY=${YOUR_SECRET_KEY} $P
# TODO: this should probably be a different secret key
heroku config:set SECRET_KEY=${YOUR_SECRET_KEY} $T

If you need to generate a temporary secret key for local dev (say you're joining a project which is already using a secret key on Heroku that you can't access), you can use this tool. You can prefix it with something that reminds you that it is in fact a temporary key. Then just add it to your .env without pushing it up to heroku.

  • Database setup
createdb ${PROJECT}
createdb ${PROJECT}-test
chmod +x ./manage.py
./manage.py syncdb
  • Django Static Files
# ${PROJECT}_project/wsgi.py
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "${PROJECT}_project.settings")

from django.core.wsgi import get_wsgi_application
from dj_static import Cling
application = Cling(get_wsgi_application())

  • Local test
# server
./manage.py runserver
open http://127.0.0.1:8000/admin/

* tests
./manage.py test
  • Test app on Heroku: TODO: update to a app.json file
# Essential Addons
for addon in heroku-postgresql logentries:tryit newrelic:wayne rediscloud; do heroku addons:add $addon $T; done
for addon in heroku-postgresql logentries:tryit newrelic:wayne rediscloud; do heroku addons:add $addon $P; done
# Procfile
echo -e "web: newrelic-admin run-program gunicorn "${PROJECT}"_project.wsgi -w 4 --log-level DEBUG\n" > Procfile
  • Push to Test Server
# Firstly enable DEBUG on the Test Server
heroku config:set DEBUG=True $T
git add .
git commit -m 'setup django project'
git push heroku-test master
heroku run "./manage.py syncdb && ./manage.py migrate" $T
open http://${PROJECT}-test.herokuapp.com/admin/
Clone this wiki locally