Deployment and Operations

Learning Website with Django

Chapter 12 · Deployment, Testing & Operations

The sites work on a development machine. This chapter is about the part that decides whether they keep working: running them with the production settings, knowing when something is wrong, backing up what cannot be rebuilt, and releasing a new version in a way that can be undone. It is also where honesty matters most, because the target is a Debian server and this was written on Windows, so a large part of it could not be run. Each section says which part was.

What was run and what was not
Run: Django's deployment check, the production settings on a real server process answering real HTTP (90 checks, 0 failed), a database backup, a restore, a full rebuild from the files, and 304 tests (all passing). Not run: Gunicorn (it needs Linux), Apache, systemd, cron, certificates, and the deploy script (its syntax was checked, nothing more). Treat those as a first draft to try on the server, one step at a time.

Where Each Thing Runs

PartJobRun here?
ApacheHTTPS, certificates, the redirect from HTTP, PDFs and solutions straight from disk, everything else passed onNo
GunicornRuns Django (3 workers, 2 threads each) on a unix socket only Apache can reachNo
Django + WhiteNoisePages, search, sitemaps, logins; the theme's static files with hashed namesYes, with the production settings
SQLitePages, search index, accounts, progress, redirectsYes
Content folderThe source of truth for pages; rebuilds the databaseYes

Step 1: Check the Production Settings

The test suite runs under the development settings, so it cannot say whether the production ones work. Django's deployment check can: manage.py check --deploy with the production settings reports two warnings, about HSTS for subdomains and HSTS preloading. They are left on deliberately. Including subdomains would make browsers insist on HTTPS for every name under osztromok.com, including any old host without a certificate, and preloading is close to permanent. HSTS is set to 3600 seconds; raise it in steps only when every host is known to work over HTTPS.

Step 2: Know When It Is Broken

Every site (and the admin host) has a /healthz/ address. It answers 200 only when the process can really serve: the database answers and holds pages (for a site, that site's pages). Otherwise it answers 503. It is never cached and says nothing else, so a monitor or the deploy script can call it freely.

The smoke test goes further. It sends real HTTP requests to a running copy and checks, for each site: health, the front page, that the front page links a stylesheet and the stylesheet loads, robots.txt, the sitemap, search, and a few pages from the sitemap whose canonical link must equal the sitemap address. For the admin it checks that it asks for a login and that /admin/ is a 404 on the content sites. Without a connect address it uses each site's real name, so DNS, the certificate and Apache's choice of virtual host are tested too.

Run (production settings, development server standing in for Gunicorn)Result
curl plain HTTP with no proxy header301 to https://languages.osztromok.com/ (correct)
smoke_test with --forwarded-proto https, as Apache would send90 checks, 0 failed in 6 seconds, 8 sites and the admin
The same without that headerFails (every request is a 301); exit code 1
Nothing listening“could not connect” (a clear error, not a traceback)

The failing runs matter as much as the passing one: a check that has never been seen to fail has not been shown to check anything.

Step 3: Back Up What the Files Cannot Rebuild

The pages are in the content folder and can always be rebuilt. But accounts, reading progress and redirect decisions exist only in the database, and so does the search index (which takes a minute to rebuild). SQLite has an online backup that is safe while the site runs. Copying the file with cp is not: a write in the middle gives a damaged copy.

  • The backup command copies with SQLite's own backup, then opens the copy the way a restore would: integrity check, the page and search tables present, pages not zero. A copy that fails is deleted and reported.
  • It keeps the newest few (14 in the cron entry) and deletes only files named exactly like its own, so a file you put in the folder is safe.
  • A test found a real bug: a damaged file made the check raise instead of reporting “not ok”. It now reports the reason.
Real runResult
Back up the real database4,410 pages, 4,410 indexed, 159 MB, 12 seconds
Restore it (copy in, point LW_DATABASE at it) and search4,410 pages; “szeretnek” 18 results, “ありがとう” 25
Rebuild from nothing: empty database, migrate, import_content4,411 pages in 42 seconds; the same search answers

So pages and search come back from the files in about a minute; accounts, progress and redirects come back only from a backup. A backup kept on the same disk does not survive the disk, so copy them elsewhere and check the copy.

Step 4: Apache, Certificates and Gunicorn (Not Run)

The Apache configuration is generated from the site map (apache_vhosts), so it cannot disagree with it: one HTTPS host for each of the eight sites and the admin host, an HTTP host for each that redirects to HTTPS, the site's own asset folder served by Apache, and X-Forwarded-Proto set so Django knows the visitor used HTTPS. The asset pattern was tested against good and bad addresses, including /../pdfs/a.pdf and /x/./pdfs/a.pdf, which it must not match.

Certificates

  • One wildcard certificate for osztromok.com and *.osztromok.com: needs the DNS challenge, and a new site needs no new certificate. The generated configuration assumes this.
  • One certificate per name (nine names now): no DNS access needed, but every new site is a new certificate and a new place for renewal to fail.

A certificate that quietly expires is the most common way sites like this fail. The smoke test, run from outside by name over HTTPS, notices an expired or wrong certificate.

Gunicorn

A small configuration file (3 workers, 2 threads, a 30-second timeout, workers restarted after about 2,000 requests so a slow leak cannot grow forever) and a systemd unit that runs it on a unix socket as an unprivileged user, with a read-only file system apart from the data, asset and backup folders. The secret key lives in an environment file readable only by that user; the production settings refuse to start without it.

Step 5: A Release With a Way Back

deploy.sh release <git-ref> 1. new folder for the release (a git worktree) + install requirements 2. backup the database # migrations only go forward 3. check --deploy, migrate, collectstatic, import_content, collect_assets 4. switch the current symlink, reload Gunicorn 5. smoke test over HTTPS; if it fails, roll back at once deploy.sh rollback # point the symlink at the previous release, reload
The honest limit of a rollback
Rolling back restores the old code. If the release ran a migration, the old code may not match the new database. Keep migrations backward compatible (add a column in one release, use it in the next), or restore the backup taken at the start of that release, which loses anything written since. The script says so when it rolls back. It passes a syntax check only: it was never run, and it needs a sudoers rule allowing the deploy user to reload that one service.

Hands-On Exercises

Exercise 1

Run Django's deployment check with the production settings and decide each warning. Add a health address to every site, write a smoke test that uses real HTTP, and run it against a server started with the production settings, showing both a passing and a failing run.

📄 View solution
Exercise 2

Write a database backup that is safe while the site runs, checks its own copy, keeps only the newest few and never deletes a file it did not make. Back up the real database, restore it and search it, and time a rebuild from the content files.

📄 View solution
Exercise 3

Generate the Apache virtual hosts from the site map, and write the Gunicorn configuration, the systemd unit and a release script with a rollback. Test what can be tested, and state plainly which parts were not run.

📄 View solution

Chapter 12 Quick Reference

  • check --deploy with the production settings; the two HSTS warnings are left on until every host is known to work over HTTPS
  • /healthz/ on every site: 200 only if the database answers and holds pages; never cached
  • smoke_test: real HTTP to every site; by name over HTTPS it also tests DNS, the certificate and the virtual host
  • Real result: 90 checks, 0 failed under the production settings; fails (exit code 1) without the proxy header
  • Back up with SQLite's online backup, never cp; the copy is opened and checked; only the newest few are kept
  • Real result: 159 MB backed up in 12 s, restored and searched; full rebuild from the files in 42 s
  • Only the database holds accounts, progress, redirects and the search index; the pages always come back from the files
  • Apache hosts are generated from the site map (apache_vhosts); one wildcard certificate covers every site
  • Release: backup, checks, migrate, collectstatic, import, switch symlink, reload, smoke test, roll back if it fails
  • Not run: Gunicorn, Apache, systemd, cron, certificates, deploy.sh (syntax checked only)

Course Complete

You have built a Django project that serves eight sites from one codebase, one database and one design system, and the tools to move the old site onto it without breaking its addresses. What each chapter left you able to do:

ChapterYou can now…
1 · Project Layout for Many SitesLay out one project with a site map as its single source of truth
2 · Subdomain RoutingPick the site from the host name, with a separate URL configuration per site
3 · The Content AppModel courses and pages and read the banner comment of each file
4 · Loading Content from FilesImport 4,400 pages with change detection, re-runnable and safe
5 · Templates & the Shared Design SystemShare one theme across sites, each with its own accent colour
6 · Navigation & BreadcrumbsDerive menus, breadcrumbs and chapter links from the data
7 · The Languages SiteGive one site its own front page and lesson-specific handling
8 · PDFs, Solutions & Static FilesServe three kinds of file by three routes, with safe caching
9 · Search & SitemapsAdd per-site full-text search, sitemaps, canonical links and robots.txt
10 · Admin, Accounts & ProgressAdd a read-only admin on its own host, optional logins and progress
11 · Migrating from the Old SiteMeasure old addresses, review redirects and cut over one site at a time
12 · Deployment, Testing & OperationsCheck production settings, monitor, back up, restore and release with a way back
What is still open
The server side is still a plan: Apache, Gunicorn, systemd, cron, certificates and the release script all need trying on Debian, and the 55 redirect suggestions need reviewing by a person. The next course, Learning Website with Next.js, builds the same sites on a different stack so the two can be compared.