Skip to content

Production logging, Docker image and free demo deployment (Render + Neon) - #13

Merged
code-kasha merged 5 commits into
mainfrom
claude/clever-tesla-9x9ams
Sep 26, 2026
Merged

code-kasha merged 5 commits into
mainfrom
claude/clever-tesla-9x9ams

Conversation

@code-kasha

Copy link
Copy Markdown
Owner

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 DEBUG is on. Otherwise it emails ADMINS, 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 with LOG_LEVEL (default INFO).

One container serves everything

  • Django serves the built React app's index.html for every path outside /api/, /admin/ and /static/, so deep links like /leads/7 survive a reload. WhiteNoise serves the assets: Vite's hashed /assets/* files get a one-year immutable cache, and everything is pre-gzipped.
  • The frontend calls /api on the same origin, so there's no CORS setup.
  • The dashboard's Open Swagger link was hard-coded to http://127.0.0.1:8000/api/docs/. It's now built from VITE_API_URL.
  • On Render, ALLOWED_HOSTS and 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: runs migrate and ensure_superuser, then starts gunicorn. Both setup steps are idempotent, so they run on every start.
  • manage.py ensure_superuser: creates the first admin from the DJANGO_SUPERUSER_* variables. Free hosting has no shell to run createsuperuser. The command never resets an existing account.
  • docker-compose.yml: docker compose up --build runs the production image against a local Postgres.
  • CI: a new Docker image job builds the image and smoke-tests the running container: an SPA route, admin static files, a 401 from the API, and admin login.

Hosting: Render (app) + Neon (Postgres), both free with no card

  • render.yaml is a blueprint for a free Docker web service in Singapore. Render generates SECRET_KEY, and you enter DATABASE_URL and the admin credentials in the dashboard.
  • The database is on Neon, because Render's free Postgres is deleted 30 days after creation. Neon's free tier doesn't expire (0.5 GB).
  • docs/DEPLOY.md covers 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

  • Backend: 80 passed. Frontend: 69 passed. All checks pass locally.
  • Checked outside Docker, with the image's layout (the image itself couldn't be built in the dev sandbox, whose proxy breaks TLS inside builds; the new CI job builds it):
    • frontend built with VITE_API_URL=/api, then collectstatic with production settings
    • the real docker-entrypoint.sh against Postgres 17 applied 34 migrations and created the admin; a second start left the admin unchanged
    • SPA routes, gzipped and immutable-cached assets, admin static files and the API all respond correctly
    • a full browser run worked: login redirect, creating a lead, a status change, and a deep-link reload
    • with only RENDER_EXTERNAL_HOSTNAME set, the app starts and still rejects foreign Host headers

To 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

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
@code-kasha
code-kasha merged commit 2cde19f into main Sep 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants