Repository navigation
Production logging, Docker image and free demo deployment (Render + Neon) - #13
Merged
Merged
Conversation
Django's default logging only writes to the console when DEBUG is on and otherwise emails ADMINS, which aren't configured, so in production every 500 traceback was silently dropped. Add a LOGGING config for production: everything goes to stderr (which container platforms collect) with a timestamp, level and logger name; django.request errors always get through; the level for everything else defaults to INFO and is set with LOG_LEVEL. Tests run the production settings in a subprocess and check that a request error's traceback and app INFO logs reach stderr, and that LOG_LEVEL filters (both fail against the previous settings). Test count now 71. Document LOG_LEVEL in the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018oMHDqDASywmwc7ncmxtCY
Prepares for running everything from one container: - frontend_index serves FRONTEND_DIST/index.html (no-cache) for every path outside /api/, /admin/ and /static/, so deep links like /leads/7 survive a reload; it 404s when no build is present (development) - production: WhiteNoise serves the build's files from the site root, with Vite's hashed /assets/ files marked immutable; compressed static storage - production: RENDER_EXTERNAL_HOSTNAME fills ALLOWED_HOSTS and the CORS/CSRF origin lists when they are unset (explicit values win), so a Render service starts without knowing its final URL in advance - SECURE_SSL_REDIRECT can be turned off to try the image over plain HTTP - the dashboard's "Open Swagger" link was hard-coded to http://127.0.0.1:8000/api/docs/; build it from VITE_API_URL Tests cover SPA routes, API/static paths not being swallowed, the missing build case, the Render fallback and explicit overrides, and the Swagger link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018oMHDqDASywmwc7ncmxtCY
Free hosting plans (e.g. Render's) have no shell, so createsuperuser can't be run by hand. `manage.py ensure_superuser` creates the admin from DJANGO_SUPERUSER_EMAIL / _PASSWORD (+ optional _FIRST_NAME / _LAST_NAME) and is safe to run on every start: it skips when unset, leaves an existing account (matched case-insensitively) untouched so a changed password is never reset, and fails loudly when only half configured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018oMHDqDASywmwc7ncmxtCY
- Dockerfile: Node stage builds the frontend with VITE_API_URL=/api; Python 3.12 stage installs the backend, copies and gzips the build, runs collectstatic and runs as a non-root user - docker-entrypoint.sh: migrate, ensure_superuser, then gunicorn on $PORT (2 workers for a 512 MB instance; logs to stdout/stderr) - docker-compose.yml: the production image against local Postgres - render.yaml: free Docker web service in Singapore, SECRET_KEY generated, DATABASE_URL and admin credentials entered in the dashboard. The database is Neon's free Postgres, because Render's free Postgres is deleted after 30 days - docs/DEPLOY.md: step-by-step Neon + Render setup, free-tier limits, operating and tearing down the demo, env var reference - README: correct prerequisites (Python 3.12+), deployment section; .env.example, CLAUDE.md and CHECKLIST.md updated (80 backend tests, 69 frontend) Verified outside Docker with the same layout: frontend built with VITE_API_URL=/api, collectstatic with production settings, and the real entrypoint against Postgres 17 (migrations, admin created once, second start leaves it unchanged); SPA routes, assets (gzip, immutable cache), admin static, API and a full browser flow all work. The image itself could not be built in this sandbox (its proxy breaks TLS inside builds). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018oMHDqDASywmwc7ncmxtCY
The image couldn't be built in the development sandbox, so let CI prove it: build it, start it with SQLite standing in for Postgres, and check that it migrates, creates the admin, serves an SPA deep link and admin static files, answers the API with 401 when logged out, and accepts the admin's login. Container logs are printed even on failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018oMHDqDASywmwc7ncmxtCY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the app deployable as a single Docker container on free hosting for a 3-month demo, and fixes production error logging.
Production errors are now logged
Django's default logging writes to the console only when
DEBUGis on. Otherwise it emailsADMINS, and none are configured, so every 500 traceback in production was silently dropped (confirmed with a probe). Production now logs everything to stderr with a timestamp, level and logger name. Request errors always get through, and the level for everything else is set withLOG_LEVEL(defaultINFO).One container serves everything
index.htmlfor every path outside/api/,/admin/and/static/, so deep links like/leads/7survive a reload. WhiteNoise serves the assets: Vite's hashed/assets/*files get a one-year immutable cache, and everything is pre-gzipped./apion the same origin, so there's no CORS setup.http://127.0.0.1:8000/api/docs/. It's now built fromVITE_API_URL.ALLOWED_HOSTSand the CORS/CSRF origin lists default to the service's own host when unset. Explicit values still win.Docker
Dockerfile: a Node stage builds the frontend, then a Python 3.12 stage runs gunicorn as a non-root user.docker-entrypoint.sh: runsmigrateandensure_superuser, then starts gunicorn. Both setup steps are idempotent, so they run on every start.manage.py ensure_superuser: creates the first admin from theDJANGO_SUPERUSER_*variables. Free hosting has no shell to runcreatesuperuser. The command never resets an existing account.docker-compose.yml:docker compose up --buildruns the production image against a local Postgres.Hosting: Render (app) + Neon (Postgres), both free with no card
render.yamlis a blueprint for a free Docker web service in Singapore. Render generatesSECRET_KEY, and you enterDATABASE_URLand the admin credentials in the dashboard.docs/DEPLOY.mdcovers the step-by-step setup, free-tier behaviour (the app sleeps after 15 minutes idle and takes about a minute to wake), operating the demo, tearing it down, and every environment variable.Also: the README's prerequisites now say Python 3.12+ (it said 3.11+), and there's a new Deployment section.
Testing
VITE_API_URL=/api, thencollectstaticwith production settingsdocker-entrypoint.shagainst Postgres 17 applied 34 migrations and created the admin; a second start left the admin unchangedRENDER_EXTERNAL_HOSTNAMEset, the app starts and still rejects foreignHostheadersTo deploy
Follow
docs/DEPLOY.md: create a Neon project, then New → Blueprint in Render and paste three values.🤖 Generated with Claude Code
https://claude.ai/code/session_018oMHDqDASywmwc7ncmxtCY
Generated by Claude Code