This project now uses Ansible to manage all environment configuration and deployment securely.
- 🔐 Secrets are stored encrypted with Ansible Vault (
ansible/group_vars/<env>/vault.yml). - 🧩 Environment files (
.env.dev,.env.test,.env.prod) are generated automatically from a Jinja2 template (.env.j2). - 🚀 Local dev: run
make env ENV=devto create.env.dev, thenmake up ENV=dev. - 🌍 TEST / PROD: deploy using:
ansible-playbook -i ansible/inventory/prod/hosts.ini ansible/playbooks/deploy.yml -e env=prod --vault-id @prompt
This replaces manual
.env.*editing while keeping.env.exampleas a public reference.
Dockerized environment for eXamc featuring:
- Django (web), Gunicorn (prod) / runserver (dev)
- MySQL 8.4
- Redis 7 (Celery broker/results)
- Celery (worker) + Celery Beat
- Nginx (reverse proxy + static/media)
- Private media via Nginx X-Accel-Redirect
⚠️ Internal repo note: the app relies on Entra ID (Azure AD) configuration.
Do not commit any real.env.*files or DB dumps.
- Prerequisites
- Layout
- Environment files
- Entra ID (OIDC) parameters
- Run in DEV
- Makefile commands
- DB seed / import / export (optional)
- MySQL Workbench access
- Private media
- Migrations & updates
- TEST / PROD overview
- Troubleshooting
- Docker & Docker Compose (v2)
make(Linux/macOS; on Windows use WSL)- Free ports: 8000 (Nginx), 3307 (MySQL exposed in dev)
- Access to your Entra ID app registration (to configure OIDC)
.
├─ compose/
│ ├─ base.yml
│ ├─ dev.yml
│ ├─ test.yml
│ └─ prod.yml
├─ deploy/
│ ├─ entrypoint.sh
│ ├─ gunicorn.conf.py
│ └─ nginx/
│ ├─ nginx.dev.conf
│ └─ nginx.ssl.conf # used in test/prod
├─ examc/ # settings/urls/wsgi/asgi
├─ examc_app/ # Django app(s)
├─ Dockerfile
├─ Makefile
├─ requirements.txt
├─ .env.example # sample env (no secrets)
└─ README.md
This section adds an automated workflow without removing the manual approach below.
- Secrets and env values are generated from Ansible templates and Vault-encrypted variables.
- Template:
ansible/templates/.env.j2 - Per-environment vars:
- Non-sensitive:
ansible/group_vars/<env>/app.yml - Secrets (encrypted):
ansible/group_vars/<env>/vault.yml
- Non-sensitive:
Generate your local .env.dev:
make env ENV=devThen start the stack:
make up ENV=devIf
.env.devis missing,make upwill prompt you to runmake envfirst. Real.env.*files remain untracked (ignored by Git). Keep.env.examplefor reference.
.gitignore excludes: .env.* (keep .env.example), SQL dumps, media/, export_tmp/, __pycache__, etc.
.dockerignore excludes: .git, .env.*, dumps, media/, caches, etc.
Create one per environment locally (not versioned), from .env.example:
cp .env.example .env.dev
cp .env.example .env.test
cp .env.example .env.prodExpected variables (example .env.dev):
ENV=dev
SECRET_KEY=change-me
DEBUG=1
ALLOWED_HOSTS=localhost,127.0.0.1
CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
MYSQL_DATABASE=examc
MYSQL_USER=examc
MYSQL_PASSWORD=change-me
MYSQL_ROOT_PASSWORD=change-me-root
DB_HOST=mysql
DB_PORT=3306
REDIS_URL=redis://redis:6379/0
STATIC_ROOT=/static
MEDIA_ROOT=/media
PRIVATE_MEDIA_ROOT=/private_media
# --- Entra ID / OIDC ---
OIDC_ISSUER=https://login.microsoftonline.com/<TENANT_ID>/v2.0
OIDC_CLIENT_ID=<app-client-id>
OIDC_CLIENT_SECRET=<secret>
OIDC_REDIRECT_URI=http://localhost:8000/oidc/callback
OIDC_LOGOUT_REDIRECT_URI=http://localhost:8000/
OIDC_SCOPES=openid,profile,emailOIDC values must match your app registration + redirect URIs.
- Create/Configure an application in Azure Entra ID (your tenant).
- Allow the following dev redirect URIs:
http://localhost:8000/oidc/callback- (optional)
http://127.0.0.1:8000/oidc/callback
- Collect
Client IDand client secret → put into.env.dev. - Django hosts & CSRF must align with your dev URL:
ALLOWED_HOSTS=localhost,127.0.0.1CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
For test/prod, adapt to your public hosts and force HTTPS.
# 1) Create .env.dev and fill values (incl. OIDC)
cp .env.example .env.dev
# ...edit .env.dev
# 2) Start
make up
# 3) Verify
make health # HEAD /healthz → 200
make ps # all services "healthy"
# 4) Open
xdg-open http://127.0.0.1:8000 # (use open/start on macOS/Windows)make up # build + start
make down # stop
make reset # stop + remove volumes (DB data!)
make ps # status
make logs # tail logs for all services
make web-shell # shell inside web container
make makemigrations # django makemigrations
make migrate # django migrate
make collectstatic # django collectstatic
make createsuperuser
make health # HEAD /healthz via Nginx
make nginx-reload # test & reload Nginx
make dbshell # mysql client (root) inside container
make dbdump # export DB -> deploy/db/dump-YYYYmmdd_HHMMSS.sql.gz
make dbimport FILE=deploy/db/foo.sql.gz # import .sql(.gz)
make rebuild-web # rebuild web service only
make prune # prune dangling imagesFor test/prod:
make up ENV=test(uses.env.test+compose/test.yml), etc.
- Export:
make dbdump # → deploy/db/dump-YYYYmmdd_HHMMSS.sql.gz - Import:
make dbimport FILE=deploy/db/my_dump.sql.gz
- Conditional seed: provide
deploy/db/dev-seed.sql.gz(not versioned) and run thedb_seedservice (dedicated profile). Seed runs only if DB is empty.
Dev connection:
- Host:
127.0.0.1 - Port:
3307 - User/Pass:
MYSQL_USER/MYSQL_PASSWORD - DB:
MYSQL_DATABASE
- Mounted under
/private_media(web/nginx). - Django returns
X-Accel-Redirectto/_protected/.... - Nginx (dev):
location /_protected/ { internal; alias /private_media/; }
- Settings:
PRIVATE_MEDIA_ROOT = os.environ.get("PRIVATE_MEDIA_ROOT", "/private_media") PRIVATE_MEDIA_URL = "/_protected/"
- DEV: entrypoint auto-runs
migrate(andcollectstaticifCOLLECTSTATIC=1). - TEST/PROD: no auto-migrate at boot. Apply migrations via CI job or manual step:
docker compose ... exec web python manage.py migrate --noinput docker compose ... exec web python manage.py collectstatic --noinput # reload Gunicorn/Nginx
- Overrides:
compose/test.yml,compose/prod.yml - HTTPS via
nginx.ssl.conf+ certs (ACME/Let’s Encrypt or internal) - Security:
SECURE_SSL_REDIRECT=1, cookie*_SECURE=1, HSTS enabled - Gunicorn in front (never
runserver) - Controlled migrations, centralized logging, backups, monitoring
Ansible deployment (TEST/PROD):
ansible-playbook -i ansible/inventory/<env>/hosts.ini ansible/playbooks/deploy.yml -e env=<env> --vault-id @promptThis playbook renders /opt/examc/.env from .env.j2, decrypts Vault values, and runs docker compose up -d --build on the target host.
- MIME
text/plainfor JS/CSS: ensuremime.typesis included;alias /static//alias /media/paths correct. the input device is not a TTY: usedocker compose exec -Tfor non-interactive commands (done in Makefile).DB not reachable: check.env.*(DB_HOST=mysql,DB_PORT=3306), startup order, healthchecks.- OIDC issues:
- Redirect URI must exactly match (including port),
ALLOWED_HOSTS&CSRF_TRUSTED_ORIGINSalign with the URL,- Box clock is correct (JWTs are time-sensitive).
eXamc is distributed under the eXamc Non-Commercial License (NCL) v1.0.
- Use in production for non-commercial purposes
- Use by universities, public institutions, research groups, non-profits, and individuals
- Modification and redistribution for non-commercial use, with attribution
- Any commercial use
- Selling or licensing the software
- Offering a SaaS or hosted service based on eXamc
- Integrating eXamc into a commercial product or paid service
- Using eXamc in a for-profit context
A separate commercial license may be granted upon request.
👉 See the full license text in LICENSE.
For questions or assistance regarding eXamc, please contact: