Making Our App Production-Ready
Our container is working fine but it’s not production-ready. Let’s try to get it there, using the tests to keep us safe.
In a way we’re applying the Red-Green-Refactor cycle to our productionisation process. Our hacky container config got us to Green, and now we’re going to Refactor, working incrementally (just as we would while coding), trying to move from working state to working state, and using the FTs to detect any regressions.
What We Need to Do
What’s wrong with our hacky container image? A few things: first, we need to host our app on the "normal" port 80 so that people can access it using a regular URL.
Perhaps more importantly, we shouldn’t use the Django dev server for production; it’s not designed for real-life workloads. Instead, we’ll use the popular Gunicorn Python WSGI HTTP server.
Django’s runserver is built and optimised for local development and debugging.
It’s designed to handle one user at a time,
it handles automatic reloading upon saving of the source code,
but it isn’t optimised for performance,
nor has it been hardened against security vulnerabilities.
|
In addition, several options in settings.py are currently unacceptable.
DEBUG=True
, is strongly discouraged for production,
we’ll want to set a unique SECRET_KEY
,
and, as we’ll see, other things will come up.
DEBUG=True is considered a security risk, because the django debug page will display sensitive information like the values of variables, and most of the settings in settings.py. |
Let’s go through and see if we can fix things one by one.
Switching to Gunicorn
Do you know why the Django mascot is a pony? The story is that Django comes with so many things you want: an ORM, all sorts of middleware, the admin site… "What else do you want, a pony?" Well, Gunicorn stands for "Green Unicorn", which I guess is what you’d want next if you already had a pony…
We’ll need to first install Gunicorn into our container,
and then use it instead of runserver
:
$ python -m pip install gunicorn Collecting gunicorn [...] Successfully installed gunicorn-2[...]
Gunicorn will need to know a path to a "WSGI server"[1]
which is usually a function called application
.
Django provides one in superlists/wsgi.py.
Let’s change the command our image runs:
[...]
RUN pip install "django<6" gunicorn (1)
COPY src /src
WORKDIR /src
CMD ["gunicorn", "--bind", ":8888", "superlists.wsgi:application"] (2)
1 | Installation is a standard pip install. |
2 | Gunicorn has its own command line, gunicorn .
It needs to know a path to a WSGI server,
which is usually a function called application .
Django provides one in superlists/wsgi.py. |
As in the previous chapter, we can use the docker build && docker run
pattern to try out our changes by rebuilding and rerunning our container:
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -it superlists
If you see an error saying
Bind for 0.0.0.0:8888 failed: port is already allocated. ,
it’ll be because you still have a container running from the previous chapter.
Do you remember how to use docker ps , and docker stop ?
If not, have another look at [how-to-stop-a-docker-container].
|
The FTs catch a problem with static files
As we run the functional tests, you’ll see them warning us of a problem, once again. The test for adding list items passes happily, but the test for layout + styling fails. Good job, tests!
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] AssertionError: 102.5 != 512 within 10 delta (409.5 difference) FAILED (failures=1)
And indeed, if you take a look at the site, you’ll find the CSS is all broken, as in Broken CSS.
The reason that we have no CSS is that although the Django dev server will serve static files magically for you, Gunicorn doesn’t.

One step forward, one step backward, but once again we’ve identified the problem nice and early. Moving on!
Serving Static Files with Whitenoise
Serving static files is very different from serving dynamically rendered content from Python and Django. There are many ways to serve them in production: you can use a web server like Nginx, or a CDN like Amazon S3, but in our case, the most straightforward thing to do is to use Whitenoise, a Python library expressly designed for serving static[2] files from Python.
First we install Whitenoise into our local environment:
pip install whitenoise
Then we tell Django to enable it, in _settings.py_[3]:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
[...]
And then we need to add it to our pip installs in the Dockerfile:
RUN pip install "django<6" gunicorn whitenoise
This manual list of pip installs is getting a little fiddly! We’ll come back to that in a moment. First let’s rebuild and try re-running our FTs:
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -it superlists
And if you take another manual look at your site, things should look much healthier. Let’s rerun our FTs to confirm:
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] ... --------------------------------------------------------------------- Ran 3 tests in 10.718s OK
Phew. Let’s commit that:
$ git commit -am"Switch to Gunicorn and Whitenoise"
Using requirements.txt
Let’s deal with that fiddly list of pip installs.
To reproduce our local virtualenv, rather than just manually pip installing things one by one, and having to remember to sync things between local dev and docker, we can "save" the list of packages we’re using by creating a requirements.txt file.[4]
The pip freeze
command will show us everything that’s installed in our virtualenv at the moment:
$ pip freeze asgiref==3.8.1 attrs==25.3.0 certifi==2025.4.26 Django==5.2.1 gunicorn==23.0.0 h11==0.16.0 idna==3.10 outcome==1.3.0.post0 packaging==25.0 PySocks==1.7.1 selenium==4.31.0 sniffio==1.3.1 sortedcontainers==2.4.0 sqlparse==0.5.3 trio==0.30.0 trio-websocket==0.12.2 typing_extensions==4.13.2 urllib3==2.4.0 websocket-client==1.8.0 whitenoise==6.9.0 wsproto==1.2.0
That shows all the packages in our virtualenv, along with their version numbers. Let’s pull out just the "top-level" dependencies, Django, Gunicorn and Whitenoise:
$ pip freeze | grep -i django Django==5.2[...] $ pip freeze | grep -i django >> requirements.txt $ pip freeze | grep -i gunicorn >> requirements.txt $ pip freeze | grep -i whitenoise >> requirements.txt
That should give us a requirements.txt file that looks like this:
django==5.2.1
gunicorn==23.0.0
whitenoise==6.9.0
Let’s try it out! To install things from a requirements.txt file,
you use the -r
flag, like this:
$ pip install -r requirements.txt Requirement already satisfied: Django==5.2.1 in ./.venv/lib/python3.13/site-packages (from -r requirements.txt (line 1)) (5.2.1) Requirement already satisfied: gunicorn==23.0.0 in ./.venv/lib/python3.13/site-packages (from -r requirements.txt (line 2)) (23.0.0) Requirement already satisfied: whitenoise==6.9.0 in ./.venv/lib/python3.13/site-packages (from -r requirements.txt (line 3)) (6.9.0) Requirement already satisfied: asgiref[...] Requirement already satisfied: sqlparse[...] [...]
As you can see, it’s a no-op because we already have everything installed. That’s expected!
Forgetting the -r and running pip install requirements.txt
is such a common error, that I recommend you do it right now
and get familiar with the error message
(which is thankfully much more helpful than it used to be).
It’s a mistake I still make, all the time.
|
Anyway, that’s a good first version of a requirements file, let’s commit it:
$ git add requirements.txt $ git commit -m "Add a requirements.txt with Django, gunicorn and whitenoise"
Now let’s see how we use that requirements file in our Dockerfile:
FROM python:3.13-slim
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"
COPY requirements.txt /tmp/requirements.txt (1)
RUN pip install -r /tmp/requirements.txt (2)
COPY src /src
WORKDIR /src
CMD ["gunicorn", "--bind", ":8888", "superlists.wsgi:application"]
1 | We COPY our requirements file in, just like the src folder. |
2 | Now instead of just installing Django,
we install all our dependencies using pip install -r . |
Let’s build & run:
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -it superlists
And then test to check everything still works:
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] OK
Hooray. That’s a commit!
$ git commit -am "Use requirements.txt in Dockerfile"
Using Environment Variables to Adjust Settings for Production
We know there are several things in settings.py that we want to change for production:
-
DEBUG
mode is all very well for hacking about on your own server, but it isn’t secure. For example, exposing raw tracebacks to the world is a bad idea. -
SECRET_KEY
is used by Django for some of its crypto—things like cookies and CSRF protection. It’s good practice to make sure the secret key in production is different from the one in your source code repo, because that code might be visible to strangers. We’ll want to generate a new, random one but then keep it the same for the foreseeable future (find out more in the Django docs).
Development, staging and production sites always have some differences in their configuration. Environment variables are a good place to store those different settings. See "The 12-Factor App".[6]
Setting DEBUG=True and SECRET_KEY
There are lots of ways you might set these settings.
Here’s what I propose; it may seem a little fiddly, but I’ll provide a little justification for each choice. Let them be an inspiration (but not a template) for your own choices!
Note that this if statement replaces the DEBUG
and SECRET_KEY
lines
that are included by default in the settings.py file:
import os
[...]
# SECURITY WARNING: don't run with debug turned on in production!
if "DJANGO_DEBUG_FALSE" in os.environ: (1)
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] (2)
else:
DEBUG = True (3)
SECRET_KEY = "insecure-key-for-dev"
1 | We say we’ll use an environment variable called DJANGO_DEBUG_FALSE
to switch debug mode off, and in effect require production settings
(it doesn’t matter what we set it to, just that it’s there). |
2 | And now we say that, if debug mode is off,
we require the SECRET_KEY to be set by a second environment variable. |
3 | Otherwise we fall-back to the insecure, debug mode settings that are useful for Dev. |
The end result is that you don’t need to set any env vars for dev, but production needs both to be set explicitly, and it will error if any are missing. I think this gives us a little bit of protection against accidentally forgetting to set one.
Better to fail hard than allow a typo in an environment variable name to leave you running with insecure settings. |
Setting environment variables inside the Dockerfile
Now let’s set that environment variable in our Dockerfile using the ENV
directive:
WORKDIR /src
ENV DJANGO_DEBUG_FALSE=1
CMD ["gunicorn", "--bind", ":8888", "superlists.wsgi:application"]
And try it out…
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -it superlists [...] File "/src/superlists/settings.py", line 23, in <module> SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^ [...] KeyError: 'DJANGO_SECRET_KEY'
Oops. I forgot to set said secret key env var, mere seconds after having dreamt it up!
Setting Environment Variables at the Docker Command Line
We’ve said we can’t keep the secret key in our source code, so the Dockerfile isn’t an option; where else can we put it?
For now, we can set it at the command line using the -e
flag for docker run
:
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -e DJANGO_SECRET_KEY=sekrit \ -it superlists
With that running, we can use our FT again to see if we’re back to a working state.
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] AssertionError: 'To-Do' not found in 'Bad Request (400)'
The eagle-eyed might spot a message saying
UserWarning: No directory at: /src/static/ .
That’s a little clue about a problem with static files,
that we’re going to deal with shortly.
Let’s deal with this 400 issue first.
|
ALLOWED_HOSTS is Required When Debug Mode is Turned Off
It’s not quite working yet! Let’s take a look manually: An unfriendly 400 error.

We’ve set our two environment variables but doing so seems to have broken things. But once again, by running our FTs frequently, we’re able to identify the problem early, before we’ve changed too many things at the same time. We’ve only changed two settings—which one might be at fault?
Let’s use the "Googling the error message" technique again, with the search terms "django debug false" and "400 bad request".
Well, the very first link in my search results
was Stackoverflow suggesting that a 400 error is usually to do with ALLOWED_HOSTS
,
and the second was the official Django docs,
which takes a bit more scrolling, but confirms it
(see Search results for "django debug false 400 bad request").

ALLOWED_HOSTS
is a security setting
designed to reject requests that are likely to be forged, broken or malicious
because they don’t appear to be asking for your site
(HTTP requests contain the address they were intended for in a header called "Host").
By default, when DEBUG=True, ALLOWED_HOSTS
effectively allows localhost,
our own machine, so that’s why it was working OK until now.
There’s more information in the Django docs.
The upshot is that we need to adjust ALLOWED_HOSTS
in settings.py.
Let’s use another environment variable for that:
if "DJANGO_DEBUG_FALSE" in os.environ:
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = [os.environ["DJANGO_ALLOWED_HOST"]]
else:
DEBUG = True
SECRET_KEY = "insecure-key-for-dev"
ALLOWED_HOSTS = []
This is a setting that we want to change,
depending on whether our Docker image is running locally,
or on a server, so we’ll use the -e
flag again:
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -e DJANGO_SECRET_KEY=sekrit \ -e DJANGO_ALLOWED_HOST=localhost \ -it superlists
Collectstatic is Required when Debug is Turned Off
An FT run (or just looking at the site) reveals that we’ve had a regression in our static files:
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] AssertionError: 102.5 != 512 within 10 delta (409.5 difference) FAILED (failures=1)
And you might have seen this warning message in the docker run
output:
/venv/lib/python3.13/site-packages/django/core/handlers/base.py:61: UserWarning: No directory at: /src/static/ mw_instance = middleware(adapted_handler)
We saw this at the beginning of the chapter,
when switching from the Django dev server to Gunicorn,
and that was why we introduced Whitenoise.
Similarly, when we switch DEBUG off,
Whitenoise stops automagically finding static files in our code,
and instead we need to run collectstatic
:
WORKDIR /src
RUN python manage.py collectstatic
ENV DJANGO_DEBUG_FALSE=1
CMD ["gunicorn", "--bind", ":8888", "superlists.wsgi:application"]
Well, it was fiddly, but that should get us to passing tests after we build & run the docker container!
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -e DJANGO_SECRET_KEY=sekrit \ -e DJANGO_ALLOWED_HOST=localhost \ -it superlists
and…
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] OK
We’re nearly ready to ship to production!
Let’s quickly adjust our gitignore, since the static folder is in a new place, and do another commit to mark this bit of incremental progress:
$ git status # should show dockerfile and untracked src/static folder $ echo src/static >> .gitignore $ git status # should now be clean $ git commit -am "Add collectstatic to dockerfile, and new location to gitignore"
Switching to a nonroot user
Let’s do one more! By default, Docker containers run as root. Although container security is a very well-tested ground by now, experts agree it’s still good practice to use an unprivileged user inside your container.
The main fiddly thing, for us, will be dealing with permissions for the db.sqlite3 file. It will need:
-
To be writable by the nonroot user.
-
To be in a directory that’s writable by the nonroot user.[7]
Making the Database File Path Configurable
First let’s make the path to the database file configurable using an environment variable:
# SECURITY WARNING: don't run with debug turned on in production!
if "DJANGO_DEBUG_FALSE" in os.environ:
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = [os.environ["DJANGO_ALLOWED_HOST"]]
db_path = os.environ["DJANGO_DB_PATH"] (1)
else:
DEBUG = True
SECRET_KEY = "insecure-key-for-dev"
ALLOWED_HOSTS = []
db_path = BASE_DIR / "db.sqlite3" (2)
[...]
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": db_path (3)
}
}
1 | Inside Docker, we’ll assume that an environment variable called
DJANGO_DB_PATH has been set.
We save it to a local variable called db_path . |
2 | Outside Docker, we’ll use the default path to the database file. |
3 | And we modify the DATABASES entry to use our db_path variable. |
Now let’s change the Dockerfile to set that env var, and to create and switch to our nonroot user, which we may as well call "nonroot" (although it could be anything!):
WORKDIR /src
RUN python manage.py collectstatic
ENV DJANGO_DEBUG_FALSE=1
RUN adduser --uid 1234 nonroot (1)
USER nonroot (2)
CMD ["gunicorn", "--bind", ":8888", "superlists.wsgi:application"]
1 | We use the adduser command to create our user,
explicitly setting its uid to 1234.[8] |
2 | The USER directive in the Dockerfile tells Docker to run
everything as that user by default. |
Using UIDs to Set Permissions Across Host/Container Mounts
Our user will now have a writable home directory at /home/nonroot
,
so we’ll put the database file in there.
That takes care of the "writable directory" requirement.
Because we’re mounting the file from outside though,
that’s not quite enough to make the file itself writable.
We’ll need to set the owner of the file to be nonroot
as well.
Because of the way Linux permissions work,
we’re going to use integer user ids (uids).
This might seem a bit magical if you’re not used to Linux permissions,
so you’ll have to trust me I’m afraid.[9]
First, let’s create a file with the right permissions, outside the container:
$ touch container.db.sqlite3 # Change the owner to uid 1234 $ sudo chown 1234 container.db.sqlite3 # This next step is needed on non-Linux dev environments, # to make sure that the container host VM can write to the file. # Change the file to be group-writeable as well as owner-writeable: $ sudo chmod g+rw container.db.sqlite3
Now let’s rebuild and run our container,
changing the --mount
path to our new file,
and setting the DJANGO_DB_PATH
environment variable to match:
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/container.db.sqlite3",target=/home/nonroot/db.sqlite3 \ -e DJANGO_SECRET_KEY=sekrit \ -e DJANGO_ALLOWED_HOST=localhost \ -e DJANGO_DB_PATH=/home/nonroot/db.sqlite3 \ -it superlists
As a first check that we can write to the database from inside the container,
let’s use docker exec
to populate the db tables using manage.py migrate
:
$ docker ps # note container id $ docker exec container-id-or-name python manage.py migrate Operations to perform: Apply all migrations: auth, contenttypes, lists, sessions Running migrations: Applying contenttypes.0001_initial... OK [...] Applying lists.0001_initial... OK Applying lists.0002_item_text... OK Applying lists.0003_list... OK Applying lists.0004_item_list... OK Applying sessions.0001_initial... OK
And, as after every incremental change, we re-run our FT suite to make sure everything works:
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] OK
Great! We wrap up with a bit of housekeeping,
we’ll add this new database file to our .gitignore
,
and commit:
$ echo container.db.sqlite3 >> .gitignore $ git commit -am"Switch to nonroot user"
Configuring logging
One last thing we’ll want to do is make sure that we can get logs out of our server. If things go wrong, we want to be able to get to the tracebacks, and as we’ll soon see, switching DEBUG off means that Django’s default logging configuration changes.
Provoking a deliberate error
To test this, we’ll provoke a deliberate error by corrupting the database file.
$ echo bla > container.db.sqlite3
Now if you run the tests, you’ll see they fail;
$ TEST_SERVER=localhost:8888 python src/manage.py test functional_tests --failfast [...] selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_list_table"]; [...]
And you might spot in the browser that we just see a minimal error page, with no debug info (try it manually if you like):

But if you look in your docker terminal, you’ll see there is no traceback:
[2024-02-28 10:41:53 +0000] [7] [INFO] Starting gunicorn 21.2.0 [2024-02-28 10:41:53 +0000] [7] [INFO] Listening at: http://0.0.0.0:8888 (7) [2024-02-28 10:41:53 +0000] [7] [INFO] Using worker: sync [2024-02-28 10:41:53 +0000] [8] [INFO] Booting worker with pid: 8
Where have the tracebacks gone? You might have been expecting that the django debug page and its tracebacks would disappear from our web browser, but it’s more of shock to see that they are no longer appearing in the terminal either! If you’re like me you might find yourself wondering if we really did see them earlier and starting to doubt your own sanity. But the explanation is that Django’s default logging configuration changes when DEBUG is turned off.
This means we need to interact with the standard library’s logging
module,
unfortunately one of the most fiddly parts of the Python standard library[10].
Here’s pretty much the simplest possible logging config which just prints everything to the console (i.e. standard out). I’ve added this code to the very end of the settings.py file.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {"class": "logging.StreamHandler"},
},
"loggers": {
"root": {"handlers": ["console"], "level": "INFO"},
},
}
Rebuild and restart our container…
$ docker build -t superlists . && docker run \ -p 8888:8888 \ --mount type=bind,source="$PWD/src/db.sqlite3",target=/src/db.sqlite3 \ -e DJANGO_SECRET_KEY=sekrit \ -e DJANGO_ALLOWED_HOST=localhost \ -it superlists
Then try the FT again (or submitting a new list item manually) and we now should see a clear error message:
Internal Server Error: /lists/new Traceback (most recent call last): [...] File "/src/lists/views.py", line 10, in new_list nulist = List.objects.create() ^^^^^^^^^^^^^^^^^^^^^ [...] File "/venv/lib/python3.13/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute return super().execute(query, params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ django.db.utils.DatabaseError: file is not a database
We can fix and re-create the database by doing:
$ echo > container.db.sqlite3 $ docker exec -it <container_id> python manage.py migrate
And re-run the FTs to check we’re back to a working state.
Let’s do a final commit for this change:
$ git commit -am "Add logging config to settings.py"
Exercise for the Reader: Using the Django "check" Command
I don’t have time in this book to cover every last aspect of production-readiness. Apart from anything else, this is a fast-changing area, and security updates to Django and its best practice recommandations change frequently, so things I write now might be incomplete by the time you read the book.
I have given a decent overview of the various different axes along which you’ll need to make production-readiness changes, so hopefully you have a toolset for how to do this sort of work.
If you’d like to dig into this a little bit more, or if you’re preparing a real project for release into the wild, The next step is to read up on Django’s Deployment Checklist.
The first suggestion is to use Django’s "self-check" command,
manage.py check --deploy
.
Here’s what it reported as outstanding when I ran it in April 2025:
$ docker exec <container-id> python manage.py check --deploy System check identified some issues: WARNINGS: ?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security. Be sure to read the documentation first; enabling HSTS carelessly can cause serious, irreversible problems. ?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS. ?: (security.W009) Your SECRET_KEY has less than 50 characters, less than 5 unique characters, or it's prefixed with django-insecure- indicating that it was generated automatically by Django. Please generate a long and random value, otherwise many of Django's security-critical features will be vulnerable to attack. ?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions. ?: (security.W016) You have django.middleware.csrf.CsrfViewMiddleware in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token.
Why not pick one of these and have a go at fixing it?
Wrap-Up
We might not have addressed every last issue that check --deploy
,
but we’ve at least touched on many or most of the things you might need to think about
when considering production-readiness,
we’ve worked in small steps and used our tests all the way along,
and we’re now ready to deploy our container to a real server!
Find out how, in our next exciting instalment…
One more recommendation for PythonSpeed and its Docker Packaging for Python Developers, article. Again, cannot recommend it highly enough. Read it before you’re too much older! |
Comments