How to create a fullstack application using Django and Python Part 31
Social Share:
Saturday, November 23, 2024 at 11:43 PM | 17 min read
Last modified on Tuesday, May 26, 2026 at 10:19 AM
#fullstack development, #macOS, #heroku, #deployment, #django, #python3, #database access, #database configuration, #development, #gunicorn, #heroku postgres, #procfile, #production, #static files, #whitenoise, #series

Photo by Elin Gann on unsplash.com
Important Note: Before committing anything to Git or pushing anything to remote, please visit How to create a fullstack application using Django and Python Part 4 where I discuss how to add the python-dotenv package to the Django site and why it is crucial to do it. This article assumes you have a working knowledge of Git.
Table of Contents
-
Configuring our Django app for Heroku
- Creating a new SECRET_KEY for use on Heroku
- Adding gunicorn, dj-database-url, psycopg2 and psycopg2-binary packages
- Creating a requirements.txt file
- Creating a Procfile
- Creating a runtime.txt file/.python-version file
- The os module
- Setting DEBUG to False on Heroku
- Differentiating between DEBUG value in local development and production on Heroku
- Configuring database access
Saving the current settings.py in case if things go wrong
In case if things go wrong, make sure that you save the contents of your settings.py file so that you can revert to it if necessary. Also make sure to save the key value pair of your original SECRET_KEY so that you can revert to your original local state. For me, it was still present when I merged the branch create-like-post-tests into the main branch:
I had not created a new branch when I reset my settings.py to its original local state, so what I did was stash my settings.py changes, create a new branch called resetting-settingspy-to-original-local-state, committed those changes, checked out into the main branch, merged that branch into the main branch, and then pushed those changes to GitHub:
git status On branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: django_boards/settings.py no changes added to commit (use "git add" and/or "git commit -a") git stash -u Saved working directory and index state WIP on main: b6cdc6f Clean up static config git checkout -b resetting-settingspy-to-original-local-state git stash apply On branch resetting-settingspy-to-original-local-state Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: django_boards/settings.py no changes added to commit (use "git add" and/or "git commit -a") git status On branch resetting-settingspy-to-original-local-state Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: django_boards/settings.py no changes added to commit (use "git add" and/or "git commit -a") git add django_boards/settings.py git commit [resetting-settingspy-to-original-local-state ff95089] Reset settings.py to original local state 1 file changed, 69 insertions(+), 143 deletions(-) git status On branch resetting-settingspy-to-original-local-state nothing to commit, working tree clean git checkout main Switched to branch 'main' Your branch is up to date with 'origin/main'. git merge resetting-settingspy-to-original-local-state --no-ff Merge made by the 'ort' strategy. django_boards/django_boards/settings.py | 212 ++++++++++++++++++++++++------------------------------------------------- 1 file changed, 69 insertions(+), 143 deletions(-) git push origin main Enumerating objects: 10, done. Counting objects: 100% (10/10), done. Delta compression using up to 10 threads Compressing objects: 100% (6/6), done. Writing objects: 100% (6/6), 1.31 KiB | 1.31 MiB/s, done. Total 6 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0) remote: Resolving deltas: 100% (5/5), completed with 4 local objects. remote: remote: GitHub found 2 vulnerabilities on interglobalmedia/django-boards's default branch (2 moderate). To find out more, visit: remote: https://github.com/interglobalmedia/django-boards/security/dependabot remote: To github.com:interglobalmedia/django-boards.git b6cdc6f..e0fe33c main -> main
This makes it easier for me to track at which point I reset my settings.py file. The name of the branch describes the changes I made within the branch so that when I go into my Git history in my repository on GitHub, I will be able to easily identify the point in time that change/those changes were made.
Please ignore other commits made directly in the main branch after that. They were my desperate attempts at configuring my django-boards structure to Heroku's requirements, package requirements, and settings.py. I will share the link to the successful configuration of django-boards settings.py at the end of this post for reference.
Preparing for deployment on Heroku
In order to be able to deploy our django-boards app on Heroku, some of the things we have to do are the following:
- Have an account on Heroku
- Install the heroku CLI
- Create a Python application on Heroku
- Add a postgres database add-on to our Heroku application
- Create a new SECRET_KEY for Heroku app
- Create a Procfile
Once we have created an account on Heroku, we have to install the Heroku CLI. For those of you that are on macOS like myself, run the following command:
brew tap heroku/brew && brew install heroku
This does assume that you already have Homebrew installed. In order to install Homebrew on macOS, run the following command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
To learn more about Homebrew, please visit the Homebrew home page.
Once we have installed the Heroku CLI, we can run the following command:
heroku login
We are then prompted to open Heroku in our web browser and to login to Heroku.
After we have successfully logged into Heroku, we can create our app. I ran the following command to create an app called django-boards:
heroku create django-boards
After it was created, I ran the following:
# if I have any changes to push ro GitHub git add . git commit git push origin main # then I simply have to push those changes to heroku git push heroku main
and my django-boards app is pushed to Heroku.
Make sure that you have connected the correct GitHub repository with your Heroku app. To learn more about how to enable GitHub integration with your Heroku app, please visit GitHub Integration (Heroku GitHub Deploys).https://devcenter.heroku.com/articles/github-integration
If I want to check what my git remote URLs are on Heroku, I can run the following command from the directory where my manage.py resides:
django_boards git remote -v
And the following is returned:
heroku https://git.heroku.com/django-boards.git (fetch) heroku https://git.heroku.com/django-boards.git (push) origin git@github.com:interglobalmedia/django-boards.git (fetch) origin git@github.com:interglobalmedia/django-boards.git (push)
Then, to open my application, I run:
heroku open
At this_point, our app will not be working on Heroku. We have to further configure our app for Heroku.
Configuring our Django app for Heroku
Creating a new SECRET_KEY for use on Heroku
Next, we create a new SECRET_KEY using the following command for use on Heroku:
python manage.py shell >>> import secrets >>> secret_key = secrets.token_urlsafe(100) >>> print(secret_key) HPXglo0sJsGs_bnrM0Eipkf_TkT9Rrl4_GKyW0uHTP9lgSOqxaqkX53mnwt2zqbUIRvYMhWnFQHHPBokUWc-qPsaWH_T1QWWmeSE9VUzf9DR6xg3M1nslC-0SPsbVDbkkIucoQ
I chose to set a random key that is 100 bytes in size, or 134 characters long. This should be large/long enough to be secure.
We use the Python secrets module to create a long random secret key. We pass in the byte size we want to the secrets.token_urlsafe() method.
The secrets module is designed for generating cryptographically strong random values, making it ideal for creating secure keys.
The token_urlsafe(n) method generates a random URL-safe string containing n bytes. I use 100 bytes here, which results in a 134-character string.
After we generate the key we copy the newly generated string of characters and paste them as the value of the SECRET_KEY variable inside our .env file as well as the value of a SECRET_KEY environment variable inside our django-boards application's Settings tab on Heroku.
Adding gunicorn, dj-database-url, psycopg2 and psycopg2-binary packages
Next, with our virtual environment activated at the root directory of our Django application (django-boards), we run the following command:
django-boards (venv) pip install gunicorn psycopg2 psycopg2-binary dj-database-url
Creating a requirements.txt file
Now that we have added more packages to django-boards and we are preparing for deployment, we need to create a requirements.txt file which will contain all the packages required for our Django application to run in production.
First, we have to create a requirements.txt file at the root of our local Git repository where files such as .gitignore reside. To create it, we run the following command in the root of the django-boards repository:
pip freeze > requirements.txt
This command generates a list of all installed packages and their versions within my virtual environment. I removed a couple of them that I ended up not using in the project. Now the hosting provider will know which packages to install when I deploy my project.
As usual, before installing something, I had to make sure that I had activated my virtual environment:
source venv/bin/activate
If you are not sure whether you re-ran pip freeze > requirements.txt to include latest installs, just run pip freeze > requirements.txt again. You can check your requirements.txt file to make sure that all your required dependencies are included there.
If ever you uninstall a package, run pip freeze > requirements.txt again.
Creating a Procfile
We need a Procfile for any application we host on Heroku. In the case of a Django application, we need to add the following to a file called Procfile:
web: gunicorn --pythonpath django_boards django_boards.wsgi --log-file -
It should reside in the root of our local Git repository, where such files as .gitignore reside. In our case, because of the structure of our Django application, we must add django_boards before django_boards.wsgi, because files like manage.py reside in the first django_boards subdirectory of our local Git repository.
The Procfile specifies the commands to run when our application starts. In our file, we instruct Heroku to use Gunicorn to start our Django application.
Gunicorn plays a crucial role when deploying Django applications on Heroku. It is a Python WSGI HTTP server. It acts as the interface between our Django application and the web, handling incoming HTTP requests and passing them to Django for processing.
Gunicorn enables our Django app to handle multiple concurrent requests by utilizing multiple worker processes. This significantly improves the performance and responsiveness of our application, especially under heavy traffic.
Gunicorn can gracefully handle worker failures and automatically restart them.
We can easily adjust the number of worker processes to match the load on our application.
Gunicorn offers various configuration options to optimize performance.
Gunicorn's --pythonpath option allows us to dynamically attach a directory to the list of directories that the Python runtime searches for when it does module look-ups. By adding --pythonpath to the gunicorn command, the interpreter is basically told to 'look inside of the (outer) django_boards directory for a package (also) called django_boards which contains a module called wsgi.
--log-file or --error-logfile followed by a FILE or - stands for the default error log file to write to.
And what is the .wsgi file for?
In Django, the .wsgi file (typically named_ wsgi.py) serves as the entry point for our web server to interact with our Django application. It acts as a bridge between the web server (like Gunicorn or Apache) and our Django application, allowing them to communicate and handle requests.
When we configure our web server (e.g., Gunicorn), we specify the path to our wsgi.py file.
The web server imports the application callable from wsgi.py. In our wegi.py file in django_boards, our wsgi.py looks something like the following:
""" WSGI config for django_boards project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_boards.settings') application = get_wsgi_application()
Creating a runtime.txt file/.python-version file
Heroku used to require a runtime.txt file to specify the Python version that an app is using. Now it is deprecated and has been replaced with a .python-version file. I currently have both a runtime.txt and .python-version file, but will remove the runtime.txt file since it is deprecated.
The os module
We need to make sure we are importing our os at the top of django_boards/settings.py. We already have done so:
import os
os refers to the Python's built-in os module. It provides a way to interact with the operating system, allowing us to perform_tasks like:
- File system operations like reading and writing files, creating directories, listing directory contents, etc.
- Path manipulations like joining paths, extracting file extensions, checking if a path exists, etc.
- Accessing and modifying environment variables.
- Spawning new processes, getting process IDs, etc.
Specifically, importing the os module allows us to read environment variables.
For example, we will be using the os module in the following way (to be discussed a bit later):
IS_HEROKU_APP = "DYNO" in os.environ and not "CI" in os.environ
os.environ is a Python dictionary-like object that allows us to access environment variables. These variables are key-value pairs that store configuration information external to our code, making it easier to manage and customize our application across different environments. The DYNO env var is set on Heroku CI, but it's not a real Heroku app, so we also have to explicitly exclude CI, for example.
Setting DEBUG to False on Heroku
django_boards heroku config:set DEBUG=False Setting DEBUG and restarting ⬢ django-boards... done, v6 DEBUG: False
Here, we set DEBUG to False on Heroku. Locally, we also want to make sure that DEBUG is set to False. Otherwise, a security warning will be returned to Terminal after deployment:
(security.W018) You should not have DEBUG set to True in deployment.
This is what was returned after I had deployed to Heroku and had DEBUG set to True in django_boards/settings.py, even though I had set it to False on Heroku itself. I will discuss the command I ran that resulted in this warning a bit later.
When we set DEBUG to False (as above), we lose any custom styling (non-Bootstrap) as well as our default topic posts and post detail default images locally. We will get to that later.
Differentiating between DEBUG value in local development and production on Heroku
We also want to make sure that we can move between development mode and production mode. Heroku provides us with a conditional statement that achieves this:
# The `DYNO` env var is set on Heroku CI, but it's not a real Heroku app, so we also have to # explicitly exclude CI: # https://devcenter.heroku.com/articles/heroku-ci#immutable-environment-variables IS_HEROKU_APP = "DYNO" in os.environ and not "CI" in os.environ # SECURITY WARNING: don't run with debug turned on in production! if not IS_HEROKU_APP: DEBUG = True # On Heroku, it's safe to use a wildcard for `ALLOWED_HOSTS``, since the Heroku router performs # validation of the Host header in the incoming HTTP request. On other platforms you may need to # list the expected hostnames explicitly in production to prevent HTTP Host header attacks. See: # https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-ALLOWED_HOSTS if IS_HEROKU_APP: ALLOWED_HOSTS = ["*"] else: ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False
This also includes differentiating between ALLOWED_HOSTS in production on Heroku and ALLOWED_HOSTS in development locally. localhost is used with the heroku local -p PORT_NUMBER command. Replace PORT_NUMBER with the port number you want to use. For example, I like to use port 8000, so I run the command heroku local -p 8000. '127.0.0.1' refers to the IP address we use when running in development. Specifically, 127.0.0.1:8000.
Configuring database access
Next, we have to find the variable called DATABASES:
# Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } }
Again, Heroku provides us with a conditional statement to differentiate between our sqlite3 development database and our production database on Heroku:
if IS_HEROKU_APP: # In production on Heroku the database configuration is derived from the `DATABASE_URL` # environment variable by the dj-database-url package. `DATABASE_URL` will be set # automatically by Heroku when a database addon is attached to your Heroku app. See: # https://devcenter.heroku.com/articles/provisioning-heroku-postgres#application-config-vars # https://github.com/jazzband/dj-database-url DATABASES = { "default": dj_database_url.config( env="DATABASE_URL", conn_max_age=600, conn_health_checks=True, ssl_require=True, ), } else: # When running locally in development or in CI, a sqlite database file will be used instead # to simplify initial setup. Longer term it's recommended to use Postgres locally too. DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } }
This replaces our (original) development configuration using the sqlite3 database that comes with Django:
# Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } }
This (initial) configuration is for an sqlite3 (SQLite) database. However, on Heroku, we would lose our entire database at least once every 24 hours.
Even if Heroku’s disks were persistent, running sqlite3 would still not be a good fit. Since sqlite3 does not run as a service, each dyno would run a separate running copy. Each of these copies need their own disk backed store. This would mean that each dyno powering our app would have a different set of data since the disks are not synchronized. Instead of using SQLite on Heroku we configure our app to run on Postgres as indicated in the if statement above.
However, our Postgres configuration will not work until we run the following command:
django-boards heroku run python django_boards/manage.py migrate
We run it at the root of our local git repository: django-boards.
The command returns something like the following in Terminal:
Running python django_boards/manage.py migrate on ⬢ django-boards... up, run.6305 Operations to perform: Apply all migrations: accounts, admin, auth, avatar, boards, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying accounts.0001_initial... OK Applying accounts.0002_alter_profile_avatar... OK Applying accounts.0003_alter_profile_avatar... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying avatar.0001_initial... OK Applying avatar.0002_add_verbose_names_to_avatar_fields... OK Applying avatar.0003_auto_20170827_1345... OK Applying boards.0001_initial... OK Applying boards.0002_remove_post_post_liked_by_post_likes_delete_postlike... OK Applying boards.0003_remove_post_likes... OK Applying boards.0004_like... OK Applying boards.0005_post_likes_delete_like... OK Applying boards.0006_remove_post_likes_topic_likes... OK Applying boards.0007_remove_topic_likes_post_likes... OK Applying boards.0008_alter_post_likes... OK Applying boards.0009_alter_post_likes... OK Applying boards.0010_alter_post_message... OK Applying boards.0011_alter_post_message... OK Applying boards.0012_alter_post_message... OK Applying boards.0013_alter_post_message... OK Applying boards.0014_remove_post_likes... OK Applying boards.0015_like... OK Applying boards.0016_alter_like_value... OK Applying boards.0017_post_dislikes_post_likes_delete_like... OK Applying boards.0018_remove_post_dislikes... OK Applying sessions.0001_initial... OK
Before I finally figured out what was going on, I did the following (which definitely was a hack and should not ever be implemented because that would mean that I wouldn't migrate my database on Heroku):
DATABASE_URL = str(os.getenv('DATABASE_URL')) DATABASES = { 'default': DATABASE_URL }
It worked after I ran heroku run python django_boards/manage.py migrate, but it is correct practice to use dj_database_url.config() instead. Just remember to also set the DATABASE_URL variable for Heroku using the following command:
heroku config:set DATABASE_URL=DATABASE_URL_VALUE -a django-boards
We can get DATABASE_URL_VALUE from within Config Vars on Heroku within our application's Settings tab. THEN the dj_database_url.config() method will work. The DATABASE_URL is generated after we add the Heroku Postgres addon to our Heroku django-boards application.
What dj_database_url.config() does is it reads the DATABASE_URL environment variable provided by Heroku and converts it into a format Django can understand.
conn_max_age=600 keeps database connections open for a longer duration (around 10 minutes in this case), improving performance.
ssl_require=True enforces secure connections to our database. We have to turn on SSL from within our application settings on Heroku. It's very easy to implement. Once we have done this, we can also check what our Heroku domain for the django-boards application is by running the following command:
heroku domains
which for me, returned the following:
=== django-boards Heroku Domain django-boards-b087dbbc34ba.herokuapp.com
If, however, you place the actual DATABASE_URL in your settings.py by accident (as I did) and then push that change to GitHub, please make sure to destroy your current database, and THEN (re)create another Heroku Postgres addon for your django-boards application. Before you destroy and create another Heroku Postgres addon, make sure to set your application to maintenance:on by running the following command in Terminal from inside the root of your application's local git repository:
heroku maintenance:on
Which returns something like the following:
Enabling maintenance mode for ⬢ django-boards... done
We can also turn on maintenance mode on Heroku inside our application Settings tab.
Static Files
Locally, as long as DEBUG is set to True in settings.py, static files are served automatically by the runserver command. Django looks automatically for static files within each app in a folder called static. Initially, we placed our static files in a project-level folder called static.
Locally, only two settings are required for static files:
# django_boards/settings.py STATIC_URL = 'static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ]
This already exists in our django_boards/settings.py. STATICFILES_DIRS defines additional locations the built-in staticfiles app will look for static files outside of an app/static directory. We, however, did not set up our static files inside our apps. We set them all up in one static directory.
Our local Django server (runserver) is not made for hosting static files in production. Best practice is to bundle all static files in one directory and then have the production server serve those files.
The Django collectstatic command collects static files into STATIC_ROOT. STATIC_ROOT is the absolute path to the directory where the collectstatic command will collect static files for deployment. This means that we have to add a STATIC_ROOT definition in django_boards/settings.py:
# what we already have # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_URL = 'static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ] # what we need to add # django_boards/settings.py STATIC_ROOT = BASE_DIR / "staticfiles"
Now the Static files section should look like the following:
# Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ # This is the way we define STATIC_ROOT according to Heroku STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_URL = 'static/'
STATIC_ROOT ensures Heroku can serve our static files from the staticfiles directory in production, and adding whitenoise.runserver_nostatic to the top of INSTALLED_APPS ensures that we run whitenoise's runserver implementation instead of the Django default. This is for development/production parity.
Next, we run python3 manage.py collectstatic to compile our static files into a folder called staticfiles which will be located at the root of our django_boards project, in the same directory as manage.py. When I ran the command, something like the following was returned in Terminal:
And in my case, the following was returned: ```shell You have requested to collect static files at the destination location as specified in your settings: /Users/mariacam/Python-Development/django-boards/django_boards/staticfiles This will overwrite existing files! Are you sure you want to do this? Type 'yes' to continue, or 'no' to cancel: yes 1 static file copied to '/Users/mariacam/Python-Development/django-boards/django_boards/staticfiles', 135 unmodified.
The command results in the following directory and structure:
- staticfiles/ - admin/ - css/ - img/ - js/ - css/ - accounts.css/ - app.css/ - bootstrap.min.css/ - styles.css - img/ - pen-container.svg - user_avatar.svg - js/ - app.js - copy-button.js - scroll-top.js
We run the collectstatic command for Heroku, and a staticfiles directory is generated from running the command.
Running the collectstatic command
Django does not have a built-in solution for serving static files, at least not in production when DEBUG has to be False. What we have to do now is set DEBUG to False, which we already did.
# django_boards/settings.py # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False
Before we install whitenoise, we run the collectstatic command:
python3 manage.py collectstatic
We are then prompted to either type "yes" to continue or "no" not to. I typed "yes" to continue.
Installing whitenoise
Django does not support serving static files in production. However, the fantastic WhiteNoise project can integrate into our Django application on Heroku, and was designed with this purpose in mind.
When a Django application is deployed to Heroku, python manage.py collectstatic --noinput is run automatically during the build. A build will fail if the collectstatic step is not successful.
If you do not already have whitenoise installed, run the following command at the root of your local Git repository. For me, it is the django-boards directory where the venv directory resides and where I installed various third-party libraries:
(venv) django-boards pip install whitenoise
Then re-run pip freeze > requirements.txt to include whitenoise.
Configuring whitenoise in django_boards/settings.py
Next, we have to add the following to our django_boards/settings.py file:
# django_boards/settings.py IINSTALLED_APPS = [ # Use WhiteNoise's runserver implementation instead of the Django default, for dev-prod parity. 'whitenoise.runserver_nostatic', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', 'boards', 'accounts', 'dotenv', 'pylint', 'graphviz', 'djlint', 'coverage', 'widget_tweaks', 'soupsieve', 'bs4', 'html5lib', 'markdown', 'avatar', 'pygments', 'nh3', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', # Django doesn't support serving static assets in a production-ready way, so we use the # excellent WhiteNoise package to do so instead. The WhiteNoise middleware must be listed # after Django's SecurityMiddleware so that security redirects are still performed. # See: https://whitenoise.readthedocs.io 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_URL = "static/" # Don't store the original (un-hashed filename) version of static files, to reduce slug size: # https://whitenoise.readthedocs.io/en/latest/django.html#WHITENOISE_KEEP_ONLY_HASHED_FILES WHITENOISE_KEEP_ONLY_HASHED_FILES = True
Media Files
Usually files associated with the FileField or ImageField model fields should be treated as media files.
Handling media files is configured in django_boards/settings.py.
-
Similar to the STATIC_URL, the MEDIA_URL is the URL where users can access media files.
-
MEDIA_ROOT is the absolute path to the directory where our Django application will serve our media files from.
Media Files in development mode
Our media files development config in django_boards/settings.py:
MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'
However, the Django development server doesn't serve media files by default. But we can (and do) add the media root as a static path to the ROOT_URLCONF in our project-level URLs:
# django_boards/urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns = [ ... ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Media Files in production
When handling media files in production, we don't have as many options as we do with static files since we can't use WhiteNoise for serving media files. Usually we might want to use Nginx along with django-storages to store media files outside the local file system where our application is running in production.
However, we are not going that route because we are deploying to Heroku. We will configure our media files for production in upcoming part 32.
django_boards/settings.py up to this point
""" Django settings for django_boards project. Generated by 'django_boards' using Django 5.1. For more information on this file, see https://docs.djangoproject.com/en/5.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ import os from pathlib import Path import dj_database_url from dotenv import load_dotenv load_dotenv() # take environment variables from .env dotenv_path = os.path.join(os.path.dirname(__file__), '.env') load_dotenv(dotenv_path) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # For user uploaded files MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/' AVATAR_PROVIDERS = ( 'avatar.providers.PrimaryAvatarProvider', 'avatar.providers.LibRAvatarProvider', 'avatar.providers.GravatarAvatarProvider', 'avatar.providers.DefaultAvatarProvider', ) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = str(os.getenv('SECRET_KEY')) # The DYNO env var is set on Heroku CI, but it's not a real Heroku app, so we also have to # explicitly exclude CI: # https://devcenter.heroku.com/articles/heroku-ci#immutable-environment-variables IS_HEROKU_APP = "DYNO" in os.environ and not "CI" in os.environ # SECURITY WARNING: don't run with debug turned on in production! if not IS_HEROKU_APP: DEBUG = True # On Heroku, it's safe to use a wildcard for `ALLOWED_HOSTS``, since the Heroku router performs # validation of the Host header in the incoming HTTP request. On other platforms you may need to # list the expected hostnames explicitly in production to prevent HTTP Host header attacks. See: # https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-ALLOWED_HOSTS if IS_HEROKU_APP: ALLOWED_HOSTS = ["*"] else: ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False # Application definition INSTALLED_APPS = [ # Use WhiteNoise's runserver implementation instead of the Django default, for dev-prod parity. 'whitenoise.runserver_nostatic', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', 'boards', 'accounts', 'dotenv', 'pylint', 'graphviz', 'djlint', 'coverage', 'widget_tweaks', 'soupsieve', 'bs4', 'html5lib', 'markdown', 'avatar', 'pygments', 'nh3', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', # Django doesn't support serving static assets in a production-ready way, so we use the # excellent WhiteNoise package to do so instead. The WhiteNoise middleware must be listed # after Django's SecurityMiddleware so that security redirects are still performed. # See: https://whitenoise.readthedocs.io 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'django_boards.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ os.path.join(BASE_DIR, 'templates') ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.request', ], }, }, ] WSGI_APPLICATION = 'django_boards.wsgi.application' # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases if IS_HEROKU_APP: # In production on Heroku the database configuration is derived from the `DATABASE_URL` # environment variable by the dj-database-url package. `DATABASE_URL` will be set # automatically by Heroku when a database addon is attached to your Heroku app. See: # https://devcenter.heroku.com/articles/provisioning-heroku-postgres#application-config-vars # https://github.com/jazzband/dj-database-url DATABASES = { "default": dj_database_url.config( env="DATABASE_URL", conn_max_age=600, conn_health_checks=True, ssl_require=True, ), } else: # When running locally in development or in CI, a sqlite database file will be used instead # to simplify initial setup. Longer term it's recommended to use Postgres locally too. DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_URL = 'static/' # Don't store the original (un-hashed filename) version of static files, to reduce slug size: # https://whitenoise.readthedocs.io/en/latest/django.html#WHITENOISE_KEEP_ONLY_HASHED_FILES WHITENOISE_KEEP_ONLY_HASHED_FILES = True # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' LOGIN_URL = 'login' LOGOUT_REDIRECT_URL = 'index' LOGIN_REDIRECT_URL = 'index' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
You can view this version of django_boards/settings.py at git commit 2b99872.
Conclusion
In this section, I modified settings in django_boards/settings.py for deployment on Heroku. I created a new SECRET_KEY for use on Heroku, added gunicorn, dj-database-url, psycopg2 and psycopg2-binary packages, created a requirements.txt file, created a Procfile, created a .python-version file, discussed the purpose of the os module, set DEBUG to False on Heroku, differentiated between DEBUG value in local development and production on Heroku, configured database access, ran the collectstatic command for serving static files in production, and installed and configured whitenoise.
Related Resources
- Django Boards repository on Github
- How to create a fullstack application using Django and Python Part 16: mariadcampbell.com
- Hosting a Django Project on Heroku: Real Python
- Deploying with Git: Heroku documentation
- Running Apps Locally: Heroku documentation
- Django and Static Assets: Heroku documentation
- Using WhiteNoise with Django: Whitenoise documentation
- Specifying a Python Version: Heroku documentation
- How to deploy with WSGI: Django documentation
- Heroku Postgres: Heroku documentation
- Deploying Your Django REST API on Heroku: A Beginner’s Guide: by nagarjun mallesh, medium.com