Django from first principles, part 19

MP 118: Deploying a simple Django project.

Note: This is the 19th post in a series about building a full Django project, starting with a single file. This series is free to everyone, as soon as each post comes out.

In the last post, we did some final reorganization of the BlogMaker Lite project so all files are in a reasonable place. If we choose to expand the project, most new code will fit into a pretty natural place within the project.

This is all good, as long as we want to work with the project on our own system. But how do you get your project onto a server, so other people can use the project as well? That's the goal with most Django projects. In this post we'll use django-simple-deploy to deploy the project to a hosting platform, with little manual configuration work on our part.

Disclaimer: I'm the author of django-simple-deploy, and it's still in the pre-1.0 phase. Now that my family has finished our cross-country move and this series is coming to a close, I'll be focusing on bringing simple_deploy to the 1.0 state.

What is deployment?

The term deployment refers to the process of pushing your project to a remote server, where anyone with an internet connection can use it. If you've never thought about deployment before, you can think of a "remote server" as a small computer in a datacenter. For all but the biggest deployments, that computer is actually a small virtual computer running on a much larger physical computer.

To get your project to run successfully on a remote server, you need to make some configuration changes so your project runs appropriately on a production server. Deployment has a reputation for being difficult because there are so many different ways a project can be configured in order to run on a remote server.

In this post we'll use a PaaS provider. This acronym is short for platform as a service. That means, instead of using a general-purpose remote server, we'll use a platform that's purpose-built for running web applications. I'll walk through the process of deploying to Fly.io, but you can use an almost identical process if you'd rather use Platform.sh or Heroku.

Note: These days, it's really hard to find a hosting platform that will host small projects for free. The development of cryptocurrencies made it just about impossible for hosting companies to offer free tiers. Any platform that offers a free tier is almost immediately inundated with fraud and crypto miners.

I believe Fly.io waives invoices if the total due for a month is less than $5, although billing policies can change at any time. Whichever platform you choose, make sure you understand their billing policy, and keep an eye on the billing section of your platform's dashboard.

Preparing for deployment

Since we started with the simplest version of a Django project that can run on a local system, we need to change a few things in order to run the project in a production environment. These changes are not specific to Fly.io. Rather, these are changes that bring the BlogMaker Lite project more in line with a default Django project that's built using startproject and startapp.

One way to see what a default Django settings file looks like is to start a new project using startproject, and then look at the settings file that's generated. We don't need every default setting in order to deploy the BlogMaker Lite project, but we do need some that haven't been required for local usage.

There are two default settings we need to add to settings.py:

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
ALLOWED_HOSTS = []

ROOT_URLCONF="blogmaker_lite.urls"
...
blogmaker_lite/settings.py

The BASE_DIR setting is used to define paths within the project. The ALLOWED_HOSTS setting specifies which servers are allowed to host the project.

Now we need to add a few entries to the MIDDLEWARE setting:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "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",
]

The term middleware refers to code that modifies the response object that's being prepared at various points in the request-response cycle. These additional pieces of middleware help serve the project securely and efficiently on a remote server, where it could be accessed by anyone on the internet.

Updating wsgi.py

We also need to modify wsgi.py for use in a production environment:

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blogmaker_lite.settings")
application = get_wsgi_application()
blogmaker_lite/wsgi.py

This sets an environment variable indicating the path to the project's settings file. Without this change, the deployed project will generate an error that the settings file can't be found.

Also, we call get_wsgi_application() instead of making an instance of WSGIHandler directly. The only real change is that get_wsgi_application() includes a call to django.setup(), which is needed before serving the project in production. When running the project on your local system, the runserver command includes a call to django.setup().

Specifying requirements

Requirements, or dependencies, are the packages that a project depends on in order to work. There are a variety of ways to specify requirements in Python. You can use pip, uv, pdm, poetry, pipenv, and a number of other tools. django-simple-deploy will work with any tool that generates a requirements.txt file, Poetry's pyproject.toml file, or Pipenv's Pipfile. Support for other requirement-specification files should be implemented shortly.

To keep things simple, I'm going to use pip here:

$ pip freeze > requirements.txt

If you look at the contents of requirements.txt, you'll see the packages that are required for this project to work on our local system:

$ cat requirements.txt
asgiref==3.8.1
django==5.1.2
factory-boy==3.3.1
faker==30.4.0
python-dateutil==2.9.0.post0
six==1.16.0
sqlparse==0.5.1
typing-extensions==4.12.2

The two highlighted packages are the ones we installed ourselves, django and factory-boy. The others are packages that those two libraries depend on. The deployed project will need a few more packages, but those will be added automatically by django-simple-deploy.

Committing all changes

It's really important to go into the deployment process with a clean Git status. That way, if anything bad happens in the deployment process, you can always go back to a state where it worked on your local system, and try the deployment process again.

If the output of git status doesn't show a clean state, make a new commit with a message such as "Works locally."

$ git status
On branch main
Changes not staged for commit:
    modified:   requirements.txt
$ git commit -am "Updated requirements."
[main afa8c69] Updated requirements.
$ git status
On branch main
nothing to commit, working tree clean

With a clean Git status, we can move on to the actual deployment.

Configuring the project for deployment

The previous changes need to be made in order to deploy BlogMaker Lite to any remote server. Now we're ready to configure the project specifically for deployment to Fly.io. This is where django-simple-deploy shines; it handles all the platform-specific configuration work for you.

There are two modes available with django-simple-deploy, a configuration-only mode and a fully automated mode. The configuration-only mode is better for most use cases, because it lets you see exactly what's happening throughout the deployment process. It doesn't require any manual configuration work on your part, it just requires you to run a few more commands.

If you don't use a tool like django-simple-deploy for this kind of configuration work, you'll need to review the platform's documentation and make manual changes to your project. This typically involves modifying settings.py, and adding some platform-specific configuration files. I've been developing django-simple-deploy to get away from the need for every Django developer to pore over each platform's deployment docs just to get an initial deployment working.

We'll be following the instructions here; the process involves these steps:

  • Install the host platform's CLI.
  • Install django-simple-deploy.
  • Add simple_deploy to INSTALLED_APPS.
  • Create a project on the host platform.
  • Run the simple_deploy command to configure the project for a specific host.
  • Review the changes that were made, and commit those changes.
  • Push the project to the remote server.
  • Open the deployed project in a browser tab.

These are the same steps you'll take whether you're deploying to Fly.io, Platform.sh, Heroku, or just about any other PaaS provider.

Installing the Fly.io CLI

If you want to carry out a deployment on your own, make an account on Fly.io if you haven't already done so. Then follow the instructions for installing the Fly CLI, which is used to issue commands that manage your project on the remote server.

Once the CLI is installed, verify the installation was successful by checking the version, and log in to your account:

$ fly version
fly v0.3.18 darwin/arm64 Commit: ...
$ fly auth login
Opening https://fly.io/app/auth/cli/...
Waiting for session... Done
successfully logged in as...

The login command will open a browser session where you can enter your credentials, and then you'll be authenticated through the CLI.

Installing django-simple-deploy

You can install django-simple-deploy with pip:

$ pip install django-simple-deploy
Collecting django-simple-deploy...
Successfully installed django-simple-deploy-0.7.2 ...

Now add simple_deploy to INSTALLED_APPS:

INSTALLED_APPS=[
    "blogs",
    "accounts",
    "simple_deploy",
    "django.contrib.admin",
    ...
    "django.contrib.staticfiles",
]
blogmaker_lite/settings.py

Let's commit this change, because it's separate from the Fly.io-specific configuration changes that will be made shortly:

$ git commit -am "Added simple_deploy to INSTALLED_APPS."
[main 5f4cd2d] Added simple_deploy to INSTALLED_APPS.
 1 file changed, 1 insertion(+)

This step isn't entirely necessary. simple_deploy checks for a clean Git status before configuring the project, but if this is the only change that's been made before the last commit it should move forward without telling you to make a commit. If you have other changes since your last commit, simple_deploy will suggest that you make a commit before configuring for deployment.

Creating a project on Fly.io

Now we can make a new project on the remote server using the fly apps create command:

$ fly apps create --generate-name
automatically selected personal organization: Eric Matthes
New app created: old-grass-2281

The --generate-name flag lets Fly generate a unique name for this project. My new project is called old-grass-2281. We'll push our code to this project after configuring it to run on Fly's servers.

Configuring BlogMaker Lite for deployment to Fly.io

Now we can run simple_deploy, which will make the changes necessary for our project to run on Fly.io:

$ python manage.py simple_deploy --platform fly_io
...
Configuring project for deployment to Fly.io...
...
*** Found one undeployed app on Fly.io: old-grass-2281 ***
Is this the app you want to deploy to? (yes|no) 
yes
...
A Postgres database is required...
Are you sure you want to do this? (yes|no) 
yes
  Creating database...
...
    Generated Dockerfile: /.../Dockerfile
    Generated fly.toml: /.../fly.toml
  Adding a Fly.io-specific settings block...
    Modified settings.py file: /.../blogmaker_lite/settings.py
...
  Added gunicorn to requirements file.
  Added psycopg2-binary to requirements file.
  Added dj-database-url to requirements file.
  Added whitenoise to requirements file.

--- Your project is now configured for deployment on Fly.io ---
To deploy your project, you will need to:
- Commit the changes made in the configuration process.
    $ git status
    $ git add .
    $ git commit -am "Configured project for deployment."
- Push your project to Fly.io's servers:
    $ fly deploy
- Open your project:
    $ fly open
- You can find a full record of this configuration in the simple_deploy_logs directory.

The --platform argument allows you to specify which host you want to deploy to; here we specify fly_io. There are two prompts in the deployment process. The first makes sure simple_deploy has identified the correct remote app to deploy to, in this case the old-grass-2281 app I just created. The second confirms that you want to create a Postgres database. Postgres is a robust open source database that's often used in production Django deployments.

The output shows exactly what's being added to the project, and what changes are being made. One of the nice things about this process is that all the configuration changes end up being contained in a single commit. Here's the result of git status immediately after running simple_deploy:

$ git status
On branch 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:   .gitignore
	modified:   blogmaker_lite/settings.py
	modified:   requirements.txt
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.dockerignore
	Dockerfile
	fly.toml

Configuration for deployment to Fly.io involved making changes to .gitignore, settings.py, and requirements.txt. It also required three new files: .dockerignore, Dockerfile, and fly.toml.

If you want to see what changes were made, you can run git diff. For example, a new section was added to the end of settings.py that defines settings that only take effect on Fly.io's servers. This way the project continues to run as it always has locally, but also runs correctly on the remote server.

You can also open the three new files and see how they support the deployment process. Dockerfile creates a "container" where the project will run on the server. A container is an environment that can be reproduced on any system, with consistent behavior. The .dockerignore file specifies which parts of the project should not be included in the project's container. The fly.toml file configures some settings that only apply to the remote instance of the project such as the deployed project name, what port it's served over, and a section that ensures the database is migrated during the deployment process.

If you're comfortable with everything you see, make a commit before pushing the project to Fly.io:

$ git add .
$ git commit -am "Configured for deployment to Fly.io."
 6 files changed, 128 insertions(+)
 ...
$ git status
On branch main
nothing to commit, working tree clean

Make sure you run git status and see that you're on your main branch, and that the status is clean before moving on. If anything goes wrong with the deployment, you can either troubleshoot the configuration or roll back to the previous commit and start over.

Deploying the project

So far we've created a project and a database on Fly.io, and we've made platform-specific configuration changes as well. But none of our code has been pushed to the server yet. The fly deploy command takes care of that step:

$ fly deploy
==> Verifying app config
...
==> Building image
...
[+] Building 19.3s (12/12) FINISHED                                                                                                                                                  
 ...
 => CACHED [2/7] RUN mkdir -p /code
 => [4/7] COPY requirements.txt /tmp/requirements.txt
 => [5/7] RUN ... pip install -r /tmp/requirements.txt ...
 => [6/7] COPY . /code/
 => [7/7] RUN ON_FLYIO_SETUP="1" python manage.py collectstatic --noinput
...
--> Building image done
...
Running old-grass-2281 release_command: python manage.py migrate
...
 ✔ release_command 78432d6c42de28 completed successfully
...
Visit your newly deployed app at https://old-grass-2281.fly.dev/

This might take a while, but the output gives you a sense of what's happening. All the code we've written gets pushed to the remote server. Then Fly.io's infrastructure "builds" the project in a way that people can access it. It creates a container based on what it finds in the Dockerfile, and configures the server for the correct kind of access from end users.

After the initial deployment has finished, you can use the fly apps open command to open the deployed version of the project in a new browser tab:

$ fly apps open
opening https://old-grass-2281.fly.dev/ ...

You should see a new browser tab open with your deployed project:

BlogMaker Lite home page, with address bar highlighted showing a fly.io URL specific to this project
The project is now running on a Fly.io server, where it's accessible to anyone on the internet.

You can share this URL with anyone, and they can access your project as well.

Destroying your project

Remember that almost any deployed project will accrue charges, depending on exactly which resources you've created, and how much traffic your project is getting. Surprise charges are no fun, so it's really important to know how to destroy projects when you're practicing deployment.

You can destroy a project using the CLI, or through the Fly.io dashboard. Here's how to destroy the resources used to deploy BlogMaker Lite using the CLI:

$ fly apps list
NAME                OWNER       STATUS      LATEST DEPLOY 
old-grass-2281      personal    deployed    28m45s ago      
old-grass-2281-db   personal    deployed                    
$ fly apps destroy old-grass-2281
Destroying an app is not reversible.
? Destroy app old-grass-2281? Yes
Destroyed app old-grass-2281
$ fly apps destroy old-grass-2281-db
Destroying an app is not reversible.
? Destroy app old-grass-2281-db? Yes
Destroyed app old-grass-2281-db
$ fly apps list
NAME    OWNER   STATUS  LATEST DEPLOY 

First, use the command fly apps list to see all the resources that have been created. Then use the command fly apps destroy <app-name> to destroy a specific app. Make sure you destroy the project you created, and the database app. If you don't destroy both, you'll start to accrue charges. It's a good idea to run fly apps list after destroying your apps, and verify the apps were actually deleted. If this is the only deployment you've made, you should see no apps listed.

You can also visit Fly.io, and click on your dashboard once you're logged in. You'll see each of your apps listed on the dashboard. You can click on each app, and click on the app's Settings link. If you scroll to the bottom of the page, you should see a DELETE button. If you use this approach, you should visit your dashboard again after deleting the apps and make sure they are no longer listed.

Most platforms offer similar ways to destroy resources once you no longer need them. I can't emphasize this enough: whichever platform you choose for hosting, make sure you become familiar with that platform's CLI and dashboard. Make sure you're fully aware of which resources you've created, and which have successfully been destroyed when you no longer need them.

Conclusions

If you're building web apps, it's important to learn about deployment. There's a lot more that goes into maintaining a deployment than what you see here, but getting your project running on a remote server is a first big step in understanding deployment. Once your project is running remotely you can continue to develop your project locally, commit your changes, and then push those changes to the remote server. You can also examine all aspects of the deployed project, to make sure it's running efficiently and securely, and to make regular backups.

The next installment will be the end of the Django from first principles series. In the final post, I'll summarize the main takeaways from this series, and offer some thoughts on where to go next if you've been enjoying these posts.

Resources

You can find the code files from this post in the django-first-principles GitHub repository. The commits from this post are on the part_19 branch. Commits for this branch start at 50caba, with the message Updated settings and wsgi for deployment.