Solid bones, but prod is broken in a few load-bearing places. 6/10 as-is, 9/10 with about 2 hours of polish. Architecture choices are right; execution has not caught up to the design.
docker-compose.yml:44-53 runs nginx:alpine with no nginx.conf mount. Default nginx serves the empty /usr/share/nginx/html. So in prod: no frontend, no /api proxy, no /static/, no /media/. The volume mounts at /app/staticfiles are not even on nginx's serving path.
Meanwhile frontend/Dockerfile:8-10 builds a different nginx image with the Vue dist embedded at /usr/share/nginx/html, but the frontend service never publishes a port and is not proxied to. So that nginx is also unreachable.
nginx service, give the frontend service the 80:80 port mapping, and have it proxy_pass /api/ to backend:8000 and serve /static/ + /media/ from a shared volume. Add an actual nginx.conf.
collectstatic is shadowed by the named volume.backend/Dockerfile:10 runs collectstatic at build time and writes to /app/staticfiles in the image. docker-compose.yml:33 then mounts the static_files named volume at the same path. On first run Docker copies image contents into the empty volume (works), but on every subsequent rebuild the volume is stale. New CSS or admin assets will not appear until you docker volume rm static_files.
collectstatic --noinput in entrypoint.sh after mount, not at image build time.
backend/app/urls.py:6 mounts rest_framework.urls (the browsable login/logout views) at /api/. The Vite proxy (vite.config.js:10) routes /api/* to backend. Fine for now, but the moment you add a real API route at /api/users/ it will work, until DRF's /api/login/ tries to take it. Convention is /api-auth/.
psycopg2-binary in prod.Psycopg maintainers explicitly say not to ship -binary to production (libpq linkage issues). Use psycopg[binary]==3.x (psycopg3, Django 4.2+ supported) or build psycopg2 from source. requirements.txt:4.
backend/Dockerfile:15 uses --workers 4 regardless of CPU. Should be --workers ${GUNICORN_WORKERS:-3} or computed from cores at entrypoint.
db has one (good), backend does not. Nginx will happily route to a 500-ing app.
base.py is missing the standard prod toggles:
SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_HSTS_SECONDS = config('SECURE_HSTS_SECONDS', default=0, cast=int)
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
A template that ships without these encourages people to skip them forever.
entrypoint.sh:3. Fine for single-instance, race condition the moment you scale to more than one replica. Worth a comment in the README pointing to a separate migrate job for k8s or Swarm later.
SECRET_KEY=change-me-in-production in .env.example.Better to ship empty (SECRET_KEY=) so config('SECRET_KEY') raises at boot if forgotten. A copy-pasted string is more dangerous than a hard fail.
backend/app/ naming.Project module called app while WORKDIR=/app makes from app.settings.base resolve via /app/app/settings/base.py. Works, but reads like a bug. Most Django templates use backend/config/ or backend/<projectname>/.
No frontend/.env.example, no VITE_API_BASE_URL pattern. First thing every consumer of this template will add.
dev.py has CORS_ALLOW_ALL_ORIGINS = True. base.py has the middleware loaded but no CORS_ALLOWED_ORIGINS. In prod everything is same-origin via nginx so it does not matter. So why is the middleware there at all? Either drop it from prod or set CORS_ALLOWED_ORIGINS from env.
Acceptable for a template, but a 5-line .github/workflows/ci.yml running ruff + npm run build would catch 80% of regressions when consumers fork it.
INSTALLED_APPS has no app of its own.README says python manage.py startapp but there is no example app, no example serializer, no example viewset. New users will hit "what goes where?" on day 1. One toy app (api/ with a HealthView) would make the template self-demonstrating.
entrypoint.sh only on prod Dockerfile.Dev Dockerfile does not copy it, dev compose runs runserver directly. So dev never auto-migrates. Inconsistent.
If you are going to use this for a real project tomorrow, fix these in order. Roughly 2 hours of work.
nginx/nginx.conf, mount it, kill the duplicate frontend nginx (showstopper).collectstatic to entrypoint.sh (silent staleness bug)./api-auth/ (5-second fix, saves a future rename).frontend/.env.example + VITE_API_BASE_URL (every consumer needs it).--workers 4.