Skip to content

Environment variables

Reference for the configuration files in dmoj/environment/ (site.env, mysql.env, mysql-admin.env): what each variable does, its default, which settings can't be changed through the environment, and how to apply edits.

⏱ ~10 min read · 👤 Operators · 🔑 SSH to the server, permission to edit files in dmoj/ and run docker compose

When you need this page

  • On a fresh install: to fill in site.env and the two MySQL files in Step 4 of Installation.
  • When changing the domain, port, Google OAuth keys or database password.
  • When a config edit doesn't take effect: see Applying changes.

An environment variable is a NAME=value pair that Docker hands to a container when it's created; Django reads them at startup. For other terms, see the Glossary.

All paths below are relative to the dmoj/ directory of the lcoj-docker repository.

Environment files

FileCreated from templateLoaded into servicesContents
environment/site.envenvironment/site.env.examplesite, celery, bridged, wsevent, nginxDjango site configuration
environment/mysql.envenvironment/mysql.env.exampledb, site, celery, bridged, wseventDatabase name, user, password
environment/mysql-admin.envenvironment/mysql-admin.env.exampledbMariaDB root password

The *.env files hold secrets and are listed in .gitignore. Only the *.example files are committed.

Creating the files

  1. Copy the templates:

    sh
    cd dmoj
    cp environment/site.env.example environment/site.env
    cp environment/mysql.env.example environment/mysql.env
    cp environment/mysql-admin.env.example environment/mysql-admin.env
  2. Replace <secret key> and <password> with real values (see Generating SECRET_KEY).

  3. Set HOST, SITE_FULL_URL, and MEDIA_URL to match your domain.

TIP

./scripts/initialize does not create the .env files. It only creates directories and copies the config templates (see Helper scripts).

How LCOJ reads environment variables

Django loads its configuration in this order:

  1. dmoj/repo/dmoj/settings.py holds the defaults. Don't edit it.
  2. At the end, settings.py executes dmoj/repo/dmoj/local_settings.py. ./scripts/initialize copies that file from dmoj/config/local_settings.py.
  3. Inside local_settings.py, some settings are read with os.environ.get('VAR_NAME', 'default'). For those settings, the value in the .env file wins; if the variable is unset, the default written in the code is used.

In other words, environment variables only override the settings that local_settings.py reads from the environment, listed in the tables below. Everything else must be edited in local_settings.py directly (see Settings that aren't environment variables).

site.env

VariableRequired?Default (if unset)Meaning
HOSTYeslocalhostPublic domain name, without http:// and without a port. Used for ALLOWED_HOSTS = [HOST] and the WebSocket addresses ws://HOST/event/ and wss://HOST/event/
SITE_FULL_URLRecommendedhttp://localhost/Full site URL, used to build absolute links (for example in webhooks). Use https:// when the site runs behind an HTTPS reverse proxy
MEDIA_URLYeshttp://localhost/Base URL for media files. nginx serves media at the site root (/martor, /pdf, ...), so this usually matches SITE_FULL_URL (including https://). Must end with /
DEBUGNo0Enabled only when the value is exactly 1. Anything else (true, yes, ...) means off
SECRET_KEYYesemptyDjango's secret key. If it's empty, Django refuses to start
EVENT_DAEMON_POSTNows://wsevent:15101/Where the site posts events to wsevent
REDIS_CACHING_URLNoredis://redis:6379/0Redis used as the cache
CELERY_BROKER_URLNoredis://redis:6379/1Celery task queue
CELERY_RESULT_BACKENDNoredis://redis:6379/1Where Celery stores task results
BRIDGED_HOSTNobridgedHostname of bridged. The site connects to BRIDGED_HOST:9998; bridged listens for judges on BRIDGED_HOST:9999
SOCIAL_AUTH_GOOGLE_OAUTH2_KEYYes (when OAUTH_ONLY = True)emptyGoogle OAuth client ID
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRETYes (when OAUTH_ONLY = True)emptyGoogle OAuth client secret
MOSS_API_KEYNoemptyMOSS key for contest plagiarism checks

Example site.env for an install served over HTTPS at lcoj.example.com (secrets shown as placeholders):

ini
HOST=lcoj.example.com
SITE_FULL_URL=https://lcoj.example.com/
MEDIA_URL=https://lcoj.example.com/

DEBUG=0
SECRET_KEY=<long random string>

# Event server
EVENT_DAEMON_POST=ws://wsevent:15101/

# Redis and Celery
REDIS_CACHING_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/1
CELERY_RESULT_BACKEND=redis://redis:6379/1

# Bridge
BRIDGED_HOST=bridged

# Google sign-in
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=<client-id>.apps.googleusercontent.com
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=<client-secret>

# Plagiarism checks (optional)
MOSS_API_KEY=<moss-user-id>

Things to note:

  • One domain only. ALLOWED_HOSTS contains exactly HOST. To serve another domain as well (for example www.lcoj.example.com), edit ALLOWED_HOSTS (and CSRF_TRUSTED_ORIGINS if you use HTTPS) in local_settings.py.
  • HTTPS needs extra Django settings. Switching SITE_FULL_URL/MEDIA_URL to https:// isn't enough: you also need CSRF_TRUSTED_ORIGINS and SECURE_PROXY_SSL_HEADER in local_settings.py. See Installation: Configure Django for HTTPS.
  • OAuth-only sign-up. The bundled local_settings.py sets OAUTH_ONLY = True, so password registration is disabled. Without the two Google OAuth variables, new users have no way to sign up.
  • SITE_FULL_URL and the trailing /. The template ends with / and the site works fine. However, some code (webhooks) concatenates strings directly, such as SITE_FULL_URL + '/user/...', which can produce // in links. If you use webhooks, consider dropping the trailing /.
  • Empty MOSS_API_KEY. The MOSS tab still shows on contest pages (for users with the moss_contest permission) even without a key, and fails when used. Only use it after you've set MOSS_API_KEY.
  • Internal Docker values (EVENT_DAEMON_POST, REDIS_*, CELERY_*, BRIDGED_HOST) use service names from docker-compose.yml. Only change them if you move services to other machines.

Never enable DEBUG in production

DEBUG=1 makes Django show detailed error pages that expose configuration and internal paths to anyone. Use it only on development machines.

NGINX_PORT

docker-compose.yml publishes nginx with ${NGINX_PORT:-8071}:80. This is a Docker Compose substitution variable, read when Compose parses the YAML file, not a variable inside the container.

  • Default: 8071. The HTTPS reverse proxy on the host (Caddy or nginx) forwards to this port; it's best bound to 127.0.0.1 only. See Installation: HTTPS on a VPS.
  • Compose only takes the value from your shell environment or a dmoj/.env file (next to docker-compose.yml). Setting NGINX_PORT in environment/site.env does not change the published port; it just ends up inside the nginx container, where nothing uses it.

To change the port, create or edit dmoj/.env:

ini
NGINX_PORT=8080

then run docker compose up -d nginx to recreate the nginx container.

mysql.env and mysql-admin.env

VariableFileRequired?Django defaultMeaning
MYSQL_HOSTmysql.envNodbDatabase host Django connects to. The MariaDB image ignores it
MYSQL_DATABASEmysql.envYesdmojDatabase name
MYSQL_USERmysql.envYesdmojApplication database user
MYSQL_PASSWORDmysql.envYesemptyPassword for MYSQL_USER
MYSQL_ROOT_PASSWORDmysql-admin.envYesMariaDB root password, only passed to the db container

Example:

ini
# environment/mysql.env
MYSQL_HOST=db
MYSQL_DATABASE=dmoj
MYSQL_USER=dmoj
MYSQL_PASSWORD=<strong password>
ini
# environment/mysql-admin.env
MYSQL_ROOT_PASSWORD=<different root password>

The database and user are named dmoj by DMOJ convention; you can keep them.

Changing passwords after installation

The MariaDB image only uses MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, and MYSQL_ROOT_PASSWORD for the first initialization, when dmoj/database/ is empty. Changing them later does not change the passwords in the database; Django will just connect with the new password and be refused. To change a password, change it in MariaDB first (ALTER USER ...), then update the .env file.

./scripts/moderate_comments also reads MYSQL_USER, MYSQL_PASSWORD, and MYSQL_DATABASE directly from environment/mysql.env.

Settings that aren't environment variables

These settings are hardcoded in local_settings.py and can't be changed through .env files:

SettingCurrent valueNotes
SITE_NAME'LCOJ'Short name shown on the site
SITE_LONG_NAME'LCOJ: Luyện Code Online Judge'Full name
SITE_ADMIN_EMAIL'[email protected]'Admin email
SERVER_EMAIL'LCOJ: Luyện Code Online Judge <[email protected]>'Sender for error emails
LANGUAGE_CODE'vi'Default language
DEFAULT_USER_TIME_ZONE'Asia/Ho_Chi_Minh'Default user time zone
OAUTH_ONLYTrueDisables password registration
ALLOWED_HOSTS[HOST]Derived from HOST
EVENT_DAEMON_GET, EVENT_DAEMON_GET_SSLws://{HOST}/event/, wss://{HOST}/event/Derived from HOST
EVENT_DAEMON_POLL'/channels/'Long-polling path
CSRF_TRUSTED_ORIGINS, SECURE_PROXY_SSL_HEADERnot setAdd them when running behind an HTTPS reverse proxy; see Installation
DMOJ_PROBLEM_DATA_ROOT, MEDIA_ROOT, STATIC_ROOT/problems/, /media/, /assets/static/Match the volumes in docker-compose.yml
VNOJ_CP_TICKET5Setting inherited from VNOJ
Email (EMAIL_BACKEND, ...), ADMINSnot configured (commented out)

To change them:

  1. Edit the live file dmoj/repo/dmoj/local_settings.py.

  2. Make the same change in the template dmoj/config/local_settings.py, because re-running ./scripts/initialize copies the template over the live file.

  3. Restart the services that run Django:

    sh
    cd dmoj
    docker compose restart site celery bridged

Keep secrets out of local_settings.py

If you need a new secret setting, read it from the environment the same way MOSS_API_KEY does: MY_KEY = os.environ.get('MY_KEY', ''), then put the value in site.env.

Variables for the generate_editorials command

The generate_editorials management command reads OPENAI_API_KEY (required) and OPENAI_BASE_URL (optional) directly from the environment. They aren't in the templates and docker-compose.yml doesn't load them. If you need them, pass them when running the command, for example docker compose exec -e OPENAI_API_KEY=<key> site python3 manage.py generate_editorials ....

Applying changes

docker compose restart does not re-read env_files. A container keeps the environment it was created with. After editing a .env file, recreate the containers with docker compose up -d:

You changedRun (in dmoj/)
environment/site.envdocker compose up -d site celery bridged
environment/mysql.envdocker compose up -d site celery bridged (see the password warning above)
dmoj/.env (NGINX_PORT)docker compose up -d nginx
local_settings.pydocker compose restart site celery bridged

docker compose up -d only recreates containers whose configuration changed; the rest keep running.

Generating SECRET_KEY

SECRET_KEY signs sessions and tokens. Generate a long random string, and don't reuse it across environments (production, dev).

sh
python3 -c 'import secrets; print(secrets.token_urlsafe(50))'
sh
cd dmoj
docker compose exec site python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'

The first command only produces A–Z a–z 0–9 - _, which is safe to paste into a .env file. The second is Django's own helper (mentioned in a comment in local_settings.py), but it can produce characters like $, #, and (; if you use it, double-check the value after pasting.

Changing SECRET_KEY

Changing SECRET_KEY on a running site invalidates existing sessions (everyone gets logged out). Keep the key secret: don't commit it to git or paste it into issues or chat.

Next steps

Need help?