Selbst betreiben

Docker Compose

DDM auf einem Server einrichten, mit Docker Compose – vom Login in die Registry bis zum ersten Konto.

Auf einem einzelnen Server ist Docker Compose der einfachste Weg. Alles, was Sie dafür brauchen, steht auf dieser Seite: drei Dateien und Ihr Deploy-Token.

1. Bei der Registry anmelden

Melden Sie den Server mit dem Deploy-Token an, das Sie von uns bekommen haben:

docker login registry.gitlab.com -u <Name des Tokens> -p <Token>

2. Die Dateien ablegen

Legen Sie einen Ordner an, zum Beispiel /opt/ddm, und darin diese drei Dateien.

compose.yaml – die vier Dienste, ihre Volumes und Health-Checks. Sie bleibt, wie sie ist; was Sie einstellen, steht in .env.

compose.yaml
# DDM on one server, with Docker Compose — as the user guide describes it
# („Selbst betreiben" / „Self-hosting").
#
#     cp .env.example .env      # and fill it in
#     docker login registry.gitlab.com   # with the deploy token you were sent
#     docker compose up -d
#
# Four services: Postgres, the cms (the management app, and the API everything
# else talks to), the render worker (text, thumbnails and PDFs of what is
# uploaded) and the reader app. The cms and the reader app answer on two
# hostnames, behind a reverse proxy that terminates TLS: your own, pointed at
# the ports below, or Caddy from this file (`docker compose --profile proxy up
# -d`, see `Caddyfile`).
#
# Every setting of the user guide's „Konfiguration" / „Configuration" can go
# into `.env`; the ones set below are fixed by this layout. The images are those of
# the release in `DDM_VERSION`; the cms migrates the database when it starts.
name: ddm

x-app: &app
  restart: unless-stopped
  # Followed by Watchtower started with `--label-enable`, for an installation
  # that updates itself (`DDM_VERSION=stable`). Not Postgres: a new major of it
  # needs its data upgraded by hand.
  labels:
    com.centurylinklabs.watchtower.enable: "true"

services:
  postgres:
    # Postgres with the German dictionary DDM searches with — by its major,
    # which only ever changes by hand.
    image: ${DDM_REGISTRY:-registry.gitlab.com/iq-me/new-ddm-images}/postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_USER: ddm
      POSTGRES_DB: ddm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - postgres:/var/lib/postgresql
    healthcheck:
      test: [CMD, pg_isready, -U, ddm, -d, ddm]
      interval: 5s
      timeout: 5s
      retries: 10

  cms:
    <<: *app
    image: ${DDM_REGISTRY:-registry.gitlab.com/iq-me/new-ddm-images}/cms:${DDM_VERSION:-latest}
    env_file: .env
    environment:
      # Which version of these files this is: a release that needs newer ones
      # says so to the administrators, rather than failing (user guide, „Updates").
      DDM_DEPLOY_VERSION: "1"
      DATABASE_URL: postgres://ddm:${POSTGRES_PASSWORD}@postgres:5432/ddm
      BETTER_AUTH_URL: https://${DDM_CMS_HOST:?set DDM_CMS_HOST in .env}
      CLIENT_ORIGIN: https://${DDM_APP_HOST:?set DDM_APP_HOST in .env}
      # The documents, and the database's dumps taken before each migration
      # (the last three) — unless `S3_ENDPOINT` names a bucket for both.
      STORAGE_DIR: /data/storage
      BACKUP_DIR: /data/backups
      # Uploads are stored here and drawn by the render worker.
      RENDER_MODE: worker
      MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-1073741824}
    volumes:
      - data:/data
    ports:
      - ${DDM_CMS_PORT:-127.0.0.1:3000}:3000
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: [CMD, bun, -e, "process.exit((await fetch('http://127.0.0.1:3000/api/auth/ok')).ok ? 0 : 1)"]
      interval: 10s
      timeout: 5s
      retries: 6
      # Migrations first, on a new release.
      start_period: 5m
      start_interval: 2s

  render-worker:
    <<: *app
    image: ${DDM_REGISTRY:-registry.gitlab.com/iq-me/new-ddm-images}/render-worker:${DDM_VERSION:-latest}
    environment:
      DATABASE_URL: postgres://ddm:${POSTGRES_PASSWORD}@postgres:5432/ddm
      STORAGE_DRIVER: ${STORAGE_DRIVER:-}
      STORAGE_DIR: /data/storage
      S3_ENDPOINT: ${S3_ENDPOINT:-}
      S3_BUCKET: ${S3_BUCKET:-}
      S3_REGION: ${S3_REGION:-}
      S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
      S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
      S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-true}
      THUMBNAILS: ${THUMBNAILS:-on}
      RENDER_LARGE_OFFICE: ${RENDER_LARGE_OFFICE:-on}
    volumes:
      - data:/data
    # After the cms has migrated the database to this release.
    depends_on:
      cms:
        condition: service_healthy
    # It finishes the documents in hand before it stops — LibreOffice may take
    # minutes over a large deck.
    stop_grace_period: 11m

  client:
    <<: *app
    image: ${DDM_REGISTRY:-registry.gitlab.com/iq-me/new-ddm-images}/client:${DDM_VERSION:-latest}
    environment:
      NUXT_API_ORIGIN: http://cms:3000
      NUXT_PUBLIC_MGMT_URL: https://${DDM_CMS_HOST}
      # The reader app on this installation, for the mobile app's „Server".
      NUXT_PUBLIC_SERVER_URL: https://${DDM_APP_HOST}
      RENDER_MODE: worker
      THUMBNAILS: ${THUMBNAILS:-on}
      NUXT_PUBLIC_SSO_PROVIDERS: ${NUXT_PUBLIC_SSO_PROVIDERS:-}
    ports:
      - ${DDM_APP_PORT:-127.0.0.1:3001}:3000
    depends_on:
      cms:
        condition: service_healthy
    # Through to the cms: what the reader app forwards answers, too.
    healthcheck:
      test: [CMD, bun, -e, "process.exit((await fetch('http://127.0.0.1:3000/api/auth/ok')).ok ? 0 : 1)"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 1m
      start_interval: 2s

  # TLS and both hostnames, with certificates from Let's Encrypt: only with
  # `--profile proxy`, for a server with nothing else on ports 80 and 443.
  caddy:
    image: caddy:2.10-alpine
    profiles: [proxy]
    restart: unless-stopped
    environment:
      DDM_CMS_HOST: ${DDM_CMS_HOST}
      DDM_APP_HOST: ${DDM_APP_HOST}
    ports:
      - 80:80
      - 443:443
      - 443:443/udp
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy:/data
    depends_on: [cms, client]

volumes:
  postgres:
  data:
  caddy:

.env – Ihre Einstellungen. Was Sie mindestens ausfüllen, steht im nächsten Schritt.

.env
# An installation's settings, read by `compose.yaml` — copied to `.env` and
# filled in. Every setting of the user guide's „Konfiguration" /
# „Configuration" may be added; these are the ones an installation needs, and
# the ones most change.

# The release to run, e.g. `0.26.0`, or `0.26` for its fixes as they come.
# `stable` follows the releases we have promoted — for an installation that
# updates itself (user guide, „Updates").
DDM_VERSION=latest
# DDM_REGISTRY=registry.gitlab.com/iq-me/new-ddm-images

# The two hostnames, each with its own certificate: the management app (and
# API), and the reader app. DNS for both points at this server.
DDM_CMS_HOST=cms.example.com
DDM_APP_HOST=app.example.com
# Their parent, so a sign-in on one counts on the other.
AUTH_COOKIE_DOMAIN=example.com
# …and a passkey in both apps.
PASSKEY_RP_ID=example.com
# Where the two apps answer on this server, for your own reverse proxy —
# unused with `--profile proxy`.
# DDM_CMS_PORT=127.0.0.1:3000
# DDM_APP_PORT=127.0.0.1:3001

# Secrets: `openssl rand -base64 32` for each. The database's password is
# kept by Postgres on its first start; changing it here later locks the cms out.
POSTGRES_PASSWORD=
BETTER_AUTH_SECRET=

# The licence we sent you (`ddm1.…`) — needed from the first start: without
# one, the installation is locked, and nobody can register its first account.
# A licence entered later under Settings › Licence replaces it.
LICENCE_KEY=

# How mail leaves the installation — your own mail server.
MAIL_DRIVER=smtp
MAIL_FROM=ddm@example.com
SMTP_HOST=mail.example.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
# SMTP_ALLOW_SELF_SIGNED=true   # for an internal server with its own CA

# The documents are kept on the `data` volume. To keep them in an
# S3-compatible bucket instead (MinIO, Ceph, a cloud's), name it here; the
# database's dumps then go to the same bucket.
# S3_ENDPOINT=https://s3.example.com
# S3_BUCKET=ddm
# S3_REGION=eu-central-1
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=

# Thumbnails and previews of uploaded documents: `off` leaves them out.
# THUMBNAILS=on

Caddyfile – nur für den mitgelieferten Reverse Proxy (Schritt 4).

Caddyfile
# The reverse proxy of `compose.yaml`'s `proxy` profile: TLS for both
# hostnames, with certificates Caddy fetches itself — the server has to be
# reachable on ports 80 and 443 under both names. Caddy sets no limit on a
# request's size, so uploads up to `MAX_UPLOAD_BYTES` pass; another proxy needs
# it raised (nginx: `client_max_body_size 1g`).
{$DDM_CMS_HOST} {
    encode zstd gzip
    reverse_proxy cms:3000
}

{$DDM_APP_HOST} {
    encode zstd gzip
    reverse_proxy client:3000
}

3. Die Einstellungen ausfüllen

Tragen Sie in .env mindestens ein:

DDM_VERSION=0.26          # die Version; 0.26 bekommt ihre Fehlerbehebungen
DDM_CMS_HOST=cms.firma.de
DDM_APP_HOST=app.firma.de
AUTH_COOKIE_DOMAIN=firma.de
PASSKEY_RP_ID=firma.de

POSTGRES_PASSWORD=...     # openssl rand -base64 32
BETTER_AUTH_SECRET=...    # openssl rand -base64 32

LICENCE_KEY=ddm1....      # die Lizenz von uns

MAIL_DRIVER=smtp
MAIL_FROM=ddm@firma.de
SMTP_HOST=mail.firma.de
SMTP_PORT=587
SMTP_USER=...
SMTP_PASSWORD=...
  • LICENCE_KEY ist von Anfang an nötig. Ohne Lizenz startet die Installation gesperrt, und niemand kann das erste Konto anlegen.
  • POSTGRES_PASSWORD übernimmt Postgres beim ersten Start. Ändern Sie es danach nicht mehr in .env – das CMS käme sonst nicht mehr an die Datenbank.
  • AUTH_COOKIE_DOMAIN und PASSKEY_RP_ID sind die gemeinsame Domain beider Hostnamen. So gelten eine Anmeldung und ein Passkey im CMS auch in der Leser-App.

Alle weiteren Einstellungen stehen unter Konfiguration.

4. Den Reverse Proxy wählen

CMS und Leser-App antworten auf dem Server nur lokal, auf 127.0.0.1:3000 und 127.0.0.1:3001. Davor gehört ein Reverse Proxy, der TLS übernimmt:

  • Der mitgelieferte Caddy, wenn auf den Ports 80 und 443 sonst nichts läuft. Er holt die Zertifikate für beide Hostnamen selbst. Starten Sie dann mit --profile proxy.
  • Ihr eigener Proxy (nginx, Traefik, ein Load Balancer). Leiten Sie DDM_CMS_HOST auf Port 3000 und DDM_APP_HOST auf Port 3001. Erlauben Sie Anfragen bis 1 GB, sonst scheitern große Uploads – bei nginx mit client_max_body_size 1g;.

5. Starten

docker compose up -d                   # mit eigenem Proxy
docker compose --profile proxy up -d   # mit Caddy

Beim ersten Start legt das CMS die Datenbank an. Das dauert eine Minute; docker compose ps zeigt die Dienste danach als healthy.

6. Das erste Konto anlegen

Öffnen Sie https://cms.firma.de und registrieren Sie sich – mit Ihrem Namen, Ihrer Firma und Ihrer E-Mail-Adresse. Sie legen damit Ihre Organisation an und sind ihr Inhaber. Danach laden Sie Ihre Kollegen über Benutzer ein oder binden Ihr Verzeichnis unter Einstellungen › Verzeichnisdienste an.

Wie viele Organisationen Ihre Installation haben darf, steht in der Lizenz. Ist die Zahl erreicht, kann sich niemand mehr eine neue anlegen.

Die Daten

VolumeInhalt
ddm_postgresDie Datenbank.
ddm_dataDie Dokumente (storage/) und die Sicherungen der Datenbank (backups/).
ddm_caddyDie Zertifikate des mitgelieferten Proxys.

Sollen die Dokumente in einem S3-Bucket liegen, tragen Sie ihn unter S3_ENDPOINT, S3_BUCKET und den Schlüsseln ein. Die Sicherungen landen dann im selben Bucket.