💻 Bitmuncher's IT Stuff 💻



Ollama mit Open WebUI in Docker



Einleitung

Generative KIs werden immer ausgereifter und lassen sich mittlerweile vielseitig einsetzen. Leider befinden sich die bekannten Anbieter fast alle in den USA, womit es schwierig ist, deren LLM datenschutzkonform einzusetzen. Eine Alternative ist das Setup einer LLM auf einem eigenen Server. Die nachfolgende Anleitung beschriebt, wie man die freien LLM Phi-3 und Orca 2 auf einem einfachen Linux-Server in Docker-Containern laufen lassen kann. Dazu benötigt der Server nichtmal eine GPU, da beide LLM problemlos auf einer normalen CPU laufen können.

Als Basis werden wir Ollama verwenden. Damit man nicht auf den Komfort eines Web-Interface verzichten muss, werde ich auch darauf eingehen, wie man Open WebUI nutzen kann um ein komfortables Web-Interface zur Verfügung zu haben.

Da das Setup mittels docker-compose mit den aus den Projekten stammenden Images bei mir nur zu Problemen führte, habe ich mich entschieden ein Custom Setup zu erstellen. Mir fehlte einfach die Zeit zum debuggen. So war das Setup unkomplizierter und schneller zu bewerkstelligen. Hier ist also die Anleitung, wie das Setup funktioniert.

Schaut man sich außerdem die Dockerfiles von Open WebUI und Ollama an (oder lässt diese mal testweise als Container laufen), stellt man fest, dass die Prozesse darin mit root-Rechten laufen. Das ist aus Sicht der IT-Sicherheit natürlich eher nicht ratsam. Es gibt keinen Grund, dass diese Prozesse mit root-Rechten laufen müssen, auch wenn sie in Docker-Containern isoliert sind.

Wie Docker unter Linux eingerichtet wird, werde ich an dieser Stelle nicht beschreiben. Dafür gibt es genug Anleitungen im Internet. Ich gehe also im Folgenden davon aus, dass eine funktionierende Docker-Umgebung bereits zur Verfügung steht.

Setup von Ollama

Als erstes benötigen wir also Ollama. Hierfür nutzen wir folgendes Dockerfile:

# als Basis benutzen wir ein aktuelles Debian GNU/Linux
FROM debian:stable

ENV TERM xterm
ENV OLLAMA_HOST "0.0.0.0"

# sicherstellen, dass das Basis-System aktuell ist
RUN apt-get update && \
    apt-get -y upgrade

RUN apt-get -y install curl

# mit diesem Befehl lässt sich Ollama unter Linux und macOS auch lokal einrichten
RUN curl -fsSL https://ollama.com/install.sh | sh

# Benutzer anlegen
RUN groupadd -g 1000 ollama
RUN useradd -g ollama -d /usr/share/ollama -u 1000 ollama
RUN mkdir -p /usr/share/ollama
RUN chown -R ollama:ollama /usr/share/ollama

# Cleanup
RUN apt-get clean && \
    apt-get autoremove --yes && \
    rm -rf /var/lib/{apt,dpkg,cache,log}/*

# der Port, auf den die API bindet
EXPOSE 11434

# hier werden die Daten von Ollama gespeichert
VOLUME /usr/share/ollama

# Container sollten niemals als root laufen, also verwenden wir runuser
ENTRYPOINT runuser -l ollama -c 'OLLAMA_HOST=0.0.0.0 OLLAMA_ORIGINS='*' /usr/local/bin/ollama serve'
              

Das Image aus diesem Dockerfile erstellen wir, indem wir in den Ordner wechseln, in dem wir das Dockerfile abgelegt haben und folgenden Befehl ausführen:

docker build -t ollama .

Damit man nicht ständig seine LLM neu installieren muss, sollte man das im Dockerfile angegebene VOLUME auf dem Host-System einhängen. Wir erstellen also einen Ordner auf dem Host dafür:

mkdir -p /docker_data/ollama

Wie im Dockerfile zu sehen ist, wird Ollama mit dem Benutzer mit der UID 1000 ausgeführt. Der Ordner auf dem Host muss also diesem Benutzer gehören, damit der Prozess darin schreiben kann:

chown 1000:1000 /docker_data/ollama

Ich weise allen meinen Containern feste IP-Adressen zu, damit sie problemlos untereinander kommunizieren können, ohne dass ich sie extra miteinander verbinden muss. Dafür wird ein Docker-Netzwerk benötigt:

docker network create --subnet 172.10.0.0/16 bit
Natürlich kann das Netzwerk auch anders benannt werden. Das muss dann aber beim Starten der Container entsprechend beachtet werden.

Jetzt kann der Container für Ollama auch schon gestartet werden.

docker run -d \
    --name ollama \
    -v /docker_data/ollama:/usr/share/ollama \
    --restart always \
    --network bit \
    --ip 172.10.0.10 \
    -p 127.0.0.1:11434:11434 \
    ollama
              
Theoretisch kann man das Port-Binding an die 127.0.0.1 auch weglassen. Es kann aber ganz praktisch sein, wenn man die Ollama-API, z.B. für die Nutzung mit Skripten, auch über localhost erreichen kann.

Jetzt kann man Phi-3 bereits über das CLI nutzen. Hierfür führt man es einfach direkt im Container aus:

docker exec -it ollama ollama run phi3
Bei der ersten Ausführung wird das Phi-3-LLM heruntergeladen und in /usr/share/ollama/.ollama/models/ im Container gespeichert.

Auf die gleiche Weise kann auch das LLM Orca 2 hinzugefügt werden:

docker exec -it ollama ollama run orca2

Open WebUI

Der erste Teil wäre somit geschafft. Nun benötigen wir noch Open WebUI als Webinterface. Auch dieses lassen wir natürlich in einem Docker-Container laufen.

Um das Image zu erstellen, nutzen wir folgendes Dockerfile:

FROM debian:stable

WORKDIR /

RUN apt-get update && \
    apt-get -y upgrade

RUN apt-get -y install git npm python3-pip python3-venv

RUN git clone https://github.com/open-webui/open-webui.git

WORKDIR /open-webui

RUN python3 -m venv ./

COPY .env /open-webui/.env

ENV PATH=/open-webui/bin:$PATH

RUN npm i
RUN npm run build

RUN cd /open-webui/backend && /open-webui/bin/pip3 install -r requirements.txt -U

RUN groupadd -g 1000 ollama
RUN useradd -g ollama -d /usr/share/ollama -u 1000 ollama
RUN chown -R ollama:ollama /open-webui
RUN mkdir -p /usr/share/ollama
RUN chown -R ollama:ollama /usr/share/ollama

RUN apt-get clean && \
    apt-get autoremove --yes && \
    rm -rf /var/lib/{apt,dpkg,cache,log}/* \
    rm -rf /root/.cache

EXPOSE 8080/tcp

VOLUME /open-webui/backend/data

ENTRYPOINT runuser -l ollama -c 'PATH=/open-webui/bin:$PATH /open-webui/backend/start.sh'
              

Wie im Dockerfile zu sehen ist, wird die Datei .env benötigt. In dieser wird festgelegt, unter welcher URL die Ollama-API erreichbar ist. Die Datei hat folgenden Inhalt:

# Ollama URL for the backend to connect
# The path '/ollama' will be redirected to the specified backend URL
OLLAMA_BASE_URL='http://172.10.0.10:11434'

OPENAI_API_BASE_URL=''
OPENAI_API_KEY=''

# AUTOMATIC1111_BASE_URL="http://localhost:7860"

# DO NOT TRACK
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true

# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"
              

Auch hier wechseln wir wieder in den Ordner, in dem das Dockerfile und die Datei .env abgelegt sind, und erstellen das Image mit folgendem Befehl:

docker build -t ollama-webui .

Auch hier sollten ein paar Daten auf dem Host persistiert werden, damit man nicht ständig seine Benutzer neu anlegen muss. Wir erstellen also einen Ordner dafür auf dem Host-System und ändern die Rechte entsprechend:

mkdir -p /docker_data/ollama-webui-backend-data
chown 1000:1000 /docker_data/ollama-webui-backend-data
              

Und schon kann auch der Container mit Open WebUI gestartet werden:

docker run -d \
    --name ollama-webui \
    -p 8080:8080 \
    -e OLLAMA_BASE_URL=http://172.10.0.10:11434 \
    -v /docker_data/ollama-webui-backend-data:/open-webui/backend/data \
    --restart always \
    --network bit \
    --ip 172.10.0.20 \
    --add-host=host.docker.internal:host-gateway \
    ollama-webui
              

Der Start kann ggf. ein paar Sekunden dauern. Ein Blick in die Container-Logs

docker logs ollama-webui
sollte nach einem erfolgreichen Start folgendes Banner zeigen:
INFO:     Started server process [11]
INFO:     Waiting for application startup.

  ___                    __        __   _     _   _ ___
 / _ \ _ __   ___ _ __   \ \      / /__| |__ | | | |_ _|
| | | | '_ \ / _ \ '_ \   \ \ /\ / / _ \ '_ \| | | || |
| |_| | |_) |  __/ | | |   \ V  V /  __/ |_) | |_| || |
 \___/| .__/ \___|_| |_|    \_/\_/ \___|_.__/ \___/|___|
      |_|


v0.1.122 - building the best open-source AI user interface.
https://github.com/open-webui/open-webui
              
Mit curl kann auch bereits verifiziert werden. ob Open WebUI korrekt läuft:
curl 'http://172.10.0.20:8080'

Nginx als Proxy-Server und für SSL-Terminierung

Ggf. wollen wir den Server aber auch über das Internet nutzen. Ich nutze üblicherweise einen Nginx auf dem Host-System als Proxy-Server für meine Docker-Container. SSL-Zertifikate beziehe ich über Let's Encrypt.

Ich möchte an dieser Stelle nicht auf alle Details eines Nginx-Setups eingehen. Das würde etwas zu weit gehen, da dabei einiges zu beachten ist. Wer jedoch einen Server im Internet betreibt, sollte sich mit einem Webserver-Setup auskennen. Ist das nicht der Fall, empfehle ich einen Blick in meinen Blog-Beitrag zur Webserver-Sicherheit. Als Domain nehme ich für das Beispiel die Dummy-Domain ai.domain.tld.

Die Server-Konfiguration für den Nginx könnte z.B. so aussehen:

upstream web-ai {
  server       172.10.0.20:8080;
}

server {
  listen                80;
  server_name           ai.domain.tld;
  return 301            https://$host$request_uri;
}

server {
  listen                443 ssl;
  server_name           ai.domain.tld;

  ssl_ecdh_curve              secp384r1;
  ssl_protocols               TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
  ssl_ciphers                 HIGH:!aNULL:!MD5:!ADH:!eNULL:!LOW:!EXP;
  ssl_prefer_server_ciphers   on;
  ssl_session_timeout         10m;
  ssl_session_cache           shared:SSL:10m;
  ssl_session_tickets         off;
  ssl_stapling                on;
  ssl_stapling_verify         on;
  ssl_dhparam                 /etc/nginx/dhparams.pem;

  ssl_certificate             /etc/letsencrypt/live/domain.tld//fullchain.pem;
  ssl_certificate_key         /etc/letsencrypt/live/domain.tld/privkey.pem;

  add_header Strict-Transport-Security    "max-age=31536000; includeSubDomains" always;
  add_header X-Content-Type-Options       nosniff;
  add_header Content-Security-Policy      "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
  add_header X-XSS-Protection             "1; mode=block";
  add_header X-Frame-Options              "SAMEORIGIN";

  client_body_buffer_size     10M;
  client_header_buffer_size   10M;
  client_max_body_size        10M;
  large_client_header_buffers 2 10M;

  location / {
    proxy_set_header    X-Real-IP           $remote_addr;
    proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto   $scheme;
    proxy_set_header    Host                $host;
    proxy_set_header    X-Forwarded-Host    $host;
    proxy_set_header    X-Forwarded-Port    $server_port;
    proxy_ssl_server_name on;
    proxy_buffering     off;
    proxy_set_header    Origin              '';
    proxy_set_header    Referer             '';
    limit_except GET HEAD POST { deny all; }
    proxy_pass          http://web-ai;
  }

  # deny access to .htaccess and .git
  location ~ /\.ht {
    deny all;
  }
  location ~ /\.git {
    deny all;
  }
}
              

Ist der Nginx korrekt eingerichtet, kann nun auf Open WebUI über die konfigurierte Domain zugegriffen werden. Der erste Benutzer, der angelegt wird, ist automatisch der Administrator. Weitere Benutzer können zwar angelegt werden, müssen jedoch vom Administrator explizit freigegeben werden. Nach dem Login zeigt sich dann auch, dass phi3 und orca2 bereits zur Verfügung stehen.

Viel Spaß!


  
Design based on Dracula UI