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 Thursday, July 16, 2026 at 12:30 PM
#fullstack development, #macOS, #heroku, #deployment, #django, #python3, #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
- Saving the current settings.py in case if things go wrong
- Preparing for deployment on Heroku
- Configuring my 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
- Static files
- Media files
- django_boards/settings.py up to this point
- Conclusion
- Related Resources
- Related Posts
Update (July 2026): This walkthrough was written against Django 5.1, which reached full end-of-life in December 2025 and is no longer receiving security patches. Run python -m django --version to check which version you have.
Note about this section (July 2026): I did not manage to successfully deploy my app to Heroku. The documentation was scant and hard to follow. I discuss how I ended up deploying it to render.com in Part 32. The process was smooth, and the documentation was clear. You won't miss anything if you skip this section and move on to Part 32.
I wrote this series as a live process, so sometimes it might seem confusing, but it all works out in the end.
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.
Preparing for deployment on Heroku
In order to be able to deploy my django-boards app on Heroku, some of the things I 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 my Heroku application
- Create a new SECRET_KEY for Heroku app
- Create a Procfile
Once I have created an account on Heroku, I 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 I have installed the Heroku CLI, I can run the following command:
heroku login
I am then prompted to open Heroku in my web browser and to login to Heroku.
After I successfully logged into Heroku, I could create my 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 to 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).
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, my app will not be working on Heroku. I have to further configure my app for Heroku.
Configuring my Django app for Heroku
Creating a new SECRET_KEY for use on Heroku
Next, I 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.
I use the Python secrets module to create a long random secret key. I pass in the byte size I 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 I generate the key I copy the newly generated string of characters and paste them as the value of the SECRET_KEY variable inside my .env file as well as the value of a SECRET_KEY environment variable inside my django-boards application's Settings tab on Heroku.
Adding gunicorn, dj-database-url, psycopg2 and psycopg2-binary packages
Next, with my virtual environment activated at the root directory of my Django application (django-boards), I run the following command:
django-boards (venv) pip install gunicorn psycopg2 psycopg2-binary dj-database-url
Creating a requirements.txt file
Now that I have added more packages to django-boards and I am preparing for deployment, I need to create a requirements.txt file which will contain all the packages required for my Django application to run in production.
First, I have to create a requirements.txt file at the root of my local Git repository where files such as .gitignore reside. To create it, I 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
I need a Procfile for any application I host on Heroku. In the case of a Django application, I 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 my local Git repository, where such files as .gitignore reside. In my case, because of the structure of my Django application, I must add django_boards before django_boards.wsgi, because files like manage.py reside in the first django_boards subdirectory of my local Git repository.
The Procfile specifies the commands to run when my application starts. In my file, I instruct Heroku to use Gunicorn to start my 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 my Django application and the web, handling incoming HTTP requests and passing them to Django for processing.
Gunicorn enables my Django app to handle multiple concurrent requests by utilizing multiple worker processes. This significantly improves the performance and responsiveness of my application, especially under heavy traffic.
Gunicorn can gracefully handle worker failures and automatically restart them.
I can easily adjust the number of worker processes to match the load on my application.
Gunicorn offers various configuration options to optimize performance.
Gunicorn's --pythonpath option allows me 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 my web server to interact with my Django application. It acts as a bridge between the web server (like Gunicorn or Apache) and my Django application, allowing them to communicate and handle requests.
When I configure my web server (e.g., Gunicorn), I specify the path to my wsgi.py file.
The web server imports the application callable from wsgi.py. In my wsgi.py file in django_boards, my 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
I need to make sure I am importing my os at the top of django_boards/settings.py. I 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 me 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 me to read environment variables.
For example, I 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 me to access environment variables. These variables are key-value pairs that store configuration information external to my code, making it easier to manage and customize my application across different environments. The DYNO env var is set on Heroku CI, but it's not a real Heroku app, so I 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, I set DEBUG to False on Heroku. Locally, I 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 I set DEBUG to False (as above), I lose any custom styling (non-Bootstrap) as well as my default topic posts and post detail default images locally. I will get to that later.
Differentiating between DEBUG value in local development and production on Heroku
I also want to make sure that I can move between development mode and production mode. Heroku provides me 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 I 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 I use when running in development. Specifically, 127.0.0.1:8000.
Configuring database access
Next, I 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 me with a conditional statement to differentiate between my sqlite3 development database and my 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 my (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, I would lose my 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 my app would have a different set of data since the disks are not synchronized. Instead of using SQLite on Heroku I configure my app to run on Postgres as indicated in the if statement above.
However, my Postgres configuration will not work until I run the following command:
django-boards heroku run python django_boards/manage.py migrate
I run it at the root of my 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
I can get DATABASE_URL_VALUE from within Config Vars on Heroku within my application's Settings tab. THEN the dj_database_url.config() method will work. The DATABASE_URL is generated after I add the Heroku Postgres addon to my 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 my database. I have to turn on SSL from within my application settings on Heroku. It's very easy to implement. Once I have done this, I can also check what my 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
I can also turn on maintenance mode on Heroku inside my 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, I placed my 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 my 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. I, however, did not set up my static files inside my apps. I set them all up in one static directory.
My 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 I have to add a STATIC_ROOT definition in django_boards/settings.py:
# what I 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 I 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 I define STATIC_ROOT according to Heroku STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_URL = 'static/'
STATIC_ROOT ensures Heroku can serve my static files from the staticfiles directory in production, and adding whitenoise.runserver_nostatic to the top of INSTALLED_APPS ensures that I run whitenoise's runserver implementation instead of the Django default. This is for development/production parity.
Next, I run python3 manage.py collectstatic to compile my static files into a folder called staticfiles which will be located at the root of my 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:
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
When I run the collectstatic command for Heroku, 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 I have to do now is set DEBUG to False, which I already did.
# django_boards/settings.py # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False
Before I install whitenoise, I run the collectstatic command:
python3 manage.py collectstatic
I am then prompted to either type "yes" to continue or "no" not to. I type "yes" to continue.
Installing whitenoise
Django does not support serving static files in production. However, the fantastic WhiteNoise project can integrate into my 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, I have to add the following to my 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 I 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 my Django application will serve my media files from.
Media files in development mode
My 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 I can (and do) add the media root as a static path to the ROOT_URLCONF in my 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, I don't have as many options as I do with static files since I can't use WhiteNoise for serving media files. Usually I might want to use Nginx along with django-storages to store media files outside the local file system where my application is running in production.
However, I am not going that route because I am deploying to Heroku. I will configure my 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 I 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 I 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'
Conclusion
In this section, I modify settings in django_boards/settings.py for deployment on Heroku. I create a new SECRET_KEY for use on Heroku, and add gunicorn, dj-database-url, psycopg2 and psycopg2-binary packages. I create a requirements.txt file, a Procfile, and a .python-version file. I discuss the purpose of the os module, set DEBUG to False on Heroku, and differentiate between DEBUG value in local development and production on Heroku. I configure database access, run the collectstatic command for serving static files in production, and install and configure 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