All posts

How to deploy a Django app in Canada

Ross Hill · September 10, 2026

Django deploys cleanly to a plain Linux server. The parts that take a Django project from "runs locally" to "runs in production" are the same everywhere: a real WSGI server, settings read from the environment, static files collected, a PostgreSQL database, and migrations run at the right moment. Doing all of that on Canadian infrastructure adds no extra steps. You are choosing where the VM lives. The method stays the same.

This post walks through the whole thing on a Coolify server, with the app and its database on the same machine in Toronto.

Why put it in Canada

Some teams have a contractual or policy requirement that customer data stays in Canada. Others just prefer their infrastructure to answer to Canadian law rather than to a US parent company. Either reason is enough on its own. The full argument for Canadian hosting covers the jurisdiction side, including why a US provider's Toronto region is not the same thing as a Canadian provider.

The practical consequence for a Django app is small: pick a server in a Canadian city, and put the database on it or beside it. The rest of this post is the deployment.

Step 1: make the project deploy-ready

Django's own deployment checklist is the authority here, and it is short. The parts that matter most for a container deploy:

Run a real WSGI server. manage.py runserver is a development tool. Gunicorn is the usual choice, and Django's Gunicorn guide is one page long. One thing that page makes explicit and that catches people in containers: by default Gunicorn "will start one process running one thread listening on 127.0.0.1:8000". Inside Docker, that means nothing outside the container can reach it. Bind to 0.0.0.0.

Read settings from the environment. The checklist says the secret key "must be a large random value and it must be kept secret", and that when DEBUG = False, "Django doesn't work at all without a suitable value for ALLOWED_HOSTS". A minimal version:

import os
import dj_database_url

SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS]
DATABASES = {"default": dj_database_url.config(conn_max_age=600)}

Django has no built-in parser for a DATABASE_URL string. dj-database-url, maintained by Jazzband, reads that variable and turns it into a DATABASES dict, which is convenient because that is the format most platforms hand you a database in. CSRF_TRUSTED_ORIGINS entries need the scheme included, so https://example.com, not example.com.

Sort out static files. The checklist is blunt about this: "In production, you must define a STATIC_ROOT directory where collectstatic will copy them." The simplest way to serve them from the app container is WhiteNoise, which requires one middleware entry placed "directly after the Django SecurityMiddleware (if you are using it) and before all other middleware".

Install the right Postgres driver. Django's database notes state that it "supports PostgreSQL 15 and higher" and that "psycopg 3.1.12+ or psycopg2 2.9.9+ is required, though the latest psycopg 3.1.12+ is recommended".

Step 2: choose a build method

Coolify can build from a Dockerfile, from Nixpacks auto-detection, or from a Docker Compose file. Our Coolify deployment guide compares the build packs in more detail, and the short version applies neatly to Django: Nixpacks will usually detect a standard project with a requirements.txt, and a Dockerfile is more predictable because you wrote it.

For Django I would write the Dockerfile. It is about ten lines:

FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
CMD ["gunicorn", "myproject.wsgi", "--bind", "0.0.0.0:8000"]

One gotcha in there. collectstatic imports your settings module, so anything your settings read from the environment at import time has to exist during the build. If SECRET_KEY = os.environ["SECRET_KEY"] raises a KeyError in your build logs, either pass a throwaway value as a build argument or move collectstatic into the container's start command instead.

Step 3: create the PostgreSQL database

In Coolify, add a PostgreSQL resource to the same project as your app. It runs as a container on the same VM, and Coolify shows you an internal connection URL in the familiar shape:

postgres://user:password@<container-name>:5432/dbname

Coolify's documentation puts the condition plainly: if the database and application are in the same network, you reach it with that internal URL, and otherwise you have to make the database reachable over the internet and use the public one. Keep them in the same project so you can use the internal URL and leave the database unexposed.

Paste that URL into your application's DATABASE_URL environment variable and dj_database_url.config() picks it up. Colocating the app and database this way also means there is one location to verify for data residency instead of two, which is the argument in our post on managed PostgreSQL in Canada.

Step 4: deploy

Set the application's environment variables in Coolify: SECRET_KEY, DATABASE_URL, ALLOWED_HOSTS, and whatever else your settings read. Set Ports Exposes to 8000 to match the Gunicorn bind above, then deploy. Coolify pulls the repo, builds the image, starts the container, and gives you a temporary URL to test against before any DNS exists.

If you have not deployed on Coolify before, deploy your first app covers connecting the Git provider and reading the build logs in more detail.

Step 5: run migrations

Coolify applications have Pre-deployment and Post-deployment command fields. Put your migration in the post-deployment one:

python manage.py migrate --noinput

The names tempt you the other way, but Coolify runs the pre-deployment command inside the container that is already running, and skips it entirely when there is none. That is the old code, not the migrations you are shipping, and on a first deploy it is nothing at all. The post-deployment command runs in the newly built container, which is the one that has your new migration files.

That container is already serving traffic by the time the command runs, so keep each migration compatible with the release it replaces. For a one-off such as createsuperuser on first launch, open Coolify's terminal on the application container and run it there.

Step 6: domain and TLS

Point an A record at your server's IP, then enter the full URL in the application's Domains field, like https://example.com. Coolify's Traefik proxy requests a Let's Encrypt certificate once DNS resolves to the server, and renews it.

Two Django settings follow from sitting behind that proxy. TLS terminates at Traefik, so Django sees a plain HTTP request unless you tell it otherwise, and SECURE_PROXY_SSL_HEADER is how you do that:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Django's condition is narrower than it first reads: "You should only set this setting if you control your proxy or have some other guarantee that it sets/strips this header appropriately." You have root on the VM, so the proxy is yours to inspect. The settings page then asks you to confirm two things about it: that it discards any X-Forwarded-Proto a client sends, and that it sets the header itself only for requests that arrived over HTTPS. Check both on your own instance before you turn the setting on. Then add your domain to ALLOWED_HOSTS, which also feeds CSRF_TRUSTED_ORIGINS in the settings snippet above.

Finish by running python manage.py check --deploy against your production settings. It flags the remaining items, including SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE, which the checklist recommends setting to True so those cookies are never sent over plain HTTP.

What MapleDeploy manages, and what you manage

"Managed" means different things on different platforms.

We manage: the VM in Toronto, the operating system and its security updates (kernel reboots land in a fixed 08:00 UTC window), the Coolify installation and its updates, uptime monitoring of the server itself rather than of your app, and weekly full-server snapshots.

You manage: your Django code, your Dockerfile or build config, your environment variables, your migrations, your database schema and its backup schedule, and how you divide the server's RAM and CPU among whatever you run on it.

Those snapshots are disaster recovery, not a database backup. One can be up to seven days old, and your database is on the same VM as the app, so configure Coolify's built-in database backup to S3-compatible storage as a second layer. Our backup guide has the limits of each.

We do not review your Django settings or your dependency versions. That checklist is yours on any host, including this one. The layer underneath is ours. If the VM, Coolify itself, or the network is the problem, email hello@mapledeploy.ca.

Plans run from $45 CAD a month for 4 GB of RAM and 2 vCPUs, up to $695 for 64 GB, flat monthly with no per-service billing for the database. Starter and Pro include a 30-day free trial. That is enough time to put the real project on it and find out whether 4 GB is the right size before you pay for anything.

Deploy Django on a Canadian server

Your app and its PostgreSQL database on a dedicated VM in Toronto. 30 days free on Starter and Pro.