OpenSlides portable – local, mobile, ready to use

I wanted to set up OpenSlides not on a server in the network, but on an Ubuntu laptop for local use on the road. Without a caddy, without a public domain, without Let’s Encrypt. Simply so that the computer in the respective event network gets an address via DHCP and OpenSlides runs directly locally – with a self-signed certificate and Docker.

For this structure, I have placed the app and runtime data in /usr/terruhn.it/ and the configuration and access data in /etc/terruhn.it/. This keeps the structure clear and fits in well with my general layout on Linux systems.

OpenSlides itself runs as a Docker compose stack. Because the docker-compose.yml can be regenerated by OpenSlides, I did not want to maintain any permanent manual customisations there. A small systemd wrapper therefore takes over the work for the automatic start after the boot. This way, the generated compose file remains untouched and the stack still comes up reliably.

This is a convenient solution for mobile operation: open the laptop, connect it to the local network, start OpenSlides and you’re done. Due to the self-signed certificate, a warning is initially displayed in the browser, after which a local, portable installation is available, which is ideal for small events, tests or applications without a fixed server infrastructure.

What I like about this variant is its simplicity. No external DNS, no reverse proxy, no dependency on a fixed public environment. Just one computer, Docker and a clear purpose: to make OpenSlides available wherever it is needed.

I have packed the entire installation into a shell script. After the download, change the extension to .sh and make the file executable. And of course I created the script with the help of KI and because Easter is just around the corner I haven’t tested the script yet!

#!/bin/sh
set -eu

###############################################################################
# OpenSlides portable auf Ubuntu 24.04 LTS
# - 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 beim Download auf eine konkrete Version aufgelöst
#   und danach in der Konfiguration fest eingetragen
###############################################################################

###############################################################################
# 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
}

###############################################################################
# 2) Interaktive Abfrage
###############################################################################

echo
echo "OpenSlides portable – interaktive Einrichtung"
echo

PATH_PREFIX="$(ask_required "Pfad-Präfix unter /usr und /etc" "terruhn.it")"
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" "w014178e.kasserver.com")"
    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

###############################################################################
# 3) 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"

OS_VERSION=""

###############################################################################
# 4) Zusammenfassung vor Ausführung
###############################################################################

echo
echo "Zusammenfassung"
echo "  Pfad-Präfix:          ${PATH_PREFIX}"
echo "  Angefragt:            ${OS_VERSION_REQUESTED}"
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

###############################################################################
# 5) 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}"

###############################################################################
# 6) Grundpakete und Docker bereitstellen
###############################################################################

log "Grundpakete prüfen"
apt update
apt install -y ca-certificates curl gnupg lsb-release sed

if ! command -v docker >/dev/null 2>&1; then
    log "Docker ist noch nicht installiert, Installation vorbereiten"

    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

    apt update
    apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
fi

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

need_cmd docker
need_cmd curl
need_cmd sed

if ! command -v docker-compose >/dev/null 2>&1; then
    log "docker-compose Wrapper anlegen"
    cat > /usr/local/bin/docker-compose <<'EOF'
#!/bin/sh
exec docker compose "$@"
EOF
    chmod 0755 /usr/local/bin/docker-compose
fi

need_cmd docker-compose

###############################################################################
# 7) OpenSlides-Binary laden und konkrete Version ermitteln
###############################################################################

log "OpenSlides-Binary herunterladen"

if [ "${OS_VERSION_REQUESTED}" = "latest" ]; then
    EFFECTIVE_URL="$(curl -fsSL -o "${OPENSLIDES_BIN}" -w '%{url_effective}' \
        "https://github.com/OpenSlides/openslides-manage-service/releases/download/latest/openslides")"

    OS_VERSION="$(printf '%s\n' "${EFFECTIVE_URL}" | sed -n 's#.*/releases/download/\([^/]*\)/openslides#\1#p')"

    if [ -z "${OS_VERSION}" ] || [ "${OS_VERSION}" = "latest" ]; then
        echo "Konnte konkrete OpenSlides-Version aus latest-Download nicht ermitteln." >&2
        exit 1
    fi
else
    curl -fL \
        "https://github.com/OpenSlides/openslides-manage-service/releases/download/${OS_VERSION_REQUESTED}/openslides" \
        -o "${OPENSLIDES_BIN}"

    OS_VERSION="${OS_VERSION_REQUESTED}"
fi

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

need_cmd openslides

###############################################################################
# 8) 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}"

###############################################################################
# 9) .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}"

###############################################################################
# 10) 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

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

if [ -f "${LIB_DIR}/docker-compose.yml" ]; then
    log "Bestehende Instanz erkannt, docker-compose.yml 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

###############################################################################
# 12) 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}"

###############################################################################
# 13) 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"

###############################################################################
# 14) 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"

###############################################################################
# 15) 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

###############################################################################
# 16) 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 "  docker-compose -f ${LIB_DIR}/docker-compose.yml 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 "Hinweis:"
echo "  Beim selbstsignierten Zertifikat erscheint zunächst eine Browserwarnung."

Last Updated on March 29th, 2026 by Rene Terruhn