Skip to content

Docker Compose ​

The docker-compose.yml at the pod-portals root orchestrates all services for local development.

Profiles ​

Services are split into two groups:

ProfileServicesStarted by
(default)django, db, redis, celeryworker, celerybeat, portals, docs, mailpit, reportsmake up
testdb-test, django-test, celerybeat-test, celeryworker-test, portals-test, e2e-v2make test-up / make test-run
legacy-e2ecypressdocker compose --profile legacy-e2e run --rm cypress

make up never starts the test stack. The test stack is opt-in only.

Dev Services ​

ServiceImage / BuildPortDescription
django./django-api8000Django REST API
dbmysql:8.0.358100MySQL — dev database
redisredis:7.2.4—Cache + Celery broker
celeryworker./django-api—Celery task worker
celerybeat./django-api—Periodic task scheduler
portalsnode:20-alpine3000React SPA (Vite dev)
docsnode:20-alpine4173VitePress docs
mailpitaxllent/mailpit8025Local email catcher
reportspython:3-alpine8090Serves Cypress HTML reports

Test Services (--profile test) ​

ServiceImage / BuildPortDescription
db-testmysql:8.0.35—MySQL — isolated test database (nhc_test)
django-test./django-api8001Django REST API pointed at nhc_test
celerybeat-test./django-api—Beat for test stack
celeryworker-test./django-api—Worker for test stack
portals-testnode:20-alpine3001React SPA pointed at port 8001
e2e-v2cypress/included:13.15.1—Cypress runner (testing-v2)

Common Commands ​

bash
# First-time setup — copies .env files and builds images
make build

# Start dev services
make up

# Stop dev services
make down

# Start test stack
make test-up

# Seed test database (run once or after make clean)
make test-seed

# Run full E2E suite (seeds + Cypress)
make test-run

# Stop test stack
make test-down

# Rebuild after dependency changes
docker compose build django

# Seed the dev database (fixtures + test users)
make seed

# Run Django management commands
docker compose run --rm django python manage.py migrate
docker compose run --rm django python manage.py shell

# Run management commands against the test database
docker compose --profile test run --rm django-test python manage.py migrate

# Run backend tests
docker compose run --rm django pytest

# View Cypress HTML report (after a run)
docker compose up reports
# then open http://localhost:8090

# Tail logs for a specific service
docker compose logs -f celeryworker
docker compose logs -f portals

Mail service (Mailpit) ​

All outgoing emails from Django are intercepted by Mailpit in local and staging environments. No emails reach real recipients.

Web UI: http://localhost:8025 — view all sent emails after make up.

How it works:

  • Django's EMAIL_BACKEND defaults to smtp in local.py
  • EMAIL_HOST=mailpit / EMAIL_PORT=1025 (set in base.py) routes mail to the Mailpit container
  • Mailpit acts as an SMTP sink and exposes a UI to inspect what was sent

Switching to a real SMTP provider (production):

Set these in the production environment:

bash
DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.yourprovider.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-smtp-user
EMAIL_HOST_PASSWORD=your-smtp-password
EMAIL_USE_TLS=True

Switching to SendGrid (production):

bash
DJANGO_EMAIL_BACKEND=anymail.backends.sendgrid.EmailBackend
SENDGRID_API_KEY=your-key

Volumes ​

VolumeMounted atPurpose
db_data/var/lib/mysqlPersist dev MySQL data
db_test_data/var/lib/mysqlPersist test MySQL data
django_venv/app/.venvPersist Poetry virtualenv (dev)
django_test_venv/app/.venvPersist Poetry virtualenv (test)
portals_node_modules/app/node_modulesIsolate portals deps from host bind mount
portals_test_node_modules/app/node_modulesIsolate portals-test deps
docs_node_modules/app/node_modulesIsolate docs deps from host bind mount
cypress_node_modules/e2e/node_modulesIsolate legacy Cypress deps
cypress_v2_node_modules/e2e/node_modulesIsolate testing-v2 Cypress deps

Environment Files ​

Each service reads its own .env file. make build copies the .example files automatically on first run. See Environment Variables for details.

Known Gotchas ​

createsuperuser error on restart ​

django/compose/local/start.sh runs python manage.py createsuperuser --noinput on every container start. On the first run this creates the admin user fine. On any subsequent restart, Django prints:

CommandError: That email address is already taken.

This is a non-fatal error — the script continues and the server starts normally. You can safely ignore it in the logs. It is pre-existing behaviour in the original start script.

drf-spectacular schema errors on startup ​

During collectstatic, drf-spectacular introspects every view to build the OpenAPI schema. Some views require an authenticated, org-scoped request to resolve their serializer — running without one causes errors like:

Error [FormViewSet]: exception raised while getting serializer.
  (Exception: 'AnonymousUser' object has no attribute 'is_admin')

Error [ContactFormView]: exception raised while getting serializer.
  (Exception: 'Request' object has no attribute 'organization')

Error [PingView]: unable to guess serializer.

These are non-fatal — drf-spectacular skips the affected views and the server starts normally. The OpenAPI schema will be incomplete for those endpoints until the underlying views are made schema-safe. See To Review for the open investigation item.

Node service cold start is slow ​

portals and docs run pnpm install every time the container starts from scratch (i.e. after make clean or a first run). Subsequent restarts skip the install because node_modules is persisted in a named volume. If you see the SPA or docs take a minute to come up on first boot, this is why.

Nova Home Care — Internal Operational Docs