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.4Cache + Celery broker
celeryworker./django-apiCelery task worker
celerybeat./django-apiPeriodic 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.35MySQL — isolated test database (nhc_test)
django-test./django-api8001Django REST API pointed at nhc_test
celerybeat-test./django-apiBeat for test stack
celeryworker-test./django-apiWorker for test stack
portals-testnode:20-alpine3001React SPA pointed at port 8001
e2e-v2cypress/included:13.15.1Cypress 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-fataldrf-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