Deployment
Personal Catalogue: Django & PostgreSQL
Chapter 9 · Deployment
Every chapter so far has run against a local dev server. This chapter moves the finished catalogue onto a real server — reusing the Debian/web-server groundwork this site's own Setting Up a Web Server on Debian and Securing Your Web Server courses already cover, rather than re-deriving general server setup here.
Settings Never Travel With the Code
Chapter 1's own settings.py has a real database password and a hardcoded SECRET_KEY sitting directly in it — fine for local development, but neither should ever be committed to version control the way the rest of the codebase is. A committed secret stays in a Git repository's own history forever, even once deleted in a later commit.
The real values live in a local .env file, added to .gitignore, with a placeholder-only .env.example committed in its place — the same real config/example split the PHP variant of this project (catalog-php1) uses for its own config.php/config.example.php pair:
DEBUG = True, an unhandled Django error shows the visitor a full, detailed traceback — real file paths, real settings values, and in a worse case, a database query. DEBUG defaults to False in the settings above specifically so a missing or misconfigured environment variable fails safe, rather than silently leaving debug output enabled on a real, publicly reachable server. Setting ALLOWED_HOSTS correctly also matters directly here — with DEBUG = False, Django refuses to serve any request whose Host header isn't in that list, a real protection against a class of HTTP Host header attacks.
Migrating the Schema to the Production Database
Rather than re-running every migration by hand and re-seeding data manually, the local database can be exported and imported directly:
Alternatively, for a genuinely fresh production database with no data to migrate yet, running python manage.py migrate directly against the production database (once .env points at it) recreates the real schema from Chapter 2's own migration files without needing a dump/restore step at all.
A Dedicated, Least-Privilege Database Role
Connecting as PostgreSQL's own postgres superuser role — even locally, as Chapter 1's example did — is a shortcut worth dropping before this goes anywhere real:
catalogue_app gets exactly what the running app actually needs — SELECT/INSERT/UPDATE/DELETE on the real tables the app queries — and nothing more. It deliberately can't DROP a table, alter the schema, or create new roles, so a bug or a successfully exploited vulnerability in the running app genuinely couldn't do that kind of damage, even in the worst case. DB_USER and DB_PASSWORD in the real production .env point at this role, never at postgres.
Static Files: Real Payoff of Chapter 8
Chapter 8 flagged that STATICFILES_DIRS only serves files in development. Production needs a real STATIC_ROOT plus collectstatic:
WhiteNoise lets Gunicorn itself serve static files efficiently, directly from STATIC_ROOT, without needing a fully separate nginx static-file configuration for a project this size — a real, deliberately lightweight choice matching the same small-project pragmatism this course has followed since Chapter 1.
Gunicorn as a Real Production WSGI Server
python manage.py runserver is explicitly documented as unsuitable for production. A real WSGI server runs the actual application instead:
A real deployment runs this under a process manager (systemd is the natural choice, already covered by this site's own Setting Up a Web Server on Debian course), with nginx or Apache configured as a reverse proxy in front of it — genuine, general reverse-proxy setup this course doesn't re-derive, since it's already covered in full there.
Locking Down the Admin
Chapter 3's own warn-box flagged this directly: the admin shouldn't sit at its default, well-known URL on a real, publicly reachable deployment.
Setting a real, non-obvious ADMIN_URL_PATH in production's own .env — something like a8x92-manage/ — doesn't replace real authentication as a security measure, but it does genuinely reduce exposure to the automated bots that specifically probe /admin/ on every Django site they find, since this project's own admin no longer sits at the address they're already looking for.
HTTPS Is Not Optional Here
This project's own admin and add-item form accept real form submissions, and real database credentials flow through the deployment process. Serving any of it over plain HTTP sends that traffic unencrypted. This site's own HTTPS/TLS Fundamentals course covers obtaining and configuring a certificate in full — a real prerequisite for this project going live, not an optional hardening step to add later.
A Simple, Real Backup Habit
pg_dump writing to the same server's own disk isn't a complete backup strategy — a real disk failure would take the backups down with the live data. For a small personal project, it's still a genuine, meaningful improvement over no backup at all, and copying the resulting .sql files somewhere off that same machine occasionally closes most of the realistic gap without needing a dedicated backup service.
Hands-On Exercises
Create .env.example with placeholder values, add .env to a real .gitignore, and confirm (with git status, assuming the project is a Git repository) that .env genuinely doesn't appear as a trackable file once .gitignore is in place.
Create the catalogue_app PostgreSQL role with exactly the privileges shown in this chapter, then confirm — by trying and expecting it to fail — that a query like DROP TABLE catalogue_item; run as that role is correctly rejected with a real permissions error.
Set DEBUG=False and a deliberately incomplete ALLOWED_HOSTS in a local test setup, then explain the real error Django returns when visiting the site through a hostname not included in that list.
Chapter 9 Quick Reference
- .env / .env.example — real secrets gitignored, placeholders committed, loaded via
python-decouple - DEBUG=False by default — fails safe if the environment variable is ever missing; paired with a real
ALLOWED_HOSTS - catalogue_app role —
SELECT/INSERT/UPDATE/DELETEonly, never the app connecting aspostgres - Static files —
STATIC_ROOT+collectstatic+ WhiteNoise, the real production payoff of Chapter 8's own setup - Gunicorn — a real WSGI server replacing
runserver, run behind a reverse proxy (nginx/Apache, per this site's own web-server courses) - Admin hardening — a non-default admin URL, resolving Chapter 3's own warn-box
- Next chapter: the capstone — integrating this catalogue into the existing Astro site, and a real plan for barcode scanning as the next phase