OpenSlides portable – lokal, mobil, einsatzbereit

OpenSlides

Ich wollte OpenSlides einmal nicht auf einem Server im Netz, sondern auf einem Ubuntu-Laptop für den lokalen Einsatz unterwegs aufsetzen. Ohne Caddy, ohne öffentliche Domain, ohne Let’s Encrypt. Einfach so, dass der Rechner im jeweiligen Veranstaltungsnetz eine Adresse per DHCP bekommt und OpenSlides direkt lokal läuft – mit selbstsigniertem Zertifikat und Docker.

Für diesen Aufbau habe ich App und Laufzeitdaten nach /usr/terruhn.it/ gelegt, Konfiguration und Zugangsdaten nach /etc/terruhn.it/. Das hält die Struktur klar und passt gut zu meinem allgemeinen Layout auf Linux-Systemen.

OpenSlides selbst läuft als Docker-Compose-Stack. Weil die docker-compose.yml von OpenSlides neu erzeugt werden kann, wollte ich dort keine dauerhaften Handanpassungen pflegen. Für den automatischen Start nach dem Boot übernimmt deshalb ein kleiner systemd-Wrapper die Arbeit. So bleibt die generierte Compose-Datei unberührt und der Stack kommt trotzdem zuverlässig wieder hoch.

Für den mobilen Betrieb ist das eine angenehme Lösung: Laptop aufklappen, ins lokale Netz hängen, OpenSlides starten, fertig. Im Browser gibt es wegen des selbstsignierten Zertifikats zunächst eine Warnung, danach steht eine lokale, transportable Installation bereit, die sich gut für kleine Veranstaltungen, Tests oder Einsätze ohne feste Server-Infrastruktur eignet.

Ich mag an dieser Variante ihre Schlichtheit. Kein externer DNS, kein Reverse Proxy, keine Abhängigkeit von einer festen öffentlichen Umgebung. Nur ein Rechner, Docker und ein klarer Zweck: OpenSlides dort verfügbar machen, wo es gerade gebraucht wird.

Ich hab die gesamte Installation in ein Shell-Script verpackt. Nach dem Download ändere die Endung in .sh und mach die Datei ausführbar. Und na klar hab ich das Script mit Hilfe von KI erstellt.

#!/bin/sh
set -eu

###############################################################################
# OpenSlides "portable" auf Ubuntu 24.04 LTS x64
# - Binary und App unter /usr/<PATH_PREFIX>
# - Konfiguration und Secrets unter /etc/<PATH_PREFIX>
# - lokales HTTPS mit selbstsigniertem Zertifikat
# - Mail-Konfiguration per Abfrage
# - systemd-Wrapper für automatischen Start
# - "latest" wird auf die höchste echte Release-Version aufgelöst
#   und vor der Installation bestätigt
#
# Terruhn.IT Manufaktur, frei zur Nutzung, auf eigenes Risiko.
# No Backup, no mercy!
###############################################################################

###############################################################################
# 1) Hilfsfunktionen
###############################################################################

log() {
    printf '\n[%s] %s\n' "$(date '+%F %T')" "$*"
}

need_cmd() {
    command -v "$1" >/dev/null 2>&1 || {
        echo "Fehlt: $1" >&2
        exit 1
    }
}

ask() {
    prompt="$1"
    default="${2:-}"
    if [ -n "$default" ]; then
        printf "%s [%s]: " "$prompt" "$default" >&2
    else
        printf "%s: " "$prompt" >&2
    fi
    IFS= read -r value
    if [ -z "$value" ]; then
        value="$default"
    fi
    printf "%s" "$value"
}

ask_required() {
    while :; do
        value="$(ask "$1" "${2:-}")"
        if [ -n "$value" ]; then
            printf "%s" "$value"
            return 0
        fi
        echo "Dieser Wert wird benötigt." >&2
    done
}

ask_secret() {
    prompt="$1"
    while :; do
        printf "%s: " "$prompt" >&2
        stty -echo
        IFS= read -r value
        stty echo
        printf "\n" >&2
        if [ -n "$value" ]; then
            printf "%s" "$value"
            return 0
        fi
        echo "Dieser Wert wird benötigt." >&2
    done
}

ask_yes_no() {
    default="${2:-true}"
    while :; do
        reply="$(ask "$1 (true/false)" "$default")"
        case "$reply" in
            true|false)
                printf "%s" "$reply"
                return 0
                ;;
        esac
        echo "Bitte true oder false eingeben." >&2
    done
}

confirm_yes() {
    prompt="${1:-Möchtest du fortfahren? [j/N]}"
    printf "%s " "$prompt" >&2
    IFS= read -r answer
    case "$answer" in
        j|J|ja|JA|Ja|y|Y|yes|YES|Yes)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

get_latest_semver_tag() {
    curl -fsSL "https://api.github.com/repos/OpenSlides/openslides-manage-service/tags?per_page=100" \
    | jq -r '.[].name' \
    | awk '/^[0-9]+\.[0-9]+\.[0-9]+$/ { print }' \
    | sort -V \
    | tail -n 1
}

compose_file_exists() {
    [ -f "${LIB_DIR}/compose.yaml" ] \
    || [ -f "${LIB_DIR}/compose.yml" ] \
    || [ -f "${LIB_DIR}/docker-compose.yml" ] \
    || [ -f "${LIB_DIR}/docker-compose.yaml" ]
}

###############################################################################
# 2) Vorab: Minimalpakete und jq/curl bereitstellen
###############################################################################

log "Minimalpakete bereitstellen"
apt update
apt install -y ca-certificates curl gnupg lsb-release sed jq coreutils

need_cmd curl
need_cmd jq
need_cmd sed
need_cmd awk
need_cmd sort

###############################################################################
# 3) Interaktive Abfrage
###############################################################################

echo
echo "OpenSlides portable – interaktive Einrichtung"
echo

PATH_PREFIX="$(ask_required "Pfad-Präfix unter /usr und /etc" "deine-firma")"
OS_VERSION_REQUESTED="$(ask_required "OpenSlides-Version oder latest" "latest")"
INSTANCE_NAME="$(ask_required "Instanzname" "laptop-local")"

OPENSLIDES_HOST="$(ask_required "Bind-Adresse" "0.0.0.0")"
OPENSLIDES_PORT="$(ask_required "Port" "8000")"

COMPOSE_PROJECT_NAME="$(ask_required "COMPOSE_PROJECT_NAME" "openslides")"

ENABLE_LOCAL_HTTPS="$(ask_yes_no "Lokales selbstsigniertes HTTPS aktivieren" "true")"
ENABLE_AUTO_HTTPS="$(ask_yes_no "Automatisches HTTPS / ACME aktivieren" "false")"

USE_EMAIL="$(ask_yes_no "E-Mail-Versand konfigurieren" "true")"

EMAIL_HOST=""
EMAIL_PORT=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_CONNECTION_SECURITY=""
EMAIL_TIMEOUT=""
EMAIL_ACCEPT_SELF_SIGNED_CERTIFICATE=""
DEFAULT_FROM_EMAIL=""

if [ "$USE_EMAIL" = "true" ]; then
    EMAIL_HOST="$(ask_required "SMTP-Host" "dein-mailserver.de")"
    EMAIL_PORT="$(ask_required "SMTP-Port" "587")"
    EMAIL_HOST_USER="$(ask_required "SMTP-Benutzer" "")"
    EMAIL_HOST_PASSWORD="$(ask_secret "SMTP-Passwort")"
    EMAIL_CONNECTION_SECURITY="$(ask_required "SMTP-Sicherheit (STARTTLS|SSL/TLS|NONE)" "STARTTLS")"
    EMAIL_TIMEOUT="$(ask_required "SMTP-Timeout in Sekunden" "5")"
    EMAIL_ACCEPT_SELF_SIGNED_CERTIFICATE="$(ask_yes_no "Selbstsignierte SMTP-Zertifikate akzeptieren" "false")"
    DEFAULT_FROM_EMAIL="$(ask_required "Absenderadresse" "")"
fi

###############################################################################
# 4) latest auflösen
###############################################################################

OS_VERSION=""

if [ "${OS_VERSION_REQUESTED}" = "latest" ]; then
    log "Aktuelle OpenSlides-Version ermitteln"
    OS_VERSION="$(get_latest_semver_tag)"

    if [ -z "${OS_VERSION}" ]; then
        echo "Konnte die aktuelle OpenSlides-Version nicht ermitteln." >&2
        exit 1
    fi

    echo
    echo "latest ist aktuell: ${OS_VERSION}"
    if ! confirm_yes "Möchtest du diese Version verwenden? [j/N]"; then
        echo "Abbruch."
        exit 0
    fi
else
    OS_VERSION="${OS_VERSION_REQUESTED}"
fi

###############################################################################
# 5) Abgeleitete Pfade
###############################################################################

BASE_USR="/usr/${PATH_PREFIX}"
BASE_ETC="/etc/${PATH_PREFIX}"

BIN_DIR="${BASE_USR}/bin"
SBIN_DIR="${BASE_USR}/sbin"
LIB_DIR="${BASE_USR}/lib/openslides/${INSTANCE_NAME}"

CONFIG_DIR="${BASE_ETC}/config"
CRED_DIR="${BASE_ETC}/credentials"

OPENSLIDES_BIN="${BIN_DIR}/openslides"
CONFIG_FILE="${CONFIG_DIR}/openslides-${INSTANCE_NAME}.yml"
ENV_FILE="${CRED_DIR}/openslides-${INSTANCE_NAME}.env"
ENV_LINK="${LIB_DIR}/.env"

WRAPPER_SCRIPT="${SBIN_DIR}/openslides-${INSTANCE_NAME}.sh"
SYSTEMD_UNIT="/etc/systemd/system/openslides-${INSTANCE_NAME}.service"

###############################################################################
# 6) Zusammenfassung vor Ausführung
###############################################################################

echo
echo "Zusammenfassung"
echo "  Pfad-Präfix:          ${PATH_PREFIX}"
echo "  Angefragt:            ${OS_VERSION_REQUESTED}"
echo "  Verwendet:            ${OS_VERSION}"
echo "  Instanzname:          ${INSTANCE_NAME}"
echo "  Host:                 ${OPENSLIDES_HOST}"
echo "  Port:                 ${OPENSLIDES_PORT}"
echo "  Compose-Projektname:  ${COMPOSE_PROJECT_NAME}"
echo "  Local HTTPS:          ${ENABLE_LOCAL_HTTPS}"
echo "  Auto HTTPS:           ${ENABLE_AUTO_HTTPS}"
echo "  E-Mail:               ${USE_EMAIL}"
echo
echo "  Binary:               ${OPENSLIDES_BIN}"
echo "  Instanz:              ${LIB_DIR}"
echo "  Config:               ${CONFIG_FILE}"
echo "  Credentials:          ${ENV_FILE}"
echo

CONFIRM="$(ask_required "Fortfahren mit diesen Werten? (ja/nein)" "ja")"
case "$CONFIRM" in
    ja|j|yes|y) ;;
    *)
        echo "Abbruch."
        exit 0
        ;;
esac

###############################################################################
# 7) Verzeichnisse vorbereiten
###############################################################################

log "Verzeichnisse anlegen"
mkdir -p "${BIN_DIR}"
mkdir -p "${SBIN_DIR}"
mkdir -p "${LIB_DIR}"
mkdir -p "${CONFIG_DIR}"
mkdir -p "${CRED_DIR}"
chmod 700 "${CRED_DIR}"

###############################################################################
# 8) Docker aus dem offiziellen Repo bereitstellen
###############################################################################

log "Docker-Repository einrichten"
install -m 0755 -d /etc/apt/keyrings

if [ ! -f /etc/apt/keyrings/docker.asc ]; then
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    chmod a+r /etc/apt/keyrings/docker.asc
fi

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
    > /etc/apt/sources.list.d/docker.list

log "Docker-Pakete installieren"
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

log "Docker-Dienst aktivieren"
systemctl enable --now docker

need_cmd docker
docker compose version >/dev/null 2>&1 || {
    echo "docker compose ist nicht verfügbar." >&2
    exit 1
}

###############################################################################
# 9) OpenSlides-Binary laden
###############################################################################

log "OpenSlides-Binary herunterladen"
curl -fL \
    "https://github.com/OpenSlides/openslides-manage-service/releases/download/${OS_VERSION}/openslides" \
    -o "${OPENSLIDES_BIN}"

chmod 0755 "${OPENSLIDES_BIN}"
ln -sf "${OPENSLIDES_BIN}" /usr/local/bin/openslides

need_cmd openslides

###############################################################################
# 10) Config-Datei schreiben
###############################################################################

log "OpenSlides-Konfiguration schreiben: ${CONFIG_FILE}"

if [ "$USE_EMAIL" = "true" ]; then
    cat > "${CONFIG_FILE}" <<EOF
---
host: ${OPENSLIDES_HOST}
port: ${OPENSLIDES_PORT}

enableLocalHTTPS: ${ENABLE_LOCAL_HTTPS}
enableAutoHTTPS: ${ENABLE_AUTO_HTTPS}

defaults:
  tag: ${OS_VERSION}

services:
  backendAction:
    environment:
      EMAIL_HOST: "\${EMAIL_HOST}"
      EMAIL_PORT: "\${EMAIL_PORT}"
      EMAIL_HOST_USER: "\${EMAIL_HOST_USER}"
      EMAIL_HOST_PASSWORD: "\${EMAIL_HOST_PASSWORD}"
      EMAIL_CONNECTION_SECURITY: "\${EMAIL_CONNECTION_SECURITY}"
      EMAIL_TIMEOUT: "\${EMAIL_TIMEOUT}"
      EMAIL_ACCEPT_SELF_SIGNED_CERTIFICATE: "\${EMAIL_ACCEPT_SELF_SIGNED_CERTIFICATE}"
      DEFAULT_FROM_EMAIL: "\${DEFAULT_FROM_EMAIL}"
EOF
else
    cat > "${CONFIG_FILE}" <<EOF
---
host: ${OPENSLIDES_HOST}
port: ${OPENSLIDES_PORT}

enableLocalHTTPS: ${ENABLE_LOCAL_HTTPS}
enableAutoHTTPS: ${ENABLE_AUTO_HTTPS}

defaults:
  tag: ${OS_VERSION}
EOF
fi

chmod 644 "${CONFIG_FILE}"

###############################################################################
# 11) .env / Credentials schreiben
###############################################################################

log "Credentials-Datei schreiben: ${ENV_FILE}"

if [ "$USE_EMAIL" = "true" ]; then
    cat > "${ENV_FILE}" <<EOF
COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
EMAIL_HOST=${EMAIL_HOST}
EMAIL_PORT=${EMAIL_PORT}
EMAIL_HOST_USER=${EMAIL_HOST_USER}
EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD}
EMAIL_CONNECTION_SECURITY=${EMAIL_CONNECTION_SECURITY}
EMAIL_TIMEOUT=${EMAIL_TIMEOUT}
EMAIL_ACCEPT_SELF_SIGNED_CERTIFICATE=${EMAIL_ACCEPT_SELF_SIGNED_CERTIFICATE}
DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL}
EOF
else
    cat > "${ENV_FILE}" <<EOF
COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
EOF
fi

chmod 600 "${ENV_FILE}"

log ".env im Instanzverzeichnis verlinken"
ln -sf "${ENV_FILE}" "${ENV_LINK}"

###############################################################################
# 12) Default-Config als Referenz erzeugen
###############################################################################

if [ ! -f "${CONFIG_DIR}/default-config.yml" ]; then
    log "Default-Konfiguration als Referenz erzeugen"
    (
        cd "${CONFIG_DIR}"
        openslides config-create-default .
    ) || true
fi

###############################################################################
# 13) Instanz erzeugen oder Compose-Datei neu bauen
###############################################################################

if compose_file_exists; then
    log "Bestehende Instanz erkannt, Compose-Datei wird neu erzeugt"
    (
        cd "${LIB_DIR}"
        openslides config --config "${CONFIG_FILE}" .
    )
else
    log "Neue Instanz wird erzeugt"
    (
        cd "${LIB_DIR}"
        openslides setup --config "${CONFIG_FILE}" .
    )
fi

###############################################################################
# 14) Wrapper-Skript für systemd
###############################################################################

log "Wrapper-Skript schreiben: ${WRAPPER_SCRIPT}"
cat > "${WRAPPER_SCRIPT}" <<EOF
#!/bin/sh
set -eu

INSTANCE_DIR="${LIB_DIR}"

case "\${1:-}" in
  start)
    cd "\$INSTANCE_DIR"
    docker compose up -d
    ;;
  stop)
    cd "\$INSTANCE_DIR"
    docker compose stop
    ;;
  down)
    cd "\$INSTANCE_DIR"
    docker compose down
    ;;
  restart)
    cd "\$INSTANCE_DIR"
    docker compose up -d
    ;;
  ps)
    cd "\$INSTANCE_DIR"
    docker compose ps
    ;;
  logs)
    cd "\$INSTANCE_DIR"
    docker compose logs --tail=200
    ;;
  *)
    echo "Usage: \$0 {start|stop|down|restart|ps|logs}" >&2
    exit 1
    ;;
esac
EOF

chmod 0755 "${WRAPPER_SCRIPT}"

###############################################################################
# 15) systemd-Unit schreiben
###############################################################################

log "systemd-Unit schreiben: ${SYSTEMD_UNIT}"
cat > "${SYSTEMD_UNIT}" <<EOF
[Unit]
Description=OpenSlides ${INSTANCE_NAME} stack
Wants=docker.service network-online.target
After=docker.service network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=${LIB_DIR}
ExecStart=${WRAPPER_SCRIPT} start
ExecStop=${WRAPPER_SCRIPT} stop
TimeoutStartSec=0
TimeoutStopSec=120

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable "openslides-${INSTANCE_NAME}.service"

###############################################################################
# 16) Images ziehen und Stack starten
###############################################################################

log "Docker-Images holen"
(
    cd "${LIB_DIR}"
    docker compose pull
)

log "OpenSlides-Stack starten"
systemctl restart "openslides-${INSTANCE_NAME}.service"

###############################################################################
# 17) Server prüfen und Initialdaten anlegen
###############################################################################

log "OpenSlides-Server prüfen"
(
    cd "${LIB_DIR}"
    openslides check-server -a "localhost:${OPENSLIDES_PORT}"
)

log "Initialdaten anlegen, falls noch keine vorhanden sind"
(
    cd "${LIB_DIR}"
    openslides initial-data -a "localhost:${OPENSLIDES_PORT}"
) || true

###############################################################################
# 18) Abschluss
###############################################################################

log "Status"
systemctl status "openslides-${INSTANCE_NAME}.service" --no-pager || true

echo
echo "Fertig."
echo
echo "Angefragt:"
echo "  ${OS_VERSION_REQUESTED}"
echo
echo "Verwendete, fest eingetragene OpenSlides-Version:"
echo "  ${OS_VERSION}"
echo
echo "Wichtige Pfade:"
echo "  Binary:      ${OPENSLIDES_BIN}"
echo "  Instanz:     ${LIB_DIR}"
echo "  Config:      ${CONFIG_FILE}"
echo "  Credentials: ${ENV_FILE}"
echo
echo "Prüfen:"
echo "  cd ${LIB_DIR} && docker compose ps"
echo "  ${WRAPPER_SCRIPT} ps"
echo "  ${WRAPPER_SCRIPT} logs"
echo
echo "Browser:"
echo "  https://localhost:${OPENSLIDES_PORT}"
echo "  https://<IP-DEINES-LAPTOPS>:${OPENSLIDES_PORT}"
echo
echo "Hinweise:"
echo "  - Beim selbstsignierten Zertifikat erscheint zunächst eine Browserwarnung."
echo
echo "Hinweis zur Firewall:"
echo "  Ein von Docker veröffentlichter Port wie 0.0.0.0:${OPENSLIDES_PORT}"
echo "  kann trotz UFW von außen erreichbar sein."
echo "  Bitte die Portbindung der Compose-Datei prüfen."

Zuletzt aktualisiert am 6. April 2026.