Northflank backend deployment
This document describes deploying the Notechondria Django backend to Northflank with Northflank's managed PostgreSQL addon. Cloudflare R2 continues to handle static and media storage, since Northflank service filesystems are ephemeral across redeploys.
What this covers
- Django backend only (one Northflank
Combined Service) - Managed PostgreSQL via the Northflank
postgresaddon - Gunicorn web service on the Dockerfile-declared port
8000 - Static and media files on Cloudflare R2
The three Flutter frontends still deploy separately to GitHub Pages via
.github/workflows/frontend-pages.yml.
Files in this repo
| Path | Purpose |
|---|---|
northflank.json | Northflank apiVersion: v1.2 template: Project step, nested Workflow with an Addon (PostgreSQL, type: postgresql) and a CombinedService built from the repo's backend/Dockerfile. Import via northflank template create --file northflank.json or the dashboard Templates > Create UI. |
sample.northflank.env | Copyable env block for the backend service. Paste into Service > Environment > Edit as text. |
deployment/northflank/scripts/northflank_start.sh | Optional start script. Use this as the Combined Service custom command if you prefer not to keep the boot logic inline. |
backend/Dockerfile | Unchanged — build context is the repo root with dockerFilePath: /backend/Dockerfile. |
Option A — Apply the template (recommended)
-
Install the Northflank CLI and authenticate:
npm install -g @northflank/cli northflank login -
Edit the VCS URL if you want to build from your fork. Open
northflank.jsonand changesteps[1].spec.steps[1].spec.vcsData.projectUrlto your repo URL, then apply the template:northflank template apply --file northflank.jsonThis creates a project named
notechondria, anotechondria-postgresaddon (PostgreSQL, TLS on, 4 GB SSD), and anotechondria-backendCombined Service that builds frombackend/Dockerfile. The template wires the PostgreSQL addon into the service'sruntimeEnvironmentvia${refs.database.*}soPOSTGRE_HOST,POSTGRE_PORT,POSTGRE_DB,POSTGRE_USERNAME,POSTGRE_PASSWORD, andDATABASE_URLare populated automatically at deploy time. -
Configure secrets — the template seeds
DJANGO_SECRET_KEYwithREPLACE_ME_FROM_SECRET_GROUP. Create a Secret Group (Project > Secrets > Create), add the sensitive variables fromsample.northflank.env(DJANGO_SECRET_KEY,CASDOOR_CLIENT_SECRET,CASDOOR_CERTIFICATE, R2 keys, and anyGITHUB_DATA_SYNC_APP_*you plan to enable), and link it to the service underService > Environment > Link secret group. -
Trigger the first build from
Service > Builds > Start build.
Option B — Manual setup in the dashboard
-
Create a project.
Create project > notechondria, pick a region close to your users. -
Provision PostgreSQL.
Addons > Add addon > PostgreSQL(addon type key ispostgresql). Picklatest, 1 replica, at least 4 GB SSD storage. Name itnotechondria-postgres. Enable TLS. Wait forRunning. -
Create the backend service.
Create new > Combined service.- Name:
notechondria-backend - VCS: select your fork of this repo, branch
main(orcodex). - Build method: Dockerfile
- Dockerfile path:
/backend/Dockerfile - Build context / work dir:
/(repo root — required because the DockerfileCOPY backend/andCOPY sample/commands run from there)
- Dockerfile path:
- Port:
8000, HTTP, public. - Resources:
nf-compute-20is enough for a smoke deployment.
- Name:
-
Link the PostgreSQL addon to inject
POSTGRE_*andDATABASE_URL. -
Add the backend env variables from
sample.northflank.env(paste intoEnvironment > Edit as text). Put every credential into a Secret Group and link it instead of pasting plaintext secrets. -
Build and deploy. First build pulls Python 3.9, installs requirements, then the container starts. The Dockerfile's
ENTRYPOINT ["/entrypoint.sh"]waits for Postgres, runsmigrate,bootstrap_platform, andcollectstatic --clear, then execs the container command. Northflank sets that command (via the template'scustomCommand) to launch gunicorn on${PORT}. If you configure the service manually, set Docker config to Custom command only and paste:gunicorn notechondria.wsgi:application --chdir /home/notechondria --bind 0.0.0.0:${PORT} --workers ${WEB_CONCURRENCY} --timeout 120
Required environment variables
Set these in the Northflank dashboard (or via the Secret Group):
Django core
DJANGO_SECRET_KEY— 50-char random stringDJANGO_ALLOWED_HOSTS— e.g.*or<service>--notechondria.<region>.code.run,notechondria.example.comDJANGO_CSRF_TRUSTED_ORIGINS— e.g.https://*.code.run,https://notechondria.example.comDJANGO_DEBUG=FalseBACKEND_CUSTOM_DOMAIN(optional) — custom domain attached viaProject > Domains
Database
Provided by the addon link:
DATABASE_URLPOSTGRE_HOST,POSTGRE_PORT,POSTGRE_DB,POSTGRE_USERNAME,POSTGRE_PASSWORD
Cloudflare R2 (required)
Same five variables as the Render deployment — the code paths are identical:
| Variable | Description |
|---|---|
CLOUDFLARE_R2_BUCKET_NAME | R2 bucket name |
CLOUDFLARE_R2_ACCOUNT_ID | Cloudflare account ID |
CLOUDFLARE_R2_ACCESS_KEY_ID | R2 API token access key |
CLOUDFLARE_R2_SECRET_ACCESS_KEY | R2 API token secret key |
CLOUDFLARE_R2_CUSTOM_DOMAIN (optional) | Public R2 hostname |
When CLOUDFLARE_R2_BUCKET_NAME is set, Django auto-switches to R2 for both
static and media files. If the bucket name is set but any of the three
required credentials are missing, the app will fail to start.
Casdoor SSO
The primary auth surface since 0.1.99. Wire the backend to a Casdoor
instance (default: https://auth.trance-0.com) by populating the six
CASDOOR_* env vars in sample.northflank.env:
| Variable | Description |
|---|---|
CASDOOR_ENDPOINT | Casdoor instance URL (no trailing slash) |
CASDOOR_CLIENT_ID | Application's Client ID |
CASDOOR_CLIENT_SECRET | Application's Client secret |
CASDOOR_ORG_NAME | Organization name (e.g. notechondria) |
CASDOOR_APP_NAME | Application name (e.g. notechondria) |
CASDOOR_CERTIFICATE | Public-key PEM, single line, with literal \n escapes |
CASDOOR_TOKEN_CACHE_TTL (optional) | JWT verification cache TTL in seconds (default 300) |
Empty values keep the backend in shadow mode (Casdoor JWT auth is no-op,
the legacy email/password fallback continues to serve). See
docs/integrations/casdoor-setup.md
for the admin-UI walkthrough and per-app redirect URI registration.
Experimental: GitHub data-sync
Since 0.1.90, every authenticated user can push their full account
state (profile, settings, MCP skill, courses, notes, custom_meta,
planner) to a GitHub repo they own via
POST /api/v1/integrations/github/push/. To enable:
- Set the
GITHUB_DATA_SYNC_APP_*env vars (seedocs/integrations/ github-sync.mdfor the full list). - Add
pyjwt + cryptographytobackend/requirements.txtand rebuild the image — the JWT signer used by_refresh_installation_tokenraisesGithubSyncErrorotherwise.
Runtime behaviour
- Northflank injects
PORT; the DockerfileEXPOSE 8000must match the service port setting. The template uses8000for both. entrypoint.shwaits up to 300 s forPOSTGRE_HOST:POSTGRE_PORTbefore runningmigrate. If the addon link is missing or the password is wrong, the container exits with a clear error.collectstatic --noinput --clearuploads built assets to<bucket>/static/on every deploy; user uploads live under<bucket>/media/.- Health check: hit
/api/health/(returns200 OK). Configure underService > Advanced > Health checks.
Custom domains
Northflank publishes services at
https://<service>--<project>.<region>.code.run. To use a custom domain:
Project > Domains > Add domain, enter the apex or subdomain.- Add the DNS records Northflank shows you.
- Set
BACKEND_CUSTOM_DOMAINand updateDJANGO_ALLOWED_HOSTS+DJANGO_CSRF_TRUSTED_ORIGINSto include it. - Update your GitHub Pages frontend config so the portal/editor/planner call the new hostname.
Troubleshooting
- Build fails on
COPY backend/— build context is the repo root. ConfirmbuildSettings.dockerfile.dockerWorkDiris/, not/backend. migratehangs onWaiting for postgres...— the addon link is missing or the service can't reach it. Verify thePOSTGRE_HOSTenv var resolves inside the container (Service > Exec > nslookup).- Static files 404 —
CLOUDFLARE_R2_CUSTOM_DOMAINprobably isn't set; URLs fall back to the S3 endpoint which the browser won't load cross-origin. Either set the custom domain or enable ther2.devpublic subdomain. - First login returns 403 —
DJANGO_CSRF_TRUSTED_ORIGINSmust include the exact origin (scheme + host) of the frontend making the POST.
Notes
- This is backend-only. Frontends still deploy via GitHub Pages.
- The Dockerfile is shared across Jenkins, Render, and Northflank — no platform-specific build image.
- To tear everything down, delete the Combined Service first, then the addon, then the project. Addon data is not retained once deleted.