Compose: Secrets management

Zulip’s Docker container uses Docker secrets to synchronize secrets between services, as well as within Zulip itself. These secrets are used to authenticate connections between services in the deployment, serving as defense in depth from SSRF, as well as to authenticate to outside providers (e.g. outgoing email services).

Docker Compose offers two backends for secrets – from the environment, or from files on disk.

Store secrets in a .env file

The simplest deployment technique is to provide secrets in the environment, most commonly stored in an environment file. Note that is the environment for the compose file itself, not the environment of the container. Environment variables defined in .env do not directly propagate into the container’s environments.

Place in compose.override.yaml (alongside any other configuration):

secrets:
  zulip__postgres_password:
    environment: "ZULIP__POSTGRES_PASSWORD"
  zulip__memcached_password:
    environment: "ZULIP__MEMCACHED_PASSWORD"
  zulip__rabbitmq_password:
    environment: "ZULIP__RABBITMQ_PASSWORD"
  zulip__redis_password:
    environment: "ZULIP__REDIS_PASSWORD"
  zulip__secret_key:
    environment: "ZULIP__SECRET_KEY"
  zulip__email_password:
    environment: "ZULIP__EMAIL_PASSWORD"

In a file named .env (which should not be checked into version control), provide the secrets.

ZULIP__POSTGRES_PASSWORD=example_postgres_password
ZULIP__MEMCACHED_PASSWORD=example_memcached_password
ZULIP__RABBITMQ_PASSWORD=example_rabbitmq_password
ZULIP__REDIS_PASSWORD=example_redis_password

ZULIP__SECRET_KEY=example_django_secret_key

ZULIP__EMAIL_PASSWORD=example_outgoing_email_password

See the .env file syntax for a complete reference on the syntax.

Store secrets in files in a directory

Secrets can also be stored as flat files on disk, at paths of your choosing.

Place in compose.override.yaml (alongside any other configuration):

secrets:
  zulip__postgres_password:
    file: /path/to/secrets/postgres
  zulip__memcached_password:
    file: /path/to/secrets/memcached
  zulip__rabbitmq_password:
    file: /path/to/secrets/rabbitmq
  zulip__redis_password:
    file: /path/to/secrets/redis
  zulip__secret_key:
    file: /path/to/secrets/django_key
  zulip__email_password:
    file: /path/to/secrets/outgoing_email

Then, in /path/to/secrets/, place each of those files. Take care to not include trailing newlines in them; for example:

echo -n "example_postgres_password" > /path/to/secrets/postgres

Additional secrets

If your deployment needs additional secrets, you must prefix them with zulip__, and add them to the top-level secrets element, as well as the secrets attribute for the zulip service.

For instance, to add a giphy_api_key secret, the compose.override.yaml might contain:

secrets:
  # Standard secrets
  zulip__postgres_password:
    environment: "ZULIP__POSTGRES_PASSWORD"
  zulip__memcached_password:
    environment: "ZULIP__MEMCACHED_PASSWORD"
  zulip__rabbitmq_password:
    environment: "ZULIP__RABBITMQ_PASSWORD"
  zulip__redis_password:
    environment: "ZULIP__REDIS_PASSWORD"
  zulip__secret_key:
    environment: "ZULIP__SECRET_KEY"
  zulip__email_password:
    environment: "ZULIP__EMAIL_PASSWORD"

  # New, additional secret
  zulip__giphy_api_key:
    environment: "ZULIP__GIPHY_API_KEY"

services:
  zulip:
    # Tell Docker Compose that the zulip container needs access to the additional secret
    secrets:
      - zulip__giphy_api_key

With an additional value in .env:

# Standard secrets
ZULIP__POSTGRES_PASSWORD=...
ZULIP__MEMCACHED_PASSWORD=...
ZULIP__RABBITMQ_PASSWORD=...
ZULIP__REDIS_PASSWORD=...
ZULIP__SECRET_KEY=...
ZULIP__EMAIL_PASSWORD=...

# Additional secret
ZULIP__GIPHY_API_KEY=PS42beKkLnOUBOqb1BgTyna87ooKgthE

Rotating the PostgreSQL password

The zulip__postgres_password secret is used by the database container only at first boot, when it creates the zulip PostgreSQL user with that password. Changing ZULIP__POSTGRES_PASSWORD (or swapping its source file) afterwards does not propagate to the already-initialized database; the zulip container will then fail to authenticate.

To rotate the password on a running deployment, update both sides:

  1. Run an ALTER ROLE query against the database with the new password:

    docker compose exec database \
      psql -U zulip -c "ALTER ROLE zulip WITH PASSWORD 'new_password';"
    
  2. Update ZULIP__POSTGRES_PASSWORD in your .env file (or the file-based secrets backend you’ve configured) to match.

  3. Restart the zulip container so it picks up the new secret:

    docker compose up -d zulip
    

See also