How to Dockerize a Django application

Key takeaways:

  • Dockerizing a Django application ensures it runs consistently across different environments without installation conflicts.

  • A typical Dockerfile uses a base image (e.g., python:3.9), installs dependencies, copies project files, exposes ports, and defines the startup command.

  • We can build a Docker image with a simple command, enabling quick deployment of the Django app.

  • Running the Docker container allows us to map host machine ports to container ports, making the application accessible.

  • Docker provides an isolated environment, reducing setup complexity and allowing developers to focus on building features.

Django is a popular Python framework for creating backend applications. Docker, on the other hand, is a tool that allows containerization, which helps run applications consistently on various infrastructures. Dockerizing a Django application, hence, means creating a container for the Django application so that it can run in any environment without needing any installations or causing any conflicts.

Sample Django application

Below is a very basic Django application that we’ll dockerize for demonstration. Run the given code and click the appended URL to see the output:

import errno
import os
import re
import socket
import sys
from datetime import datetime

from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.servers.basehttp import WSGIServer, get_internal_wsgi_application, run
from django.utils import autoreload
from django.utils.regex_helper import _lazy_re_compile

naiveip_re = _lazy_re_compile(
    r"""^(?:
(?P<addr>
    (?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) |         # IPv4 address
    (?P<ipv6>\[[a-fA-F0-9:]+\]) |               # IPv6 address
    (?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN
):)?(?P<port>\d+)$""",
    re.X,
)


class Command(BaseCommand):
    help = "Starts a lightweight web server for development."

    # Validation is called explicitly each time the server is reloaded.
    requires_system_checks = []
    stealth_options = ("shutdown_message",)
    suppressed_base_arguments = {"--verbosity", "--traceback"}

    default_addr = "0.0.0.0"
    default_addr_ipv6 = "::1"
    default_port = "8000"
    protocol = "http"
    server_cls = WSGIServer

    def add_arguments(self, parser):
        parser.add_argument(
            "addrport", nargs="?", help="Optional port number, or ipaddr:port"
        )
        parser.add_argument(
            "--ipv6",
            "-6",
            action="store_true",
            dest="use_ipv6",
            help="Tells Django to use an IPv6 address.",
        )
        parser.add_argument(
            "--nothreading",
            action="store_false",
            dest="use_threading",
            help="Tells Django to NOT use threading.",
        )
        parser.add_argument(
            "--noreload",
            action="store_false",
            dest="use_reloader",
            help="Tells Django to NOT use the auto-reloader.",
        )
        parser.add_argument(
            "--skip-checks",
            action="store_true",
            help="Skip system checks.",
        )

    def execute(self, *args, **options):
        if options["no_color"]:
            # We rely on the environment because it's currently the only
            # way to reach WSGIRequestHandler. This seems an acceptable
            # compromise considering `runserver` runs indefinitely.
            os.environ["DJANGO_COLORS"] = "nocolor"
        super().execute(*args, **options)

    def get_handler(self, *args, **options):
        """Return the default WSGI handler for the runner."""
        return get_internal_wsgi_application()

    def handle(self, *args, **options):
        if not settings.DEBUG and not settings.ALLOWED_HOSTS:
            raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")

        self.use_ipv6 = options["use_ipv6"]
        if self.use_ipv6 and not socket.has_ipv6:
            raise CommandError("Your Python does not support IPv6.")
        self._raw_ipv6 = False
        if not options["addrport"]:
            self.addr = ""
            self.port = self.default_port
        else:
            m = re.match(naiveip_re, options["addrport"])
            if m is None:
                raise CommandError(
                    '"%s" is not a valid port number '
                    "or address:port pair." % options["addrport"]
                )
            self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
            if not self.port.isdigit():
                raise CommandError("%r is not a valid port number." % self.port)
            if self.addr:
                if _ipv6:
                    self.addr = self.addr[1:-1]
                    self.use_ipv6 = True
                    self._raw_ipv6 = True
                elif self.use_ipv6 and not _fqdn:
                    raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)
        if not self.addr:
            self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr
            self._raw_ipv6 = self.use_ipv6
        self.run(**options)

    def run(self, **options):
        """Run the server, using the autoreloader if needed."""
        use_reloader = options["use_reloader"]

        if use_reloader:
            autoreload.run_with_reloader(self.inner_run, **options)
        else:
            self.inner_run(None, **options)

    def inner_run(self, *args, **options):
        # If an exception was silenced in ManagementUtility.execute in order
        # to be raised in the child process, raise it now.
        autoreload.raise_last_exception()

        threading = options["use_threading"]
        # 'shutdown_message' is a stealth option.
        shutdown_message = options.get("shutdown_message", "")

        if not options["skip_checks"]:
            self.stdout.write("Performing system checks...\n\n")
            self.check(display_num_errors=True)
        # Need to check migrations here, so can't use the
        # requires_migrations_check attribute.
        self.check_migrations()

        try:
            handler = self.get_handler(*args, **options)
            run(
                self.addr,
                int(self.port),
                handler,
                ipv6=self.use_ipv6,
                threading=threading,
                on_bind=self.on_bind,
                server_cls=self.server_cls,
            )
        except OSError as e:
            # Use helpful error messages instead of ugly tracebacks.
            ERRORS = {
                errno.EACCES: "You don't have permission to access that port.",
                errno.EADDRINUSE: "That port is already in use.",
                errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
            }
            try:
                error_text = ERRORS[e.errno]
            except KeyError:
                error_text = e
            self.stderr.write("Error: %s" % error_text)
            # Need to use an OS exit because sys.exit doesn't work in a thread
            os._exit(1)
        except KeyboardInterrupt:
            if shutdown_message:
                self.stdout.write(shutdown_message)
            sys.exit(0)

    def on_bind(self, server_port):
        quit_command = "CTRL-BREAK" if sys.platform == "win32" else "CONTROL-C"

        if self._raw_ipv6:
            addr = f"[{self.addr}]"
        elif self.addr == "0":
            addr = "0.0.0.0"
        else:
            addr = self.addr

        now = datetime.now().strftime("%B %d, %Y - %X")
        version = self.get_version()
        print(
            f"{now}\n"
            f"Django version {version}, using settings {settings.SETTINGS_MODULE!r}\n"
            f"Starting development server at {self.protocol}://{addr}:{server_port}/\n"
            f"Quit the server with {quit_command}.",
            file=self.stdout,
        )
Django application

Creating the Dockerfile

The Dockerfile for containerizing a Django application is pretty straightforward. First of all, we’ll use the python:3.9 base image for the container:

FROM python:3.9

Alternatively, we can also use Ubuntu as our base image, but that would introduce unnecessary complexity in our Dockerfile since we’ll have to manually install Python and its dependencies.

Next up, we’ll install Django inside the container using the pip3 package manager:

RUN pip3 install django

You can also install any other dependencies that your application may require in the same way.

Then, we’ll copy all the project files to the container. The first . here represents the source directory (the directory where the Dockerfile is located), while the second . represents the destination (the current working directory inside the container).

COPY . .

Let’s now expose the port that the container will use to listen on:

EXPOSE 8000

Our Dockerfile is almost complete. We just need to add an instruction to run the Django application when the container is started:

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Building the image

We can build the Docker image based on our Dockerfile by running the following command inside the directory where the Dockerfile is located:

docker build -t django_app .

The above command will create a Docker image with the name django_app. Feel free to change the name of the image as per your liking.

We can then start a container based on the resulting Docker image as follows:

docker run -p 8000:8000 django_app

Here, -p 8000:8000 maps port 8000 on the host machine to port 8000 inside the container.

Given below is the final Dockerfile and the sample application we saw before. Click “Run” to build the image and start the container. Go to the appended URL and you’ll see the same result as before:

FROM python:3.9

# Install Django
RUN pip3 install django

# Copy project files inside the container
COPY . .

EXPOSE 8000

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
Dockerizing the Django application

Conslusion

Dockerizing a Django application enhances its portability and reliability, allowing developers to focus on building features rather than managing environment-specific issues. By encapsulating the application and its dependencies in a Docker container, you can ensure consistent performance across various infrastructures, making it an essential practice for modern application development and deployment.

Frequently asked questions

Haven’t found what you were looking for? Contact Us


Does Docker work with Django?

Yes, Docker can be used to containerize Django applications, ensuring consistent environments and easy deployment.


What does the `-p` flag do in the Docker run command?

The -p flag maps a port on the host machine to a port in the Docker container, enabling access to the application.


Can I use a database with my Dockerized Django app?

Yes, we can connect to databases like PostgreSQL or MySQL in a Dockerized Django application.


What is Docker compose?

Docker compose orchestrates an environment to run multi-container Docker applications. In layman’s language, it enables you to bring multiple containers together to make an application.


Free Resources

Copyright ©2025 Educative, Inc. All rights reserved