> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy Django on Upsun

> A step-by-step guide to deploy a Django application on Upsun, from a blank project to a live URL.


export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

export const GuidesRequirements = ({name}) => {
  const isSymfony = name === "Symfony";
  return <>
      <h2>Before you begin</h2>
      <p>You need:</p>
      <ul>
        <li>
          <a href="https://git-scm.com/downloads">Git</a>.{' '}
          Git is the primary tool to manage everything your app needs to run.
          Push commits to deploy changes and control configuration through YAML files.
          These files describe your infrastructure, making it transparent and version-controlled.
        </li>
        <li>
          An Upsun account.{' '}
          If you don't already have one, <a href="https://auth.upsun.com/register">register for a trial account</a>.{' '}
          You can sign up with an email address or an existing GitHub, Bitbucket, or Google account.
          If you choose one of these accounts, you can set a password for your Upsun account later.
        </li>
        <li>
          The {isSymfony ? <a href="https://symfony.com/download">Symfony CLI</a> : <a href="/cli">Upsun CLI</a>}.{' '}
          This lets you interact with your project from the command line.
          You can also do most things through the <a href="/docs/administration/web">Web Console</a>.
        </li>
      </ul>
    </>;
};

This guide walks you through deploying a Django application on Upsun step by step — from a blank project to a live URL.

If you have read the [Getting started guide](/docs/get-started/here), this page adds Django-specific detail that the generic guide skips.
If you haven't, no problem — this guide is self-contained.

<GuidesRequirements name="Django" />

You also need **Python 3.9+** and `pip` installed locally.

## 1. Create a Django project

Create a directory, activate a virtual environment, and install Django:

```bash theme={null}
mkdir djangotutorial && cd djangotutorial
python -m venv venv
source venv/bin/activate
pip install Django
```

<Info>
  <h4>Windows</h4>
  Replace `source venv/bin/activate` with `venv\Scripts\activate`.
</Info>

Scaffold the project. The trailing `.` places all files directly in the current directory and avoids a nested folder:

```bash theme={null}
django-admin startproject mysite .
```

Your directory now looks like this:

```
djangotutorial/
├── manage.py
└── mysite/
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
```

Initialize a Git repository and make your first commit:

```bash theme={null}
git init && git branch -M main
git add .
git commit -m "Initial Django project"
```

## 2. Add production dependencies

Upsun runs your Django app using [Gunicorn](https://gunicorn.org), a production-grade WSGI server.
You also need a database adapter that matches your chosen database.

<Tabs>
  <Tab title="MariaDB">
    ```bash theme={null}
    pip install gunicorn mysqlclient
    ```
  </Tab>

  <Tab title="PostgreSQL">
    ```bash theme={null}
    pip install gunicorn psycopg2-binary
    ```
  </Tab>
</Tabs>

Save the dependency list so Upsun can install it at build time:

```bash theme={null}
pip freeze > requirements.txt
git add requirements.txt
git commit -m "Add production dependencies"
```

## 3. Set environment variables

Create a `.environment` file at the root of your project.
Upsun sources this file automatically before starting your app, on every environment (production, preview branches, etc.).

```bash .environment theme={null}
export DJANGO_SETTINGS_MODULE="mysite.settings"
export DJANGO_SECRET_KEY="$PLATFORM_PROJECT_ENTROPY"
export DJANGO_ALLOWED_HOSTS="$(echo "$PLATFORM_ROUTES" | base64 --decode | jq -r 'to_entries[] | select(.value.primary == true) | .key' | sed 's:/*$::' | sed 's|https\?://||')"
```

What each variable does:

* **`DJANGO_SETTINGS_MODULE`** — tells Django which settings file to load.
* **`DJANGO_SECRET_KEY`** — uses `PLATFORM_PROJECT_ENTROPY`, a unique value Upsun generates per project. The key is never stored in your repository.
* **`DJANGO_ALLOWED_HOSTS`** — dynamically extracts the hostname of the current environment from your project routes, so every preview branch and production environment works automatically.

<Warning>
  <h4>Commit .environment to Git</h4>
  This file contains no secrets — the values are resolved at runtime from Upsun variables.
  It must be committed so Upsun can source it on every deploy.
</Warning>

```bash theme={null}
git add .environment
git commit -m "Add Upsun environment variables"
```

## 4. Update `settings.py`

Open `mysite/settings.py` and make three changes.

**Add `import os`** at the top of the file, after `from pathlib import Path`:

```python mysite/settings.py theme={null}
import os
```

**Replace the `ALLOWED_HOSTS` line.** The default `[]` rejects all requests when `DEBUG = False`.
Use the environment variable set in the previous step:

```python mysite/settings.py theme={null}
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',')
```

**Add the Upsun production block** at the very bottom of the file.
It only activates when `PLATFORM_APPLICATION_NAME` is present — a variable Upsun injects automatically at runtime.
Your local development settings are untouched.

<Tabs>
  <Tab title="MariaDB">
    ```python mysite/settings.py theme={null}
    # Upsun production settings
    if os.getenv('PLATFORM_APPLICATION_NAME') is not None:
        DEBUG = False

        if os.getenv('PLATFORM_APP_DIR') is not None:
            STATIC_ROOT = os.path.join(os.getenv('PLATFORM_APP_DIR'), 'static')

        if os.getenv('PLATFORM_PROJECT_ENTROPY') is not None:
            SECRET_KEY = os.getenv('PLATFORM_PROJECT_ENTROPY')

        if os.getenv('PLATFORM_ENVIRONMENT') is not None:
            DATABASES = {
                'default': {
                    'ENGINE': 'django.db.backends.mysql',
                    'NAME': os.getenv('DATABASE_PATH'),
                    'USER': os.getenv('DATABASE_USERNAME'),
                    'PASSWORD': os.getenv('DATABASE_PASSWORD'),
                    'HOST': os.getenv('DATABASE_HOST'),
                    'PORT': os.getenv('DATABASE_PORT'),
                }
            }
    ```
  </Tab>

  <Tab title="PostgreSQL">
    ```python mysite/settings.py theme={null}
    # Upsun production settings
    if os.getenv('PLATFORM_APPLICATION_NAME') is not None:
        DEBUG = False

        if os.getenv('PLATFORM_APP_DIR') is not None:
            STATIC_ROOT = os.path.join(os.getenv('PLATFORM_APP_DIR'), 'static')

        if os.getenv('PLATFORM_PROJECT_ENTROPY') is not None:
            SECRET_KEY = os.getenv('PLATFORM_PROJECT_ENTROPY')

        if os.getenv('PLATFORM_ENVIRONMENT') is not None:
            DATABASES = {
                'default': {
                    'ENGINE': 'django.db.backends.postgresql',
                    'NAME': os.getenv('DATABASE_PATH'),
                    'USER': os.getenv('DATABASE_USERNAME'),
                    'PASSWORD': os.getenv('DATABASE_PASSWORD'),
                    'HOST': os.getenv('DATABASE_HOST'),
                    'PORT': os.getenv('DATABASE_PORT'),
                }
            }
    ```
  </Tab>
</Tabs>

The `DATABASE_*` environment variables are exposed automatically by the relationship you define in the next step.

```bash theme={null}
git add mysite/settings.py
git commit -m "Add Upsun production settings"
```

## 5. Create your Upsun project

If you don't have an Upsun project yet, create one with the CLI:

```bash theme={null}
upsun project:create
```

The CLI asks for a project name and region, then automatically links your local repository to the new project.

<Info>
  <h4>Already have a project?</h4>
  If you created a project from the [Upsun Console](https://console.upsun.com), link your local repository to it:

  ```bash theme={null}
  upsun project:set-remote <PROJECT_ID>
  ```

  Find your project ID in the console or by running `upsun project:list`.
</Info>

## 6. Configure Upsun

Create a `.upsun/` directory at the root of your project, then add a `config.yaml` file inside it.
This single file defines your application container, database service, and routing.

<Tabs>
  <Tab title="MariaDB">
    <DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
      {`
              applications:
                mysite:
                  type: python:3.14

                  build:
                    flavor: none

                  relationships:
                    database: "db:mysql"

                  hooks:
                    build: |
                      set -eux
                      pip install --upgrade pip
                      pip install -r requirements.txt
                    deploy: |
                      set -eux
                      python manage.py collectstatic --noinput
                      python manage.py migrate

                  mounts:
                    static:
                      source: storage
                      source_path: static

                  web:
                    commands:
                      start: "gunicorn mysite.wsgi:application -b unix:$SOCKET"
                    upstream:
                      socket_family: unix
                    locations:
                      /:
                        passthru: true
                      /static:
                        allow: true
                        expires: 1h
                        root: static

              routes:
                https://{default}/:
                  type: upstream
                  upstream: "mysite:http"

              services:
                db:
                  type: mariadb:12.3
            `}
    </DynamicCodeBlock>
  </Tab>

  <Tab title="PostgreSQL">
    <DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
      {`
              applications:
                mysite:
                  type: python:3.14

                  build:
                    flavor: none

                  relationships:
                    database: "db:postgresql"

                  hooks:
                    build: |
                      set -eux
                      pip install --upgrade pip
                      pip install -r requirements.txt
                    deploy: |
                      set -eux
                      python manage.py collectstatic --noinput
                      python manage.py migrate

                  mounts:
                    static:
                      source: storage
                      source_path: static

                  web:
                    commands:
                      start: "gunicorn mysite.wsgi:application -b unix:$SOCKET"
                    upstream:
                      socket_family: unix
                    locations:
                      /:
                        passthru: true
                      /static:
                        allow: true
                        expires: 1h
                        root: static

              routes:
                https://{default}/:
                  type: upstream
                  upstream: "mysite:http"

              services:
                db:
                  type: postgresql:17
            `}
    </DynamicCodeBlock>
  </Tab>
</Tabs>

Key points:

* **`build: flavor: none`** — disables default build behaviors so only your `hooks.build` commands run.
* **`relationships`** — exposes `DATABASE_*` environment variables to your app (used by `settings.py`).
* **`hooks.build`** — installs Python dependencies during the build phase (no database access yet).
* **`hooks.deploy`** — runs `collectstatic` and migrations at deploy time (database is available).
* **`mounts`** — declares `static/` as a persistent writable directory for collected static files.
* **`web.commands.start`** — starts Gunicorn over a Unix socket (`$SOCKET`), which Upsun manages automatically.

```bash theme={null}
git add .upsun/
git commit -m "Add Upsun configuration"
```

## 7. Deploy

Push everything to Upsun:

```bash theme={null}
upsun push
```

The first deploy installs dependencies, collects static files, and runs database migrations.
It takes a minute or two. Subsequent deploys are faster.

Open your app once the deploy completes:

```bash theme={null}
upsun url --primary
```

## Troubleshooting

**400 Bad Request on every page**

`ALLOWED_HOSTS` is rejecting the request. Check two things:

1. `mysite/settings.py` has `ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',')` at the **top level** — not inside the `if` block.
2. `.environment` is committed to Git.

**Build fails — `mysqlclient` or `psycopg2` not found**

`requirements.txt` is missing the database adapter. Run:

```bash theme={null}
pip install gunicorn mysqlclient   # or psycopg2-binary
pip freeze > requirements.txt
git add requirements.txt && git commit -m "Fix missing DB adapter"
upsun push
```

**`gunicorn: command not found` in deploy logs**

Same cause — `gunicorn` is missing from `requirements.txt`. Apply the same fix above.

**Static files return 404**

Check that `STATIC_ROOT` in `settings.py` uses `os.path.join(os.getenv('PLATFORM_APP_DIR'), 'static')` and that the mount key in `.upsun/config.yaml` is also `static`. They must match exactly.

## Further resources

### Documentation

* [Python documentation](/docs/languages/python)
* [Managing Python dependencies](/docs/languages/python/dependencies)
* [Configuring web servers](/docs/languages/python/server)
* [Getting started guide](/docs/get-started/here)

### Blogs

* [*Up(sun) and running with Django*](https://upsun.com/blog/setting-up-django-on-upsun/)
