OpenSlides ist für digitale Versammlungen, Abstimmungen und strukturierte Entscheidungsprozesse eine kraftvolle Plattform. Viele Organisationen nutzen externe Hosting-Angebote, doch für meine Arbeitsweise war klar: Ich will eine OpenSlides Server Installation auf dem eigenen Server, vollständig unter meiner Kontrolle, mit einer klaren und transparenten Dokumentation.
Im Folgenden beschreibe ich Schritt für Schritt, wie ich OpenSlides 4 auf meinem Home-Server cube eingerichtet habe. Der Server läuft mit Ubuntu, Docker und docker und ist über die Domain openslides.terruhn.it erreichbar. Der gesamte Stack ist sauber getrennt, wartbar und schnell aktualisierbar.
Was bedeuten Onlinewahlen für unsere Demokratie?
Bevor wir loslegen, möchte ich einen zentralen Aspekt klarstellen. Klassische Wahlen sind entweder anonym oder namentlich organisiert, und für beide Formen gibt es gute Gründe. Wenn wir als Volk unsere Regierung wählen, geschieht das anonym. Diese Anonymität schützt vor Einflussnahme durch Dritte, denn im Nachhinein kann niemand sicher sein, ob ein erzwungenes oder erkauftes Wahlverhalten tatsächlich umgesetzt wurde. Deshalb gehen wir einzeln hinter den Sichtschutz. Das schützt die Wählenden und ist eine wesentliche Grundlage für freie und legitime Wahlen.
Gleichzeitig existiert ein Papertrail, also die Möglichkeit, Stimmzettel bei Bedarf erneut auszuzählen, falls Unstimmigkeiten auftreten. Damit dabei keine Zuordnung zu einzelnen Personen möglich ist, prüfen die Wahlhelfer die Stimmzettel ausschließlich auf ihre Gültigkeit. Übrig bleiben nur gültige Stimmen, die keinen Rückschluss auf individuelle Wählerinnen oder Wähler zulassen.
Genau an diesem Punkt stoßen Online-Wahlsysteme an strukturelle Grenzen. Anonyme Online-Wahlen lassen sich nicht mit derselben Nachvollziehbarkeit und Prüfbarkeit umsetzen, da es keine physischen Stimmzettel gibt, die unabhängig nachgezählt werden könnten.
Online zuverlässig und transparent realisierbar sind daher vor allem namentliche Wahlen. Diese eignen sich gut für bestimmte Entscheidungsprozesse, sind jedoch nicht für alle Anwendungsfälle passend, etwa für eine Bundestagswahl. Für Werkzeuge wie OpenSlides bedeutet das, dass der Einsatzbereich klar definiert und bewusst gewählt sein sollte.
Bruce Schneier hat sich ausgiebig zu diesem Thema geäußert und ich folge ihm schon seit Jahrzehnten. Zum Thema Datensicherheit im weitesten Sinne ist er mein Maßstab.
Lessons Learned
Dass ich OpenSlides auf dem Cube nun ein drittes Mal neu aufbaue, hat einen guten Grund. Die erste Installation war im Kern ein freigelegter Host, mit dem Ziel, den Dienst überhaupt erst einmal von außen erreichbar zu machen. Im zweiten Schritt ging es um IPv6-Erreichbarkeit, DNS, DyDNS, FritzBox, SSL und die Einbindung hinter Caddy, also um einen sauberen technischen Zugang von außen.
Mit dieser dritten Runde verschiebt sich der Schwerpunkt noch einmal: Nun geht es nicht mehr vorrangig um Erreichbarkeit, sondern um Betriebsstabilität, Wartbarkeit und eine Architektur, die sich auch nach Monaten anderer Projekte noch klar lesen und verlässlich pflegen lässt.
Der neue Aufbau ist deshalb weniger ein weiterer Versuch als eine bewusste Reifung des Setups: weg von gewachsener Historie, hin zu einer nachvollziehbaren, reproduzierbaren und langfristig tragfähigen Betriebsform. Ziel dieser dritten Runde ist es, das bisher Gelernte zusammenzuführen und von Beginn an in die Architektur einfließen zu lassen.1
Zielbild und Architektur
Für den Neuaufbau auf dem Cube verfolge ich nun ein klar getrenntes Zielbild. Caddy bleibt der einzige öffentliche Einstiegspunkt und übernimmt die TLS-Terminierung sowie die Anbindung an meine bestehende Außenwelt aus FritzBox, DNS und DyDNS.
OpenSlides selbst läuft dahinter als frisch aufgebaute, intern angebundene Instanz im Docker-Stack und bringt keine eigene öffentliche Zertifikats- oder Erreichbarkeitslogik mehr mit. Die gesamte Payload liegt künftig unter /data/terruhn.it/openslides/ und damit bewusst außerhalb von /, mit getrennten Bereichen für Anwendung, persistente PostgreSQL-Daten, Backups und betriebliche Hilfsdateien.
Backups werden als Zusammenspiel aus persistenter Datenhaltung, zusätzlichen Datenbank-Dumps und dem bereits etablierten Restic-Setup gedacht, während Fehler- und Update-Meldungen über den vorhandenen Mailversand mit msmtp laufen. OpenSlides folgt dabei so nah wie möglich dem offiziellen Setup-Pfad, damit die Installation nachvollziehbar, reproduzierbar und langfristig wartbar bleibt.
So entsteht auf dem Cube keine gewachsene Sonderkonstruktion mehr, sondern eine Betriebsform, in der Zuständigkeiten, Datenpfade und Wartungswege von Anfang an klar voneinander getrennt sind.
Vorbereitung
Für den Neuaufbau habe ich zunächst nur die neue Zielstruktur unter /data/terruhn.it/openslides/ angelegt und das aktuelle2 openslides-Manage-Binary von github in das neue App-Verzeichnis geholt.
Damit steht jetzt die saubere Grundlage für die frische Installation bereit: getrennte Verzeichnisse für Anwendung, PostgreSQL-Daten, Backups, Logdateien und Runbook sowie das Werkzeug, mit dem die neue OpenSlides-Instanz offiziell erzeugt wird.
mkdir -p /data/terruhn.it/openslides/{app,postgres,backups,log,runbook,archive}
chown -R root:root /data/terruhn.it/openslides
chmod 755 /data/terruhn.it/openslides
find /data/terruhn.it/openslides -maxdepth 1 -type d | sort
cd /data/terruhn.it/openslides/app
wget https://github.com/OpenSlides/openslides-manage-service/releases/download/latest/openslides
chmod 755 openslides
ls -l openslides
Home Improvement
Zuerst wurde der neue Arbeitsbereich unter /data/terruhn.it/openslides/app vorbereitet.
mkdir -p /data/terruhn.it/openslides/{app,archive,backup,log,postgres,runbook}
mkdir -p /data/terruhn.it/openslides/app/secrets
chmod 700 /data/terruhn.it/openslides/app/secrets
Die Struktur unter /data/terruhn.it/openslides trennt den laufenden Anwendungsstand, die dauerhaften Nutzdaten und die projektbezogenen Betriebsdateien sauber voneinander.
/data/terruhn.it/openslides/
├── app/ -> laufende Compose-/Konfigurationsdateien, .env, secrets
│ ├── config.yml
│ ├── docker-compose.yml
│ ├── .env
│ └── secrets/
├── archive/ -> abgelegte Altstände, exportierte Zwischenstände oder ausrangierte Dateien
├── backup/ -> lokale Sicherungen rund um die OpenSlides-Installation
├── log/ -> projektbezogene Logdateien oder gesammelte Diagnoseausgaben
├── postgres/ -> persistente PostgreSQL-Daten von OpenSlides
└── runbook/ -> begleitende Betriebsdokumentation und Arbeitsnotizen
Das habe ich mir dabei gedacht:
/data/terruhn.it/openslides/
Das ist das gemeinsame Wurzelverzeichnis für die OpenSlides-Installation auf dem Cube.
Darin liegen typischerweise zwei verschiedene Arten von Dingen:
- der Arbeits- und Konfigurationsbereich
- die persistenten Daten
/data/terruhn.it/openslides/app
Das ist das Anwendungsverzeichnis.
Hier liegen die Dateien, mit denen du die Installation steuerst:
config.ymldocker-compose.yml.envsecrets/
Das ist also der Ort für alles, was den Stack beschreibt und startet.
Praktisch ist das dein Betriebsverzeichnis für:
cd /data/terruhn.it/openslides/app
docker ps
docker compose up -d
vi docker-compose.yml
/data/terruhn.it/openslides/app/secrets
Hier liegen die lokalen Secret-Dateien für den Stack, also zum Beispiel:
auth_token_keyauth_cookie_keymanage_auth_passwordinternal_auth_passwordpostgres_passwordsuperadmin
Diese Dateien gehören zum Betrieb, nicht zu den eigentlichen Anwendungsdaten.
Sie liegen unter app, weil sie direkt von der Compose-Definition referenziert werden.
/data/terruhn.it/openslides/postgres
Das ist der persistente Datenbereich der PostgreSQL-Datenbank.
Hier liegt der eigentliche Datenbestand von OpenSlides, also die Datenbankdateien selbst.
Wichtig ist die Trennung:
app/beschreibt den Stackpostgres/enthält die Nutzdaten
Das ist für Backup und Wiederherstellung hilfreich, weil du sofort siehst:
- Für die Rekonstruktion des Setups brauchst du
app/ - Für die eigentlichen OpenSlides-Daten brauchst du
postgres/
Warum diese Trennung sinnvoll ist
Sie macht drei Dinge klar:
1. Konfiguration und Daten sind nicht vermischt
Du musst nicht zwischen Compose-Dateien und Datenbankblöcken suchen.
2. Backups werden verständlicher
Du kannst bewusst unterscheiden zwischen:
- Betriebsdateien
- Nutzdaten
3. Neuaufbau wird leichter
Wenn du den Stack neu erzeugen willst, bleibt das Datenverzeichnis davon getrennt.
OpenSlides neu aufbauen
Da Docker Compose seine Container- und Netzwerknamen standardmäßig aus dem Verzeichnis der Compose-Datei ableitet, würde ein Start aus /data/terruhn.it/openslides/app ohne weitere Vorgaben zu generischen Namen mit dem Präfix app_ führen. Um von Anfang an sprechende Objektnamen zu erhalten, wurde deshalb in der .env der Projektname mit COMPOSE_PROJECT_NAME=openslides fest vorgegeben. Dadurch entstehen nachvollziehbare Namen wie openslides_proxy_1, openslides_postgres_1 oder openslides_uplink.
cd /data/terruhn.it/openslides/app
cat .env
COMPOSE_PROJECT_NAME=openslides
OPENSLIDES_EMAIL_HOST_PASSWORD=SMTP_MAIL_PASSWORD_HERE
Mit dem OpenSlides-Binary erzeuge ich die die Standardkonfiguration in config.yml.
cd /data/terruhn.it/openslides/app
./openslides config-create-default .
Anschließend habe ich die config.yml an das Zielbild des Cube angepasst:
- fester Release-Stand statt
latest - kein lokales HTTPS innerhalb von OpenSlides
- kein eigenes ACME
- Mailversand über SMTP
- Bindung des OpenSlides-Proxys nur an
127.0.0.1:8000
Wesentlich war dabei unter anderem dieser Rahmen in der config.yml:
host: 127.0.0.1
port: 8000
enableLocalHTTPS: false
enableAutoHTTPS: false
defaults:
containerRegistry: ghcr.io/openslides/openslides
tag: 4.2.29
Auf dieser Basis erzeugt OpenSlides seine eigene docker-compose.yml:
./openslides config --config config.yml .
Im nächsten Schritt habe ich die erzeugte docker-compose.yml weiter an meine Vorgaben angepasst:
Erstens der PostgreSQL-Datenpfad.
Statt des vom Generator vorgesehenen benannten Volumes
volumes:
- postgres-data:/var/lib/postgresql/data
haben wir den Bind-Mount auf dein Datenfilesystem gesetzt:
volumes:
- /data/terruhn.it/openslides/postgres:/var/lib/postgresql/data
Zweitens die Netzwerknamen.
Die vom Generator nur funktional beschriebenen Netze haben wir auf sprechende Namen festgezogen:
email:
name: openslides_email
internal: falsefrontend:
name: openslides_frontend
internal: truedata:
name: openslides_data
internal: true
Drittens das Uplink-Netz als externes Koppelnetz zu Caddy.
Aus dem intern vom Stack verwalteten Netz
uplink:
internal: false
wurde:
uplink:
external: true
name: openslides_uplink
Viertens das Entfernen des nicht mehr benötigten benannten Volumes unten im Compose-File.
Also dieser Block wurde gelöscht:
volumes:
postgres-data:
ACHTUNG: Alle Änderungen an der docker-compose.yml gehen nach einem erneuten Aufruf von openslides config --config config.yml verloren und müssen nachgezogen werden!
Hier ist ein Patch, ohne Gewähr:-)
cd /data/terruhn.it/openslides/app
python3 - <<'PY'
from pathlib import Path
p = Path("docker-compose.yml")
s = p.read_text()
s = s.replace(
" - postgres-data:/var/lib/postgresql/data",
" - /data/terruhn.it/openslides/postgres:/var/lib/postgresql/data"
)
s = s.replace(
" uplink:\n internal: false",
" uplink:\n external: true\n name: openslides_uplink"
)
s = s.replace(
" email:\n internal: false",
" email:\n name: openslides_email\n internal: false"
)
s = s.replace(
" frontend:\n internal: true",
" frontend:\n name: openslides_frontend\n internal: true"
)
s = s.replace(
" data:\n internal: true",
" data:\n name: openslides_data\n internal: true"
)
s = s.replace(
"\nvolumes:\n postgres-data:\n",
"\n"
)
lines = s.splitlines()
out = []
in_services = False
current_service = None
service_block = []
def flush_service(block):
if not block:
return []
has_restart = any(
line.startswith(" restart:")
for line in block[1:]
)
if not has_restart:
block.insert(1, " restart: unless-stopped")
return block
for line in lines:
if line == "services:":
in_services = True
if current_service is not None:
out.extend(flush_service(service_block))
current_service = None
service_block = []
out.append(line)
continue
if in_services and line.startswith(" ") and not line.startswith(" ") and line.endswith(":"):
if current_service is not None:
out.extend(flush_service(service_block))
current_service = line
service_block = [line]
continue
if current_service is not None:
if line and not line.startswith(" "):
out.extend(flush_service(service_block))
current_service = None
service_block = []
in_services = False
out.append(line)
else:
service_block.append(line)
else:
out.append(line)
if current_service is not None:
out.extend(flush_service(service_block))
s = "\n".join(out) + "\n"
p.write_text(s)
PY
Mailing
Ein eigener Punkt war das Mail-Handling per SNMP. Die dafür nötigen Parameter liegen in der OpenSlides-Konfiguration des backendAction-Dienstes: Absenderadresse, Server, Benutzername, Port und das zugehörige Passwort aus der .env. Dadurch bleibt der Versandweg konsistent mit dem übrigen System, ohne dass für OpenSlides ein eigener Mailunterbau aufgebaut werden muss.
services:
backendAction:
environment:
DEFAULT_FROM_EMAIL: openslides@terruhn.it
EMAIL_HOST: DEIN_MAILSERVERNAME_HIER
EMAIL_HOST_PASSWORD: ${OPENSLIDES_EMAIL_HOST_PASSWORD}
EMAIL_HOST_USER: DEIN_MAILUSERNAME_HIER
EMAIL_PORT: 465
EMAIL_CONNECTION_SECURITY: SSL/TLS
Abschluss
Danach wurden die benötigten Secrets erzeugt und im Arbeitsverzeichnis abgelegt. Zusammen mit der .env für den Mailversand ergibt sich daraus eine Installation, deren sensible Werte lokal und nachvollziehbar an einem Ort liegen.
umask 077
printf %s "$(openssl rand -base64 48 | tr -d '\n')" > secrets/auth_token_key
printf %s "$(openssl rand -base64 48 | tr -d '\n')" > secrets/auth_cookie_key
printf %s "$(openssl rand -base64 32 | tr -d '\n')" > secrets/manage_auth_password
printf %s "$(openssl rand -base64 32 | tr -d '\n')" > secrets/internal_auth_password
printf %s "$(openssl rand -base64 32 | tr -d '\n')" > secrets/postgres_password
printf %s "$(openssl rand -base64 24 | tr -d '\n')" > secrets/superadmin
Anschließend wurde der Stack gestartet und die internen Dienste initialisiert. Der Zugriff von außen erfolgte an diesem Punkt noch nicht direkt, sondern zunächst über eine Wartungsantwort im Caddy, damit die Domain während des Aufbaus aussagefähig reagiert.
docker compose up -d
docker compose ps
Erst als der OpenSlides-Stack intern vollständig lief, wurde der Caddy-Eintrag für openslides.terruhn.it auf den neuen lokalen Proxy umgestellt. Der öffentliche Zugriff endet damit am Cube, während OpenSlides selbst intern bleibt.
openslides.terruhn.it {
reverse_proxy openslides_proxy_1:8000
}
Damit steht nun ein frischer OpenSlides-Stack auf dem Cube, mit fester Version, Datenhaltung unter /data, sauberem Reverse Proxy vor dem Dienst, eingebundenem Mailversand über den vorhandenen Systemweg, sprechenden Objekt-Namen und einer Architektur, die sich deutlich klarer pflegen lässt als die vorherigen gewachsenen Zwischenstände.
Wartung und Pflege
(kommt im April 2026)3
Backup und Restore
(kommt im April 2026)4
Performance-Prognose und mögliche Engpässe
Die Leistungsfähigkeit der Installation ergibt sich aus mehreren Faktoren. OpenSlides erzeugt auf dem Cube selbst nur moderate Last, da die Dienste überwiegend leichtgewichtige JSON-Daten austauschen. Die Berechnung erfolgt größtenteils im Browser der Teilnehmenden, während der Server vor allem kurze strukturierte Anfragen beantwortet. PostgreSQL verarbeitet gut indexierte Datensätze, und Redis liefert viele Antworten direkt aus dem Speicher. Dadurch bleibt der Ressourcenbedarf im Cube begrenzt.
Auf dieser Grundlage ergibt sich eine vorsichtige Einschätzung:
- bis ca. 200 Teilnehmende
Der Betrieb läuft voraussichtlich stabil. Die JSON-Updates werden zuverlässig verteilt und die Container behalten Reserven für typische Vorgänge wie Abstimmungen und Anträge. - bis ca. 400 Teilnehmende
Die Installation bleibt wahrscheinlich nutzbar, reagiert allerdings empfindlicher auf gleichzeitige Aktionen großer Gruppen. Für diesen Bereich empfiehlt sich später ein gezielter Lasttest.
Viele potenzielle Engpässe entstehen nicht im Server, sondern in der Umgebung der Teilnehmenden:
- Endgeräte
Browser-basierte Anwendungen verteilen einen großen Teil der Last auf die Geräte der Nutzenden. Schwächere oder ausgelastete Hardware kann die Darstellung der JSON-Daten verzögern. - WLAN-Infrastruktur vor Ort
In Räumen mit vielen verbundenen Geräten begrenzt das lokale WLAN die Geschwindigkeit stärker als der Server. Live-Updates können dadurch langsamer ankommen. - Upload-Bandbreite des Anschlusses
Die parallele Übertragung an alle Teilnehmenden nutzt dieselbe Upload-Strecke. JSON-Pakete sind klein, doch die Summe aller Updates bleibt ein relevanter Faktor. - Spitzenlasten
Gleichzeitiges Öffnen großer Tagesordnungspunkte oder das Starten einer Abstimmung erzeugen kurzfristig höhere Last, vor allem im Presenter- und Datenbankdienst.
Rechtliche Einordnung für den Betrieb meiner OpenSlides-4-Instanz
Für OpenSlides existiert ein rechtsverbindliches Gutachten der GOB Legal Rechtsanwaltsgesellschaft mbH vom 25.03.2021. Dieses Gutachten bestätigt, dass OpenSlides 3 – unter definierten technischen und organisatorischen Voraussetzungen – für geheime elektronische Abstimmungen und Wahlen geeignet ist.
Mit einem Schreiben vom 3. Juni 2024 stellt die Intevation GmbH klar, dass dieses Gutachten unverändert auch für OpenSlides 4 gilt.
Damit gelten alle im Gutachten beschriebenen technischen Anforderungen, Sicherheitsmechanismen und Wahlverfahren ebenso für die Version 4, die ich auf meinem Server betreibe.
Gleichzeitig macht das Gutachten sehr deutlich, unter welchen übergeordneten Bedingungen der rechtskonforme Betrieb steht. Diese Punkte betreffen nicht die Software selbst, sondern die organisatorische und infrastrukturelle Umgebung. Für meine eigene Installation auf dem Cube ergibt sich daraus folgende Einschätzung.
Was meine OpenSlides-4-Instanz technisch erfüllt
Die folgenden Anforderungen erfüllt OpenSlides 4 bereits durch seine Architektur und durch meine konkrete Installation:
- TLS-gesicherte Kommunikation über Auto-HTTPS
- Trennung von Stimmberechtigung und Stimminhalt
- Anonymisierte Speicherung nicht-namentlicher Stimmen
- Verhinderung mehrfacher Stimmabgaben durch Transaktionen
- Tokenbasierte Stimmabgabe ohne Rückschluss auf die Person
- Keine Übermittlung einer „Quittung“ an Teilnehmende
- JSON-basierter Datenaustausch ohne Speicherung sensibler Metadaten im System
Diese Punkte entsprechen den Anforderungen des Gutachtens an das technische System und decken sich vollständig mit der Funktionsweise von OpenSlides 4.
Was meine Instanz nicht erfüllt und warum das rechtlich relevant ist
Das Gutachten basiert auf Hosting durch die Intevation GmbH, also durch einen externen, vertraglich gebundenen Dienstleister in zertifizierten Rechenzentren (ISO/IEC 27001).
Meine Installation weicht davon in mehreren Punkten ab:
1. Kein externer Betreiber
Ich betreibe die Instanz selbst, nicht als externer Dienstleister.
→ Das Gutachten setzt voraus, dass der Betreiber nicht Teil der abstimmenden Organisation ist und über eine qualifizierte Verschwiegenheitsklausel gebunden ist.
Diese Voraussetzung erfülle ich naturgemäß nicht.
2. Kein professionelles Rechenzentrum
Der Cube steht in einer privaten Infrastruktur, nicht in einem zertifizierten RZ.
→ Es fehlen die im Gutachten angenommenen Rahmenbedingungen wie
- Zutrittskontrolle
- Redundanz und Backup-RZ
- Notfallplanung
- dokumentierte IT-Prozesse
3. Keine formale Verpflichtung Dritter
Im Gutachten werden Administrator*innen arbeits- bzw. dienstvertraglich zur Verschwiegenheit verpflichtet.
→ Bei einem privaten Heimserver entfällt diese organisatorische Absicherung.
4. Satzungsrechtliche Grundlage
Ob eine Organisation geheime elektronische Wahlen zulässt, bestimmt ihre Satzung. Das gilt unabhängig von der verwendeten Technik.
→ Dies muss im Einzelfall geprüft werden, da es außerhalb des technischen Setups liegt.
Fazit für den Einsatz meiner Instanz
OpenSlides 4 erfüllt technisch alle Voraussetzungen, die das Gutachten beschreibt.
Meine Installation erfüllt jedoch nicht alle organisatorischen Randbedingungen, die das Gutachten als Voraussetzung für rechtsverbindliche geheime Wahlen nennt.
Das bedeutet:
- Für interne Arbeitsprozesse, informelle Abstimmungen und nicht-strittige Entscheidungen ist die Instanz gut geeignet.
- Für offiziell geheime Wahlen mit Anfechtungsrisiko wäre zusätzlich erforderlich:
- Hosting in einer professionellen Infrastruktur
- Betrieb durch einen externen, zur Verschwiegenheit verpflichteten Dienstleister
- dokumentierte Sicherheitsprozesse
- klare, satzungsgemäße Erlaubnis der Organisation
Damit ist transparent nachvollziehbar, wo OpenSlides 4 rechtlich abgesichert ist und wo eine private Installation – auch bei technisch korrektem Betrieb – nicht die vom Gutachten vorausgesetzten Rahmenbedingungen erfüllt.
- Dabei kann nun auch meine langjährige Erfahrung aus dem sicheren Betrieb kritischer Infrastruktur mit einfließen: Über 23 Jahre hinweg war ich für wichtige Infrastrukturkomponenten eines großen deutschen Unternehmens teil-verantwortlich. Genau dieser Blick auf Stabilität, Nachvollziehbarkeit und verlässlichen Betrieb prägt nun auch den Neuaufbau dieser OpenSlides-Installation. ↩︎
- 4.2.29 ↩︎
- jetzt erstmal Osterferien:-) ↩︎
- siehe oben ↩︎
Zuletzt aktualisiert am 4. April 2026.
