---
title: How to deploy a Django app in Canada
description: >-
  A practical walkthrough for deploying Django and its PostgreSQL database on a
  Canadian server: build config, environment variables, migrations, domain and
  TLS.
date: '2026-09-10'
lastUpdated: '2026-09-10'
keywords:
  - deploy django in canada
  - django hosting canada
  - django postgresql canadian server
  - django coolify deploy
  - django gunicorn docker deploy
  - canadian django hosting
type: article
author: Ross Hill
locale: en_CA
site_name: MapleDeploy
slogan: Powerful hosting on Canadian soil
organization_url: 'https://mapledeploy.ca/'
logo: 'https://mapledeploy.ca//api/logo/lockup'
creator: MapleDeploy
publisher: MapleDeploy
founding_date: '2026-01-13'
email: hello@mapledeploy.ca
geo_region: CA-ON
geo_placename: Toronto
address_country: CA
area_served: Canada
application_category: DeveloperApplication
app_url: 'https://app.mapledeploy.ca'
llms_txt: 'https://mapledeploy.ca/llms.txt'
offers: >-
  Starter $45/mo, Pro $95/mo, Ultra $195/mo, Ultra 32 $395/mo, Ultra 64 $695/mo
  CAD
in_language: en-CA
canonical_url: 'https://mapledeploy.ca/blog/deploy-django-canada'
---

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](/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](https://docs.djangoproject.com/en/stable/howto/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](https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/gunicorn/) 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:

```python
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](https://pypi.org/project/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](https://docs.djangoproject.com/en/stable/ref/settings/#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](https://whitenoise.readthedocs.io/en/latest/django.html), 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](https://docs.djangoproject.com/en/stable/ref/databases/) 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](/blog/deploy-app-coolify-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:

```dockerfile
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:

```text
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](/blog/managed-postgresql-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](/docs/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:

```bash
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](https://docs.djangoproject.com/en/stable/ref/settings/#secure-proxy-ssl-header) is how you do that:

```python
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](/docs/backups) 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](mailto: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.

{% cta-section title="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.
{% /cta-section %}
