diff --git a/README.md b/README.md index d41ec8d..cf99e65 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags baseline ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags ocr ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags virtualbox ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags docker +ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags local_llm ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags grobid ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags languagetool ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags quarto @@ -107,6 +108,49 @@ ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags desktop You can also combine tags, e.g. `--tags baseline,docker,grobid,vscode`. + +## Local LLMs (Ollama + Open WebUI) + +This repository includes a `local_llm` role that runs: + +- `ollama` (API on `127.0.0.1:11434`) +- `open-webui` (web UI on `http://127.0.0.1:3000`) + +Both services run in Docker with persistent storage under `/opt/local-llm`: + +- Ollama models: `/opt/local-llm/ollama` +- Open WebUI data: `/opt/local-llm/open-webui` + +Install only this role: + +```sh +ansible-playbook -i inventory -K playbooks/lab-stack.yml --tags local_llm +``` + +Optional model preloading (to avoid manual `ollama pull` after install): + +```yaml +# host_vars/localhost.yml +local_llm_models: + - llama3.2:3b +``` + +Optional Open WebUI defaults: + +```yaml +# host_vars/localhost.yml +local_llm_webui_auth: true +local_llm_webui_default_models: "llama3.2:3b" +``` + +Quick checks: + +```sh +curl -s http://127.0.0.1:11434/api/tags | jq . +curl -I http://127.0.0.1:3000 +docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'ollama|open-webui' +``` + ## Day-to-day ### Update software and configuration diff --git a/playbooks/lab-stack.yml b/playbooks/lab-stack.yml index ce70d9b..0a53e91 100644 --- a/playbooks/lab-stack.yml +++ b/playbooks/lab-stack.yml @@ -20,6 +20,9 @@ - role: docker tags: [docker] + - role: local_llm + tags: [local_llm] + - role: grobid tags: [grobid] diff --git a/roles/local_llm/defaults/main.yml b/roles/local_llm/defaults/main.yml new file mode 100644 index 0000000..abd5fd8 --- /dev/null +++ b/roles/local_llm/defaults/main.yml @@ -0,0 +1,30 @@ +--- +local_llm_docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin +local_llm_python_packages: + - python3-docker + +local_llm_data_dir: /opt/local-llm +local_llm_network_name: local-llm + +local_llm_ollama_image: ollama/ollama:latest +local_llm_ollama_container_name: ollama +local_llm_ollama_port: 11434 +local_llm_ollama_models_dir: "{{ local_llm_data_dir }}/ollama" + +local_llm_webui_image: ghcr.io/open-webui/open-webui:main +local_llm_webui_container_name: open-webui +local_llm_webui_port: 3000 +local_llm_webui_data_dir: "{{ local_llm_data_dir }}/open-webui" +local_llm_webui_auth: true +local_llm_webui_default_models: "" + +# Keep empty by default to avoid pulling large models unintentionally. +# Example: +# local_llm_models: +# - llama3.2:3b +local_llm_models: [] diff --git a/roles/local_llm/tasks/main.yml b/roles/local_llm/tasks/main.yml new file mode 100644 index 0000000..0401901 --- /dev/null +++ b/roles/local_llm/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Install Docker Engine packages + ansible.builtin.dnf: + name: "{{ local_llm_docker_packages }}" + state: present + update_cache: true + +- name: Install Python Docker SDK dependency for Ansible modules + ansible.builtin.dnf: + name: "{{ local_llm_python_packages }}" + state: present + +- name: Enable and start Docker service + ansible.builtin.systemd: + name: docker + enabled: true + state: started + +- name: Create local LLM data directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - "{{ local_llm_data_dir }}" + - "{{ local_llm_ollama_models_dir }}" + - "{{ local_llm_webui_data_dir }}" + +- name: Ensure dedicated Docker network exists + community.docker.docker_network: + name: "{{ local_llm_network_name }}" + state: present + +- name: Run Ollama container + community.docker.docker_container: + name: "{{ local_llm_ollama_container_name }}" + image: "{{ local_llm_ollama_image }}" + restart_policy: unless-stopped + state: started + recreate: true + published_ports: + - "127.0.0.1:{{ local_llm_ollama_port }}:11434" + volumes: + - "{{ local_llm_ollama_models_dir }}:/root/.ollama" + networks: + - name: "{{ local_llm_network_name }}" + +- name: Wait for Ollama API to be reachable + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ local_llm_ollama_port }}" + timeout: 120 + +- name: Run Open WebUI container + community.docker.docker_container: + name: "{{ local_llm_webui_container_name }}" + image: "{{ local_llm_webui_image }}" + restart_policy: unless-stopped + state: started + recreate: true + published_ports: + - "127.0.0.1:{{ local_llm_webui_port }}:8080" + volumes: + - "{{ local_llm_webui_data_dir }}:/app/backend/data" + env: + OLLAMA_BASE_URLS: "http://{{ local_llm_ollama_container_name }}:11434" + WEBUI_AUTH: "{{ local_llm_webui_auth | default('True') | string }}" + DEFAULT_MODELS: "{{ local_llm_webui_default_models | default('') | string }}" + networks: + - name: "{{ local_llm_network_name }}" + +- name: Pull configured Ollama models (optional) + ansible.builtin.command: "docker exec {{ local_llm_ollama_container_name }} ollama pull {{ item }}" + loop: "{{ local_llm_models }}" + changed_when: false