docs.na.id.au

How to build from scratch

08 — Build Instructions

How to build the system from scratch on a fresh Ubuntu install: native llama.cpp inference servers, a Docker application tier, and the initial Web UI wiring.

How to build from scratch

The build order, and what each piece produces:

1. Ubuntu                           → a working base OS
2. PostgreSQL                       → host database for Open WebUI
3. Docker + folder layout           → /etc/docker/compose, /var/local/docker-files
4. NVIDIA drivers                   → nvidia-smi shows both GPUs
5. llama.cpp                        → /usr/local/bin/llama-server
6. Model + config folders           → /usr/local/llama/models, /etc/llama
7. systemd services                 → llama-primary (:8082), llama-secondary (:8083)
8. Docker containers                → Open WebUI (:8081), Open Terminal (:8080), Pipelines (:9099)
9. Web UI configuration             → connections, integrations, sub-agent, skills
10. (Bonus) Authentik OAuth         → login via https://auth.<TLD>

Steps 1–5 are standard installs; where upstream documentation exists, follow it. This document covers the parts specific to this stack: the folder layout, the systemd units, the model preset files, the compose files, and the Web UI wiring.

Security note: all API keys and secrets are redacted (<REDACTED>) throughout. Generate your own.

1 — Install Ubuntu

Install Ubuntu Server (minimal) and SSH access per the official Ubuntu installation guide. No stack-specific configuration is needed at this stage.

2 — PostgreSQL

PostgreSQL is not required, but recommended for storing application and user data.

2.1 — Install PostgreSQL

Install per the official PostgreSQL installation guide:

sudo apt install postgresql

2.2 — Enable Password Authentication

Configure /etc/postgresql/<version>/main/pg_hba.conf for local authentication — ensure this line is present (uncomment or add):

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     scram-sha-256
# IPv4 local connections:
host    all             all             127.0.0.1/32            scram-sha-256
# IPv6 local connections:
host    all             all             ::1/128                 scram-sha-256
sudo systemctl reload postgresql

2.3 — Create the openwebui user and database

On Debian and Ubuntu, the quickest way to admin PostgreSQL is via the postgres system account:

sudo -u postgres psql

Once connected, create the user with:

CREATE USER openwebui WITH NOCREATEDB PASSWORD '<Insert-Password>';

With the user created, you can now create a database owned by that user:

CREATE DATABASE openwebui WITH OWNER openwebui;

You will need these credentials later when configuring the OpenWebUI docker container.

3 — Install Docker and create the folder layout

Install Docker Engine and the Compose plugin per the official Docker install guide.

Register the NVIDIA runtime so containers can request GPUs (add to /etc/docker/daemon.json):

{
    "runtimes": {
        "nvidia": {
            "args": [],
            "path": "nvidia-container-runtime"
        }
    }
}

Create the two roots used throughout the build:

/var/local follows the FHS convention for variable data belonging to applications installed under /usr/local1.

sudo mkdir -p /etc/docker/compose/open-webui
sudo mkdir -p /var/local/docker-files/{open-webui,open-terminal,open-webui-pipelines}

4 — Install NVIDIA drivers and llama.cpp

While Ubuntu ships packaged NVIDIA drivers (apt install nvidia-driver installs the currently supported version), upstream drivers will provide new features.

4.1 — Install NVIDIA Upstream Drivers

Install the proprietary NVIDIA driver and toolkit per the NVIDIA Driver Installation Guide — Ubuntu.

Choose the Network Repository Enablement (amd64) instructions. The <distribution> is ubuntu2604.

Verify with nvidia-smi -L — you should see one line per GPU (GPU 0 = the big card, GPU 1 = the small card).

5 — Install llama.cpp

llama.cpp can be installed as pre-built packages, or compiled from source. If you intend to use asymmetric quantisation of the KV cache, currently you will need to build from source.

5.1 — Upstream llama.cpp packages

Install llama.cpp from the official release builds — download the CUDA-enabled llama-*.tar.gz asset and place the binaries in /usr/local/bin:

sudo tar -xzf llama-*.tar.gz -C /usr/local/bin

⚠️ The release asset names and folder layout change between releases; check the current release page.

5.2 — Build from source

Install the prerequisites:

sudo apt update
sudo apt install -y git build-essential cmake libcurl4-openssl-dev

Clone the repository:

git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp

Build:

cmake -B build -DGGML_CUDA=ON -DGGML_CUDA_FA_ALL_QUANTS=ON -DGGML_NATIVE=OFF
cmake --build build --config Release -j$(nproc)

Install:

sudo cmake --install build --prefix /usr/local

5.3 — Verify your installation

Verify:

/usr/local/bin/llama-server --version

6 — Create model and configuration folders

sudo mkdir -p /usr/local/llama/models /usr/local/llama/templates /etc/llama

Download models from Ollama or Hugging Face and place the .gguf files under /usr/local/llama/models. The current deployment is a pure llama.cpp install (no Ollama): every preset model = line points at a named .gguf file in that directory, and custom chat templates live in /usr/local/llama/templates/.

7 — Create the systemd services for the inference servers

Two units, one per GPU — see Primary vs Secondary for the role split. The model presets live in /etc/llama/; sizing guidance is in Context & Quantisation.

/etc/llama/models-primary.ini (big card — the “brain”; two models, large context):

[version]
version = 1

[*]
# Global fallbacks for all models
sleep-idle-seconds = 3600
load-on-startup = false
flash-attn = true
batch-size = 1024
ubatch-size = 256
threads = 6
threads-batch = 6
gpu-layers = 99

[gemma4-12b]
model = /usr/local/llama/models/Gemma4-12B-Q4_K_M.gguf
ctx-size = 262144
context-shift = true
keep = 1024
cache-type-k = q8_0
cache-type-v = q4_0

[qwen3.8-27b]
model = /usr/local/llama/models/Qwen3.8-27B-UD-Q4_K_M.gguf
ctx-size = 185000
context-shift = true
keep = 1024
cache-type-k = q8_0
cache-type-v = q4_0
jinja = true
chat-template-file = /usr/local/llama/templates/froggeric_chat_template.jinja

/etc/llama/models-secondary.ini (small card — resident embedding model + sub-agent worker):

[version]
version = 1

[*]
flash-attn = true
threads = 6
threads-batch = 6
gpu-layers = 99
chat-template-kwargs = {"enable_thinking": false}

[bge-m3]
model = /usr/local/llama/models/BGE-M3.gguf
load-on-startup = true
pooling = cls
ctx-size = 2048
cache-type-k = f16
cache-type-v = f16
batch-size = 1024
ubatch-size = 1024

[qwen3.5-4b]
model = /usr/local/llama/models/Qwen3.5-4B-Q6_K.gguf
load-on-startup = true
parallel = 2
ctx-size = 8192
kv-unified = true
cache-type-k = q8_0
cache-type-v = q4_0
batch-size = 2048
ubatch-size = 512

/etc/systemd/system/llama-primary.service:

[Unit]
Description=Llama.cpp Primary Server

[Service]
Type=simple
User=ollama
Group=ollama
WorkingDirectory=/usr/local/bin
Environment=CUDA_VISIBLE_DEVICES=0
ExecStart=/usr/local/bin/llama-server \
    --models-preset /etc/llama/models-primary.ini \
    --models-max 1 \
    --no-mmproj \
    --metrics \
    --host :: \
    --port 8082
Restart=on-failure
RestartSec=5
StandardOutput=append:/var/log/llama_primary.log
StandardError=append:/var/log/llama_primary_error.log

[Install]
WantedBy=multi-user.target

/etc/systemd/system/llama-secondary.service — same shape, with these differences:

[Unit]
Description=Llama.cpp Secondary Server (preset: bge-m3 embeddings, qwen2.5:0.5B, qwen2.5:7B-instruct, qwen3.5:9B)
After=network.target

[Service]
User=ollama
Group=ollama
WorkingDirectory=/usr/local/bin
Environment=CUDA_VISIBLE_DEVICES=1
ExecStart=/usr/local/bin/llama-server \
    --models-preset /etc/llama/models-secondary.ini \
    --no-mmproj \
    --embeddings \
    --metrics \
    --host :: \
    --port 8083
Restart=always
RestartSec=5
StandardOutput=append:/var/log/llama_secondary.log
StandardError=append:/var/log/llama_secondary_error.log

Notes:

Enable and verify:

sudo systemctl daemon-reload
sudo systemctl enable --now llama-primary llama-secondary
curl http://localhost:8082/v1/models
curl http://localhost:8083/v1/models

8 — Deploy the Docker containers

Create the compose directories from step 3 and bring up the containers without OAuth configured (that is step 10). All application containers share the ai-shared-net network and map host.docker.internal to the Docker host so they can reach the native llama servers.

/etc/docker/compose/open-webui/docker-compose.yaml (three services; without OAuth the .env is minimal — just WEBUI_AUTH=true and a DATABASE_URL, see step 9 for the full version):

services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    ports:
      - "8081:8080"
    env_file:
    - .env
    volumes:
      - /var/local/docker-files/open-webui:/app/backend/data
      - /run/postgresql:/postgresql
    networks:
      - ai-network
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

  open-terminal:
    image: ghcr.io/open-webui/open-terminal
    container_name: open-terminal
    ports:
      - "8080:8080"
    volumes:
      - /var/local/docker-files/open-terminal:/home
    environment:
      - OPEN_TERMINAL_API_KEY=<REDACTED>
      - OPEN_TERMINAL_MULTI_USER=true
    networks:
      - ai-network
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

  pipelines:
    image: ghcr.io/open-webui/pipelines:main
    container_name: open-webui-pipelines
    ports:
      - "9099:9099"
    environment:
      - PIPELINES_API_KEY=<REDACTED>
    volumes:
      - /var/local/docker-files/open-webui-pipelines:/app/pipelines
    networks:
      - ai-network
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

networks:
  ai-network:
    name: ai-shared-net

Deploy:

docker network create ai-shared-net   # once; the first compose file also creates it
cd /etc/docker/compose/open-webui && docker compose up -d

9 — Initial Web UI configuration

Open WebUI at http://<host>:8081. On first start it creates an admin account — note it; you’ll be the first user.

9.1 — Configure model connections

Admin Panel → Connections → Add Connection (OpenAI API style). The base URLs are entered in the UI and stored in Open WebUI’s data volume, so they survive image upgrades but not a wiped volume — keep the volume backed up (see 01 — Architecture).

NameBase URLModel(s)Role
llama-primaryhttp://host.docker.internal:8082/v1brain models (12B / 27B)main chat, sub-agent planning
llama-secondaryhttp://host.docker.internal:8083/v1worker (4B), bge-m3sub-agent worker, embeddings

⚠️ The exact UI labels for a raw OpenAI-compatible endpoint vs the “OpenAI API” connection type vary between Open WebUI releases; if a plain endpoint isn’t offered, use the “OpenAI API” connection type with the base URLs above and no API key.

9.2 — Configure Integrations

Add Local-Terminal as an integration (Admin Panel → Integrations → Add), pointing at the Open Terminal service from step 8:

NameBase URLNotes
Local-Terminalhttp://host.docker.internal:8080Open Terminal tool; API key from step 8

See Open Terminal & Tooling for the security notes that apply to this integration.

9.3 — Configure the sub-agent

Sub-agents are provided by the community Sub Agent tool — import the tool code from the Sub Agent Tool post into the Admin Panel (Tools → Add Tool → paste the code) and enable it. It runs tool-heavy tasks in an isolated context and returns only the final result; the model it delegates to is the worker on the llama-secondary connection (see Sub-agents for the pattern).

Requirement: the model must have native Function Calling enabled (Model settings → Advanced Params → Function Calling: native).

9.4 — Configure skills

Skills are files in the agent’s workspace, not Web UI settings — see Skills for the concept and 05 — Open Terminal & Tooling for the terminal they direct. In practice: create a known directory (persisted, e.g. under /var/local/docker-files/open-terminal/ so it survives container rebuilds), drop skill files in it, and reference that path in the system prompt or the sub-agent briefs.

⚠️ The skill storage location and how the deployed agent discovers skills is deployment-specific — confirm the path the agent actually reads.

10 — Bonus: configure OAuth for Authentik

Once the UI works with a local account, switch it to Authentik via OpenID Connect, per the Open WebUI OAuth2/OIDC documentation.

Create the application. In Authentik, create an Application (Provider: OpenID, Protocol: OIDC) for Open WebUI and note the client ID/secret.

Replace the minimal .env from step 8:

OAUTH_CLIENT_ID=<REDACTED>
OAUTH_CLIENT_SECRET=<REDACTED>
OAUTH_PROVIDER_NAME=Auth.<TLD>
OAUTH_AUTO_REDIRECT=true
OPENID_PROVIDER_URL=https://auth.<TLD>/application/o/open-web-ui/.well-known/openid-configuration
OPENID_REDIRECT_URI=https://ai.<TLD>/oauth/oidc/callback

# Core auth: OAuth only, no local sign-up
WEBUI_AUTH=true
ENABLE_LOGIN_FORM=false
ENABLE_SIGNUP=false
ENABLE_OAUTH_SIGNUP=true

# General
WEBUI_URL=https://ai.<TLD>
CORS_ALLOW_ORIGIN=https://ai.<TLD>;http://<lan-ip>:8081
DATABASE_URL=postgresql://openwebui:<REDACTED>@/openwebui?host=/postgresql

Restart: docker compose up -d.

⚠️ This assumes a reverse proxy (Caddy/Traefik/nginx) terminating TLS at ai.<TLD> and forwarding to :8081, with the container able to reach the Authentik server. The redirect URI, WEBUI_URL and CORS_ALLOW_ORIGIN must all match the proxy hostname.

Uncertainties

Areas above that are flagged with ⚠️, summarised:


Source Disclaimer


  1. FHS — Variable Hierarchy ↩︎