Docker deployment (2 images, 3 composes, Tailscale funnels) + committed security suite

Test suite (the manual audit harness, now a real test):
- backend/apps/core/tests/test_security.py: 20 transactional tests across
  guest visibility, object ownership, staged-upload/similarity privacy,
  staff role boundaries, deletion rules, encrypted credentials, throttling
  and the download allowlist. Uses temp media folders and clears cache.
  Needs a one-time GRANT on test_j621 (documented in the module + README).

Images (deploy/J621-Frontend, deploy/J621-Backend, repo root as context):
- Frontend: node build -> static nginx with SPA fallback, asset caching and
  an internal health endpoint.
- Backend: gunicorn + whitenoise (admin static collected at build), ffmpeg
  for video thumbnails, migrations applied on start, GIT_HASH build arg so
  the shell's version pill shows the commit.

Composes (distinct project names so they coexist with the dev stack):
- compose.yml (both), compose.frontend.yml, compose.backend.yml.
- A shared nginx proxy service is the only entry point (no host nginx, no
  published host ports): /api,/admin,/static,/health -> backend, everything
  else -> SPA; both upstreams resolve at request time so one config serves
  all variants.
- A Tailscale sidecar per compose shares the nginx network namespace;
  serve.default/frontend/backend.json use funnel ports 443 and 8443 only
  (10000 is the remaining allowance) with ${TS_CERT_DOMAIN} substitution.

Registry: push_frontend.sh / push_backend.sh / push_all.sh build multi-arch
images and push :latest + :<sha> to the Gitea registry, following the
existing Packs-site pattern.

Docs: deploy/README.md + .env.example, ROADMAP section 6 updated,
AGENTS.md deployment and test notes.

Verified: all three composes validate, both nginx configs pass nginx -t,
both images build, the combined stack boots against real MariaDB/Redis
(migrations applied, /health ok, whitenoise serving admin static, env=prod,
git hash baked in), the proxy serves the SPA and routes /api, and
manage.py test apps.core.tests passes 20/20.
This commit is contained in:
2026-09-18 00:51:29 -05:00
parent f86eccf9a3
commit 8ebda6ab20
23 changed files with 1271 additions and 9 deletions
+21 -1
View File
@@ -61,7 +61,14 @@ if os.getenv("TRUST_PROXY_HEADERS", "false").lower() == "true":
def _git_commit_hash():
"""Short git hash of the running build, mirroring the original app."""
"""Short git hash of the running build, mirroring the original app.
Containers rarely ship the .git directory, so an explicit GIT_COMMIT_HASH
environment variable (baked in at image build time) wins when present.
"""
configured = os.getenv("GIT_COMMIT_HASH", "").strip()
if configured:
return configured[:12]
try:
return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
@@ -98,6 +105,9 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
# Serves the Django admin's static files in production (collected at
# image build time); harmless in dev.
"whitenoise.middleware.WhiteNoiseMiddleware",
# Must sit above CommonMiddleware so preflights are answered early.
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
@@ -173,6 +183,16 @@ USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
# No manifest: works whether or not collectstatic ran (dev included).
"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage",
},
}
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"