Damara Tern image installation and deployment (Docker)
Table of Content |
Docker quick setup |
Introduction
This document describes how to run the Damara Tern application with Docker Compose using the two published images:
- Frontend:
camden.ornl.gov/damaratern/frontend - Runs Nginx and serves static assets
- Reverse-proxies requests to the backend
- Backend:
camden.ornl.gov/damaratern/backend - Runs Gunicorn for a Django-based application
- Applies database migrations on container startup
The stack also uses:
- PostgreSQL 17 (required)
- Redis (optional; when disabled, the application falls back to local in-memory caching)
Support note: Our team primarily supports Linux hosts. Running Docker on Windows/macOS may work, but support for platform-specific issues may be limited.
Requirements
- Docker Engine (includes the
dockerCLI) - Docker Compose (the
docker composesubcommand) - Access to camden.ornl.gov
Reference: Docker installation docs: https://docs.docker.com/engine/install/
Note: Container engines compatible with the Docker image format (such as Podman) can also be used as alternatives. Commands may differ slightly depending on the tool and environment.
Authenticating to camden.ornl.gov
The container images for Damara Tern are hosted in Camden, the internal Harbor registry, at camden.ornl.gov and requires a UCAMS account to log in.
Before pulling images, sign in to Harbor and retrieve your CLI secret:
- Open camden.ornl.gov in your browser
- Sign in with AzureAD (if prompted, use your ORNL email and UCAMS password)
- Open your User Profile in Harbor (top right)
- Copy your CLI secret
- Log in from the command line with your container engine (docker, podman, etc.)
docker login camden.ornl.gov
When prompted, enter: - Username: your ORNL email address - Password: your Harbor CLI secret
Upon successfully authenticating, you may pull the Damara Tern images from Camden accordingly.
Note: Treat your CLI secret like a password. Do not share it or store it in scripts. You may generate a new secret or set your own by clicking on the
...next to the copy icon in your Harbor user profile.
Images and tags
Pull the images from your registry:
docker pull camden.ornl.gov/damaratern/frontend:latest
docker pull camden.ornl.gov/damaratern/backend:latest
Tagging: Examples below use
:latestfor brevity. For production usage, consider a fixed, versioned tag so upgrades are deliberate and repeatable. Fixed versions are tagged with a hash and pipeline ID (example,:main-30c3668d-3318)
Quick start with Docker compose
This section review the basic configuration steps required to run the images using Docker.
Create your compose file
First, use the compose.yml example below as a template to create a local compose.yml to run your service. Copy it and modify the values as needed for your environment (such as file paths, ports, or environment variables), then run it with Docker Compose to start the services.
The example below is intentionally explicit about:
- Backend runtime UID/GID (via environment variables) – The backend container reads
UIDandGIDduring its entrypoint to determine the runtime user. - Health checks for Postgres and Redis – These checks ensure dependent services are ready before the backend fully starts.
- A hardened default posture for the frontend container – Security options such as
cap_dropandno-new-privilegesare included to reduce the container’s privilege level.
Important (backend UID/GID): The backend service uses
UIDandGIDenvironment variables during its entrypoint. Do not rely on theuser:field in Compose to set these values, as it will not configure the runtime user correctly for this container.
Example: compose.yml
name: damaratern
x-db-credentials: &db-credentials
POSTGRES_USER: &db-user django
POSTGRES_PASSWORD: placeholder
POSTGRES_DB: data
services:
frontend:
container_name: damaratern-frontend
image: camden.ornl.gov/damaratern/frontend:latest
restart: unless-stopped
ports:
- "443:443"
# Optional (HTTP -> HTTPS redirect example below expects this)
- "80:80"
volumes:
# SSL materials (recommended)
- ./path/to/ssl:/etc/nginx/ssl:ro
# Your Nginx config
- ./path/to/nginx.conf:/etc/nginx/nginx.conf:ro
# Optional: mount media directory if using X-Accel-Redirect/internal media serving
# - ./path/to/media:/data:ro
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE, SETUID, SETGID]
security_opt: [no-new-privileges:true]
depends_on:
- backend
backend:
container_name: damaratern-backend
image: camden.ornl.gov/damaratern/backend:latest
restart: unless-stopped
environment:
# Running user/group IDs
UID: 10001
GID: 10001
# Gunicorn settings
SERVER_PORT: 8000
SERVER_WORKER_COUNT: 4
SERVER_TIMEOUT: 120
# Default admin credentials
ADMIN_USERNAME: "admin"
ADMIN_PASSWORD: "admin"
ADMIN_EMAIL: "admin@example.com"
volumes:
- ./path/to/settings.toml:/damaratern/settings.toml:ro
# Optional: mount shared media/data paths used by the app
# - ./path/to/media:/tmp/media
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
container_name: damaratern-db
image: postgres:17
restart: unless-stopped
environment:
<<: *db-credentials
volumes:
- db:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", *db-user]
interval: 5s
retries: 5
timeout: 5s
redis:
container_name: damaratern-redis
image: redis:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
retries: 5
timeout: 5s
volumes:
db: {}
Create and mount the settings file in the container
Before starting the containers, create a settings.toml configuration file for your Damara Tern instance.
The Backend configuration section provides details and examples for creating this file.
The backend container expects its configuration at:
/damaratern/settings.toml
The settings.toml file is stored on the host system and mounted into the container using a bind mount.
In your compose.yml file, edit the backend > volumes section to mount the settings.toml file you created on the host so it is accessible inside the container at /damaratern/settings.toml.
backend:
[...]
volumes:
# Mount the local settings file inside the container (read-only)
- ./settings.toml:/damaratern/settings.toml:ro
Start the services
Once you have created your settings.toml file and updated the compose.yml configuration, you can start the application stack from the directory containing the compose.yml file using:
docker compose up -d
This command:
- Creates and starts all services defined in the Compose file
- Runs the containers in detached mode (
-d), allowing them to run in the background
Note: On the first run, Docker may need to pull the required container images (if they are not already present locally). This may take a few moments depending on your network connection.
Check service status and logs
You can verify that the containers are running and reviewing the logs for the backend service using :
docker compose ps
docker compose logs -f backend
If all services start successfully, the application should now be running according to the configuration defined in settings.toml.
Frontend image (Nginx)
Container behavior
- Starts Nginx
- Serves static files (typically under
/static/) - Reverse-proxies application routes to the backend
Mount points
| Path in container | Purpose | Required |
|---|---|---|
/etc/nginx/nginx.conf |
Main Nginx config | Yes (recommended to provide your own) |
/etc/nginx/ssl/ |
TLS cert/key files (mounted read-only) | Only if using HTTPS |
/run/nginx/ssl/ |
Runtime copy of SSL files | No (managed by entrypoint) |
The image copies /etc/nginx/ssl to /run/nginx/ssl at startup to reduce the likelihood of permissions issues for the nginx worker user.
HTTP-only deployments: If you choose to run without TLS, you may omit the SSL mount and remove TLS directives from your Nginx config. (This is discouraged for anything beyond local testing.)
Example: Nginx configuration
Update at least:
server_name- the HTTP → HTTPS redirect target
- the
proxy_passupstream (it should point at the backend service)
pid /run/nginx.pid;
user nginx;
worker_processes auto;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
# Adjust for your expected upload sizes
client_max_body_size 100G;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
# Redirect HTTP -> HTTPS
server {
listen 80 default_server;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
charset utf-8;
ssl_certificate /run/nginx/ssl/cert.crt;
ssl_certificate_key /run/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# You may want to tailor ciphers to your organization’s baseline requirements
ssl_ciphers HIGH:EECDH+AESGCM:EECDH+AES256:EECDH+AES128:EDH+AES:RSA+AESGCM:RSA+AES:!MEDIUM:!ECDSA:!aNULL:!MD5:!SEED:!IDEA:!DES:!RC4:!DSS:!3DES;
ssl_session_cache shared:SSL:15m;
ssl_session_timeout 10m;
# Serve static files
location /static/ {
alias /var/www/staticfiles/;
autoindex off;
expires 30d;
access_log off;
}
# Internal-only media serving (optional)
location /media/ {
internal;
alias /data/;
}
# Reverse proxy to Gunicorn/Django backend
location / {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Origin "https://localhost";
}
}
}
Backend image (Django + Gunicorn)
Container behavior
On startup, the backend container will typically:
- Read the mounted settings file
- Perform database migrations (against Postgres)
- Launch Gunicorn to serve the Django app
Required mount points and environment variables
| Item | Purpose | Required |
|---|---|---|
/damaratern/settings.toml (mounted) |
Backend configuration file | Yes |
UID / GID (env) |
Runtime user/group selection for file permissions and process ownership | Yes (default: 10001 / 10001) |
The container clones /damaratern/settings.toml to /run/damaratern/settings.toml at startup to avoid permissions issues when running as a non-root user. UID and GID are used to set a custom user and group to run the application under and the user and group that should access relevant data paths. If neither are set, 10001 will be used for each variable. Not setting these to align with your environment may result in permissions issues. If using /data in the container, if it is empty, ownership of the directory will be changed to the provided UID and GID values. Write tests are performed at startup with the provided UID and GID to verify whether or not /data can be written to.
Gunicorn settings
The backend uses Gunicorn to run Damara Tern. The configuration file may be found at /damaratern/gunicorn.conf.py; however, some properties
may be set through the backend container's environment variables out of the box. For customization beyond the below settings, mounting a custom gunicorn.conf.py may be appropriate.
Reference: Gunicorn settings docs: https://gunicorn.org/reference/settings/
Settings
- Port:
SERVER_PORT - Worker Count:
SERVER_WORKER_COUNT - Timeout:
SERVER_TIMEOUT
Default settings
- Port:
8000 - Worker Count:
4 - Timeout:
120
Initial application setup
When the backend container starts for the first time, Damara Tern automatically performs several initialization steps to prepare the database and ensure the application can be accessed.
These steps run after database migrations and include creating an administrator account and loading the required reference data.
Default administrator user
To guarantee access to the Damara Tern interface, the initialization process ensures that an administrator account exists.
If no superuser is present, the system creates one using the credentials defined in compose.yml:
- Username:
ADMIN_USERNAME - Password:
ADMIN_PASSWORD - Email:
ADMIN_EMAIL
If these values are not provided and no superuser exists, a default administrator account is created with the following credentials:
- Username:
admin - Password:
admin - Email:
admin@example.com
âš For security reasons, update these credentials or replace the default administrator account as soon as possible.
Pre-loaded reference data
The initialization process also loads a set of required or suggested reference data using the custom Django management command run_fixtures_initial.
This command populates core system tables with baseline data and runs only when the relevant tables are empty, ensuring that new environments start with a consistent initial state.
Backend configuration: settings.toml
The backend configuration is defined in a settings.toml file.
Create this file using the example configuration below as a template and adjust the values for your environment.
The file must be mounted inside the container at /damaratern/settings.toml. See the Quick start with Docker Compose section for instructions on configuring this mount in compose.yml.
General notes on values and default settings
- Values shown in the examples are defaults or placeholders unless otherwise noted.
- Some options only accept specific values. For example, LDAP enablement flags are strict about the values they allow.
- If an optional setting is omitted, the application will fall back to its internal default.
Example: settings.toml
[default]
# Settings module - static value
DJANGO_SETTINGS_MODULE = "dt.settings"
# Core application settings
[default.APP]
DEBUG = true
DEBUG_TOOLBAR = false
INSTANCE_NICKNAME = "dev" # If unset, defaults to HOSTNAME
REGISTRATION_URL = "https://localhost/login/" # If unset, no link will be shown
TIMEZONE = "America/New_York"
# Module settings
MIDDLEWARE = ["my_app.middleware.CustomMiddleware"]
OPTIONAL_MODULES = []
CUSTOM_MODULES = []
# Data folders and selection headers
DATA_PATH_NON_SENSITIVE = "non_sensitive"
DATA_PATH_SENSITIVE = "sensitive"
DATA_PATH_DEFAULT = "/data"
DATA_PATH_SECONDARY = "/mnt/data"
DATA_PATH_HEADERS = [
["DATA_PATH_DEFAULT", "Default Data"],
["DATA_PATH_SECONDARY", "Secondary Data"],
]
# Path overrides
STATIC_SERVE_MODE = "external"
STATIC_ROOT = "./staticfiles/"
MEDIA_ROOT = "/tmp/media/"
MAX_UPLOAD_SIZE = "100g"
MAX_UPLOAD_MEMORY_SIZE = "100m"
# Security settings
[default.SECURITY]
SECRET_KEY = ""
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
TRUSTED_ORIGINS = [
"http://localhost",
"https://localhost",
"http://127.0.0.1",
"https://127.0.0.1",
]
# Logging settings
[default.LOGGING]
LEVEL = "DEBUG"
PATH = "/var/log/django.log"
# Database settings
[default.POSTGRES]
HOST = "db"
PORT = 5432
NAME = "data"
PASSWORD = ""
# Redis settings
[default.REDIS]
ENABLED = false
HOST = "redis"
# SMTP/Email settings
[default.SMTP]
HOST = "localhost"
PORT = 25
USER = ""
PASSWORD = ""
TLS = false
FROM_EMAIL = ""
MAILTO_EMAIL = ""
MAILTO_DEV_EMAIL = ""
MAILTO_RECIPIENTS = ""
# Primary/main LDAP settings
[default.LDAP.PRIMARY]
ENABLED = true
URI = "ldap://localhost:389"
TEMPLATE = "uid=%(user)s,ou=Users,dc=example,dc=com"
ATTR_MAP = { first_name = "givenName", last_name = "sn", email = "mail" }
# Secondary LDAP settings for custom backends
[default.LDAP.SECONDARY]
ENABLED = false
URI = "ldap://localhost:389"
TEMPLATE = "uid=%(user)s,ou=Users,dc=example,dc=com"
ATTR_MAP = { first_name = "givenName", last_name = "sn", email = "mail" }
Settings reference
This section documents the settings used by the backend. Unless stated otherwise, settings are optional and have sane defaults.
Application Settings
DEBUG (bool)
Enables Django debug mode and debug-level behavior.
Default: false (recommended in production).
DEBUG_TOOLBAR (bool)
Controls whether the Django Debug Toolbar is enabled.
Default: false (disabled).
INSTANCE_NICKNAME (string)
Display name for the instance (visible in the UI, e.g., navbar).
If unset, the application uses the container hostname.
Default: (hostname).
REGISTRATION_URL (string)
When set, the UI may present a link directing users to a registration page or onboarding flow.
If unset, no registration link is shown.
Default: unset.
TIMEZONE (string)
IANA timezone name (for example, America/New_York).
Default: America/New_York unless configured.
MIDDLEWARE (list of strings)
Additional middleware to load. Use this to inject or override behavior
(logging, access checks, request transforms, etc.).
Default: [].
OPTIONAL_MODULES (list of strings)
A list of optional modules to enable when present.
No optional modules are provided by default in the public application.
Default: [].
OPTIONAL_MODULES_ROUTERS (map)
A mapping of modules to their database routers, if present.
Default: {}.
Example:
OPTIONAL_MODULES_ROUTERS = { mymodule = "mymodule.dbrouter.MyModuleRouter" }
CUSTOM_MODULES (list of strings)
Custom modules that override built-in behavior
(for example, authentication backends).
Default: [].
Data paths and UI labels
DATA_PATH_NON_SENSITIVE (string)
Default directory name for non-sensitive data.
Default: non_sensitive unless configured.
DATA_PATH_SENSITIVE (string)
Default directory name for sensitive data.
Default: sensitive unless configured.
DATA_PATH_* (strings)
Any DATA_PATH_… values define named, mounted directories
the application can access (besides ...NON_SENSITIVE and
...SENSITIVE).
These should be absolute paths that exist inside the containers
(or on the host, if not using Docker).
Examples:
DATA_PATH_DEFAULT = "/data"DATA_PATH_SECONDARY = "/mnt/data"
DATA_PATH_HEADERS (list of 2-item arrays)
A TOML list of ["VARIABLE_NAME", "Display Label"] pairs
that maps each DATA_PATH_* variable to a label.
Example:
DATA_PATH_HEADERS = [
["DATA_PATH_DEFAULT", "Default Data"],
["DATA_PATH_SECONDARY", "Secondary Data"],
]
If you add a newDATA_PATH_*setting, you should also add a corresponding entry inDATA_PATH_HEADERS.
Static/media behavior
STATIC_SERVE_MODE (string)
When set to external, indicate to the backend that a web server
(Nginx/Apache) is serving static assets.
Set this to external when the frontend container
(or another web server) serves /static/.
Set this to django explicitly to serve static assets with the backend.
Default: external
STATIC_ROOT (string)
Path used by Django for static collection / static root.
Default: ./staticfiles/
MEDIA_ROOT (string)
Path for uploaded media storage.
Default: /tmp/media/
MAX_UPLOAD_SIZE (string)
Maximum allowed upload size. Supports human-friendly sizes such as
100g, 500m.
Default: 100g
MAX_UPLOAD_MEMORY_SIZE (string)
Maximum chunk size to buffer in memory per upload.
Default: 100m
Security settings: [default.SECURITY]
SECRET_KEY (string)
Django secret key used for cryptographic signing (sessions/cookies).
Required. Default: empty (invalid for production).
Generate a new secret key:
With local Python
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
With Docker using backend image
docker run --rm camden.ornl.gov/damaratern/backend:latest python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
ALLOWED_HOSTS (list of strings)
Hosts the application is allowed to serve from (Django ALLOWED_HOSTS).
Default: ["localhost", "127.0.0.1"]
TRUSTED_ORIGINS (list of strings)
Trusted origins used for CSRF/CORS-related behavior. This must match where your frontend is hosted.
Docker examples often include https://localhost when proxying between containers.
Default:
["http://localhost", "https://localhost", "http://127.0.0.1", "https://127.0.0.1"]
Logging settings: [default.LOGGING]
LEVEL (string)
Logging level. Supported values:
DEBUGINFOWARNINGERRORCRITICAL
Default: INFO unless configured.
PATH (string)
Where logs are written:
- a file path (example:
/var/log/django.log) stdoutstderr
Default: stdout is commonly preferred in Docker,
but this depends on your deployment.
PostgreSQL settings: [default.POSTGRES]
HOST (string)
Database host to connect to.
- In Docker Compose bridge networking, use the service name (example:
db). - Outside Docker, use a hostname or IP.
Default: localhost
PORT (int)
Database port.
Default: 5432
NAME (string)
Database name.
Default: data
PASSWORD (string)
Password for the configured database user.
Default: empty (placeholder)
You will also typically need to configure a database user and password in your Postgres container (for example,POSTGRES_USERandPOSTGRES_PASSWORDin Compose).
Redis settings: [default.REDIS]
ENABLED (bool)
Enable Redis caching when true. When false, the application
falls back to local in-memory caching.
Default: false
HOST (string)
Redis host to connect to (use the Docker service name, e.g., redis,
when on a Compose bridge network).
Default: localhost
SMTP / Email settings: [default.SMTP]
HOST (string)
SMTP host or relay.
Default: localhost
PORT (int)
SMTP port.
Default: 25
USER (string)
SMTP auth username.
Default: empty
PASSWORD (string)
SMTP auth password.
Default: empty
TLS (bool)
Enable TLS for SMTP.
Default: false
FROM_EMAIL (string)
Email address used in the “From” header for outbound email.
Default: empty
MAILTO_EMAIL (string)
Primary recipient for feedback/notifications.
Default: empty
MAILTO_DEV_EMAIL (string)
Optional developer/support recipient address.
Default: empty
MAILTO_RECIPIENTS (string)
Additional recipients to CC (format depends on your organization’s conventions).
Default: empty
LDAP settings: [default.LDAP]
The application can be configured with two LDAP profiles:
PRIMARY and SECONDARY.
- The primary configuration takes precedence when both are enabled.
- The secondary configuration is intended for custom authentication backends or multi-directory setups.
If neither LDAP profile is enabled, the application falls back to basic authentication, managed via Django’s admin interface.
Primary LDAP settings: [default.LDAP.PRIMARY]
ENABLED (bool)
Enable primary or secondary authentication services.
Default: false
URI (string)
LDAP URI, for example:
ldap://host:389ldaps://host:636
Default: placeholder value — update for your environment.
TEMPLATE (string)
Bind/search template used to locate a user entry.
Example:
TEMPLATE = "uid=%(user)s,ou=Users,dc=example,dc=com"
ATTR_MAP (map)
Maps application user fields to LDAP attributes.
first_name, last_name, and email
should be mapped based on available Active Directory attributes.
Default:
ATTR_MAP = { first_name = "givenName", last_name = "sn", email = "mail" }
Secondary LDAP settings: [default.LDAP.SECONDARY]:
Keys and default values are identical to PRIMARY:
ENABLEDURITEMPLATEATTR_MAP
Updating a Docker Container
In our hosting setup, containers are typically managed via Docker Compose. While it is possible to update individual containers manually, the recommended approach is to use the Compose engine and your docker-compose.yml file. This ensures consistency across services, networking, and environment variables.
1. Update All Containers to the Latest Images
To pull the latest images for all services and redeploy them:
docker compose -f compose.yml up -d --pull always
-f compose.ymlspecifies your Compose file (omit if your file isdocker-compose.yml).--pull alwaystells Compose to ignore the local cache and fetch the latest images.-druns containers in detached mode.
This approach updates all services defined in your Compose file while preserving their configuration.
2. Pull a Specific Service Image
If you only want to update a single service (for example, frontend), you can pull the latest image for that service:
docker compose -f compose.yml pull frontend
Then, restart that service with:
docker compose -f compose.yml up -d frontend
This ensures only the specified service is updated without affecting others.
3. Verify the Update
Check that your containers are running:
docker compose ps
Optionally, you can execute a command inside of a specific container:
docker compose exec <service_name> <command>
Example:
docker compose exec frontend ash
Notes:
- The examples above assume you are using the example Compose file for your project. Adjust service names and images according to your actual Compose configuration.
- Avoid manually stopping and removing containers unless there is a specific reason; Compose handles container replacement automatically.
Troubleshooting notes
- If the backend fails to start, check:
- Postgres connectivity (
HOST,PORT, credentials) - whether migrations are failing (backend logs)
- file permissions for mounted paths (UID/GID mismatch)
- Postgres connectivity (
- If you can load the site but assets are missing:
- confirm
STATIC_WEB_SERVED = truewhen using the frontend/Nginx container - confirm your Nginx
/static/alias matches where static files exist inside the container
- confirm
- If the frontend fails to start and HTTPS is in use:
- confirm your certificates are mounted to
/etc/nginx/ssl - confirm your certificates copy to
/run/nginx/ssl
- confirm your certificates are mounted to
Appendix: recommended production adjustments
These are not required to run, but are commonly encouraged in production deployments:
- Use fixed image tags instead of
latest - Use TLS with organization-approved certificates and ciphers
- Set
DEBUG = false - Set a strong
SECRET_KEY - Send logs to
stdout/stderrfor container log collection (or ensure file logs are shipped) - Configure Postgres backups and retention in accordance with your policy