Damara Tern image installation and deployment (Docker)

Table of Content

Docker quick setup

  1. Install Docker engine and Docker compose
  2. Pull Damara Tern frontend and backend images
  3. Update the compose and settings files
  4. Start the stack with Docker compose

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 docker CLI)
  • Docker Compose (the docker compose subcommand)
  • 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:

  1. Open camden.ornl.gov in your browser
  2. Sign in with AzureAD (if prompted, use your ORNL email and UCAMS password)
  3. Open your User Profile in Harbor (top right)
  4. Copy your CLI secret
  5. 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 :latest for 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 UID and GID during 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_drop and no-new-privileges are included to reduce the container’s privilege level.

Important (backend UID/GID): The backend service uses UID and GID environment variables during its entrypoint. Do not rely on the user: 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_pass upstream (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:

  1. Read the mounted settings file
  2. Perform database migrations (against Postgres)
  3. 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 new DATA_PATH_* setting, you should also add a corresponding entry in DATA_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:

  • DEBUG
  • INFO
  • WARNING
  • ERROR
  • CRITICAL

Default: INFO unless configured.


PATH (string)

Where logs are written:

  • a file path (example: /var/log/django.log)
  • stdout
  • stderr

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_USER and POSTGRES_PASSWORD in 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:389
  • ldaps://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:

  • ENABLED
  • URI
  • TEMPLATE
  • ATTR_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.yml specifies your Compose file (omit if your file is docker-compose.yml).
  • --pull always tells Compose to ignore the local cache and fetch the latest images.
  • -d runs 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)
  • If you can load the site but assets are missing:
    • confirm STATIC_WEB_SERVED = true when using the frontend/Nginx container
    • confirm your Nginx /static/ alias matches where static files exist inside the container
  • 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

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/stderr for container log collection (or ensure file logs are shipped)
  • Configure Postgres backups and retention in accordance with your policy