My thoughts on Slackware, life and everything

Month: April 2026

KDE 6_26.04 for Slackware-current (almost free of Qt5)

To be honest, I was waiting for a move from Pat. But I got restless, Pat is otherwise occupied for a few days so I took the plunge.
What I am talking about is of course packaging KDE Gear 26.04.0 for Slackware. The latest release of Applications and KDEPIM is nicknamed the “KDE at 30″ edition because KDE is around for 30 years already (!). Congratulations are in order.

The reason I wanted to wait for Pat is that the new Kleopatra release (part of KDEPIM) requires a version of gpgme which is not present in Slackware-current. I had hoped to see an upgrade to gpgme 2.x in Slackware first, followed by a rebuild of affected packages, which according to avid Slackware user gmgf aka Gérard Monpontet is at least: gmime, gpa, libcups-filters, mccabber, mutt, poppler, samba, volume_key, wget2 and labplot. But that did not happen, and I wanted to have a stable ‘ktown’ which is fully ported to Qt6 before my 65th birthday next week.
Therefore I decided to trick Slackware by upgrading gpgme from 1.24.3 to 2.0.1 but not rebuilding all those other Slackware packages that depend on gpgme 1.24.3. Instead I added another package to ‘ktown’ called gpgme1 which contains all the libraries from the previous gpgme 1.24.3 package. Zero broken Slackware packages and I could finally move on with KDE Gear without having to wait for the upstream.
As you might have guessed after all these years – I hate to be dependent on others and like to have full control. So, move aside Pat 😉
Jokes aside, I hope that Pat picks up this completed work and adds it to Slackware-current soon.

As promised when I revived the ‘ktown’ repository for Plasma6, the addition of a legacy-free KDE Gear 26.04.0 marks the change for this repository from ‘testing’ to ‘latest’, as witnessed by the change in the package download URL: https://slackware.nl/alien-kde/current/latest/. The latest KDE Plasma6 Desktop Environment is absolutely ready for production use. It’s snappy, feature-complete and beautiful. I have been using the Wayland session ever since my first batch of packages and the quirks have by now been removed that annoyed me in the beginning. By now, okteta in ‘applications-extra’ is the only left-over of the old Qt5/KF5 era.

Formally kwayland-integration is also still built against Qt5 and the two old Frameworks kwayland5 and kwindowsystem5, but that’s required for the Plasma6 Wayland session to still support older 3rd-party Qt5 based applications).

The new release of packages is accompanied by an expansive README which will help you remove KDE Plasma5 from your Slackware-current computer and install the ‘ktown‘ version of KDE Plasma6 instead.
The origin host is of course https://slackware.nl/alien-kde/ (rsync://slackware.nl/mirrors/alien-kde/), but you could choose the alternative
mirror https://slackware.uk/people/alien-kde/ (rsync://slackware.uk/people/alien-kde/) which is faster for some people, but you may have to wait until it syncs against slackware.nl. Or if you live in the US, try https://us.slackware.nl/alien-kde/ (rsync://us.slackware.nl/mirrors/alien-kde/), this server has a lot of bandwidth available.

If you want to peek at the source code management, I track everything in a git repository. You will find the new 6_26.04 branch at: https://git.slackware.nl/ktown/

Have fun with KDE Plasma6 and please  leave your feedback in the comments section below.

Cheers, Eric

Slackware Cloud Server Series, Episode 12: Local AI

The world is on fire, thanks to the orange clown who wages war for personal gain. Or is it because data centers are super-heated running all these AI models 24/7 ?
In any case, the AI boom wreaked havoc with my plans to purchase a new computer in order to replace my ageing build server here at home. RAM sticks are 4 to 5 times as expensive now compared to half a year ago, and hard drives are pretty hard to come by.
I decided to wait a bit with a full replacement of the server hardware. Instead I bought one item which has had only a moderate price increase until now: a GeForce RTX 5060 Ti graphics card with 16 GB of VRAM. I installed that as the second GPU card in the server and did not connect any screen to it.

Instead I decided to do a local experiment with Artificial Intelligence. The result of that experiment is a new episode in my Slackware Cloud Server series. I am going to show you how to make the un-used Nvidia GPU available to Slackware, how to install and configure a tool that manages (downloads, runs) local Large Language Models (aka LLM’s aka AI models) and then expose the AI models via a web page that looks a lot like a Claude Chat or a ChatGPT instance.


Check out the list below which shows past, present and future episodes in my Slackware Cloud Server series. If the article has already been written you’ll be able to click on the subject.
The first episode also contains an introduction with some more detail about what you can expect.


Why the hell would I want to run a local AI at home?

I think that the advantages are pretty obvious, but let me spell them out for you.

  • Privacy & data security
    It’s my main reason for doing this. The whole Cloud Server series is about owning, controlling and managing your own data without giving it away to the big tech corporations. Literally all of the processing happens on our Slackware server. None of your sensitive files, private documents or even the software code that you are developing gets sent to someone else’s infrastructure.
  • Offline accessibility
    The local LLM does not require an active internet connection. It’ll be your conscious decision to give the AI model access to Internet search engines.
  • Cost efficiency (at least long-term)
    I had to make an upfront investment in the required hardware of course. LLM’s need to run inside the VRAM of a high-end GPU in order to respond with a decent speed. If you have a spare GPU in your gaming rig, then by all means re-use that card! But really, running LLMs locally will remove the need for monthly subscription fees and per-token API costs. I know people who pay 100 euros per month to be able to consume the API tokens that they need for business development.
    If you are a high-volume LLM user, running the model locally can lead to substantial savings over time. Your local AI may not be one the fancy new commercial models and the speed of answering may be a bit slower, but there’s always going to be trade-offs.
  • Reduced Latency
    Often overlooked actually, but all your ChatGPT, Gemini or Claude queries involve a “network round trip” to a remote server. If you want to create an interactive AI service like a voice assistant, a local model may be able to offer snappier responses.
  • Customization & Control
    Obviously, you have full ownership of the model you downloaded and you control its environment. This allows you to:

    • Fine-tune the model on your own niche datasets.
    • Choose specific open-source models (like Llama 3, Gemma 4 or Mistral) and quantization levels that fit your hardware.
    • Avoid content restrictions or “guardrails” defined by commercial providers.
  • Reliability & Independence
    You are not at the mercy of Big Tech! Any downtime is your own problem to solve; you never run into rate limits; you will never be hit with sudden policy changes that deprecate the model you rely on overnight.

Here is an architectural overview what the stack looks like. We install Ollama on bare-metal and will be running Open WebUI in a Docker container.


Preamble

This section describes the technical details of our setup, as well as the things which you should have prepared before trying to implement the instructions in this article.

Web Hosts

For the sake of this instruction, I will use the URL “https://ai.darkstar.lan” as your landing page for your private AI chatbot.

Setting up your domain (which will hopefully be something else than “darkstar.lan”…) with new hostnames and then setting up web servers for the hostnames in that domain is an exercise left to the reader. Before continuing, please ensure that your equivalent for the following host has a web server running. It doesn’t have to serve any content yet but we will add some blocks of configuration to the VirtualHost definition during the steps outlined in the remainder of this article:

  • ai.darkstar.lan

Using a  Let’s Encrypt SSL certificate to provide encrypted connections (HTTPS) to your webserver is documented in an earlier blog article.

Note that I am talking about webserver “hosts” but in fact, all of these are just virtual webservers running on the same machine, at the same IP address, served by the same Apache httpd program, but with different DNS entries. There is no need at all for multiple computers when setting up your Slackware Cloud server.

Port numbers

  • Ollama uses TCP port 11434 by default and we are not going to change that.
  • The Open WebUI docker container will listen at the loopback TCP port 3456 which is where the Apache Reverse Proxy will direct incoming traffic.

Secret keys

  • For persistent logins:
    WEBUI_SECRET_KEY=eePAAjEgEnZdgAQcVKb/DA993rwU+xbBb1scG0Zz1sQ=
  • Connecting Open Terminal to Open WebUI:
    OPEN_TERMINAL_API_KEY=qIShpFT2IUZaglqLTX5UCw6oQSyuCuKgpgF/xViqUWA=

Random strings like these can be generated using a convoluted series of commands like:

$ cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 | openssl dgst -binary -sha256 | openssl base64

… which outputs a 45-character string ending on ‘=’.
Or generate 32 random characters with a truncated version of that commandline:

$ cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1

Docker network

  • We assign a Docker network segment to our Open WebUI container: 172.24.1.0/24 and call it “localai.lan

File Locations

  • The Docker configuration goes into: /usr/local/docker-localai/
  • The vector database for maintaining chat history and other data downloaded by the Ollama server or generated by Open Terminal go into: /opt/dockerfiles/localai/
  • Our Nextcloud server is installed into /var/www/htdocs/nextcloud/

Server configuration steps

I will break down the story into its main parts:

  1. Physically install a GPU with sufficient Video RAM (VRAM)
  2. Install the Nvidia binary driver and kernel module
  3. Install CUDA toolkit
  4. Install and configure Ollama – which will make use of the Nvidia driver and the CUDA toolkit to load LLMs into the GPU’s VRAM
  5. Create the Docker network and the local directory structure
  6. Install Open WebUI – which gives us a nice web page where we can manage and query our AI models
  7. Configure Apache reverse proxy to expose the Open WebUI to the network

Install the GPU card

Before you buy any new GPU hardware, you need to make sure that your motherboard and PSU support the card. In my case, the GeForce RTX 5060 Ti card needs a 8-pin MOLEX power connector and requires a 650W PSU. Even my 10-year old server meets those requirements.  Caveat: the GeForce RTX 5060 supports PCIe 5.0 but my old ASUS Prime B350-plus motherboard only supports PCIe 3.0. This is a backward compatible protocol, so this rather recent graphics card still works in my server, but it will not be able to reach its full performance and speed. Eventually, I will have to upgrade the rest of my server hardware also.
However, the point is to run a local AI model entirely in the Video RAM (VRAM) and then PCIe speeds are not an important consideration.

I kept my fanless GeForce GT 1030 card in the server as well. It is connected to a monitor using a regular kernel driver. That way, the new card can be fully utilized for AI inference and I still have local access to the server console..

Install the NVidia binary driver and kernel module

The GeForce RTX 5060 (which is based on the Blackwell architecture) requires the Nvidia open GPU kernel modules for proper functionality on Linux. The standard proprietary kernel module downright refuses to support this rather new card.
On the other hand, my old GT1030 card is not even detected by the open GPU kernel module, which made it really easy for me to keep both cards in the server – the old card using the Linux kernel driver to allow local access to the console, and the new card using the Nvidia open driver which enables the use of local AI.

Typically I would now point you to packages in my local repository to install the software you need. But for the Nvidia driver I do not have packages. They are too much of a moving target, with the multiple versions each supporting ranges of GPU models, and each having a kernel module that should match a Slackware kernel.
Instead I would like to point you to the SlackBuilds.org script repository, where you can download the required SlackBuild scripts and supporting files to compile these packages yourself.
You will need:

  • nvidia-driver (I used the 580.105.08 release but the current available version is already at 595)
  • nvidia-kernel (edit the SlackBuild script and enable the “OPEN” build by setting the variable OPEN to “yes

Build those two packages and install them, then reboot your computer. Use the ‘nvidia-smi’ program which is part of the nvidia-driver package to verify that your GPU is recognized and ready for use:

# nvidia-smi -L
GPU 0: NVIDIA GeForce RTX 5060 Ti (UUID: GPU-065f08c5-cd2f-48bc-ac7c-2454613fd064)

We’re ready for the next step.

The CUDA toolkit

CUDA what?

A brief explanation first. I assume that you have had some thought about why there’s this hype around GPU’s in relation to AI. There is a fundamental overlap between the technologies (more specifically the Graphical Processing Unit or GPU) that were developed to speed up the rendering of three-dimensional graphics in computer games, and the capabilities needed to train AI programs and make them respond in real-time. Both require hardware that can perform thousands of simple, identical calculations simultaneously, at scale:

  • To render a 3D scene, a GPU must calculate the color and position of millions of pixels at once. This is done using linear algebra (matrix and vector multiplication).
  • Training a neural network (the basic building block of any AI program) also relies on massive matrix multiplications to adjust millions of “weights” or parameters.

The math involved here is nearly identical! The hardware designed to “paint” a video game frame was accidentally perfect for “training” an AI model.

Now, CUDA (Compute Unified Device Architecture) is a parallel computing platform and programming model developed by NVIDIA. With CUDA, you can use Nvidia GPUs for general-purpose processing, not just graphics. It enables you to harness the power of GPU parallelism to accelerate stuff like scientific simulations and deep learning. CUDA has  “democratized” the graphics hardware. This allows AI researchers to write code for GPU’s using standard languages like C++ and Python. The speedup compared to CPU-only sequential computing implementations is gigantic.

The CUDA toolkit is what’s going to drive our local AI management, see also the architecture diagram I included previously.

By the way, here is an interesting read for you: “The Origins of GPU Computing“.

Install the CUDA toolkit

Similar to the NVIDIA driver and kernel module, we install the CUDA toolkit using the SlackBuilds.org scripts.
I needed to use cudatoolkit_13 which was at version 13.2.0 when I compiled my package. The compilation of an Ollama package in a next step kept failing because of CUDA issues until I upgraded the toolkit from the 10.x (default at SBo) to 13.x.

After installing the CUDA toolkit, logout and login again to make your shell can ‘source’ the installed profile script ‘/etc/profile.d/cuda-13.2.sh

Install Ollama

For this article, I tried building Ollama from source. That went well eventually, but the resulting binary would never contain a working set of GPU library stubs (CUDA). What happens if you use an Ollama binary without support for CUDA is that the AI models you use will be running on your CPU instead of your GPU, killing the real-time experience.
I will leave the build instructions for Ollama in this section, but what I actually did was downloading the official binary and installing that as ‘/usr/local/bin/ollama’. If any of you can explain to me what I potentially did wrong,  and show how to compile Ollama with CUDA support, let me know in the comments below!

Compile from source

Before we can compile Ollama, some packages need to be installed first:

  • google-go-lang
    This is part of Slackware -current but if you are still on Slackware 15.0 you can download this Go compiler from my own repository.
  • go-md2man
    Needed to generate man pages.You can download this package from my own repository.
  • jq
    A commandline JSON processor which is part of Slackware -current and which you can download from my own repository if you are running Slackware 15.0

Note that after installing google-go-lang you need to logout and login again to allow your shell to ‘source’ the profile script ‘/etc/profile.d/go.sh‘.

Then, compile Ollama,  using yet another script you can download from SlackBuilds.org.  Install the resulting package.

Or… download official binaries

In stead of compiling Ollama from source, you can also download and install the official binaries from Ollama’s server. Simply do this:

# wget https://ollama.com/download/ollama-linux-amd64.tar.zst
# tar -C /usr/local -xf ollama-linux-amd64.tar.zst
# /usr/local/bin/ollama --version

Since we have an NVIDIA card in our server, and the NVIDIA proprietary driver as well as the CUDA toolkit have already been installed, the Ollama binary auto-detects these capabilities and no extra steps are needed on Slackware as long as libcuda.so is in the dynamic linker path.

Create dedicated system account and directories

The ‘ollama’ account will be created as system user and group:

# groupadd -g 393 -r ollama
# useradd -r -u 393 -g ollama -d /var/lib/ollama -s /sbin/nologin -c "Ollama service account" ollama

The directory to store the AI models:

# mkdir -p /var/lib/ollama/models
# chown -R ollama:ollama /var/lib/ollama
# chmod 750 /var/lib/ollama

Pre-create the log file:

# touch /var/log/ollama.log
# chown ollama:ollama /var/log/ollama.log
Start Ollama

We’ll make sure that  Ollama starts when the server boots using a ‘rc’ script. It is also possible to run Ollama in a container, which may be a future extension to the article.

Create the following ‘rc’ script called ‘/etc/rc.d/rc.ollama‘ to start Ollama when your computer boots:

#!/bin/bash
# /etc/rc.d/rc.ollama - Ollama service for Slackware
# Created by Jerry B Nettrouer II  https://www.inpito.org/projects.php
# Load configuration (if file exists)
[ -f /etc/default/ollama ] && . /etc/default/ollama
# Load CUDA toolkit locations if those exist:
[ -f /etc/profile.d/cuda-13.2.sh ] && . /etc/profile.d/cuda-13.2.sh
# Set the Process ID file
PIDDIR="/run/ollama/"
PIDFILE="/run/ollama/ollama.pid"
# Set the log file
LOGFILE="/var/log/ollama/ollama.log"

case "$1" in
  start)
    if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
      echo "Ollama already running."
      exit 0
    fi
    echo "Starting Ollama... (models: $OLLAMA_MODELS, host: $OLLAMA_HOST)"
    # Create the run directory
    mkdir -p $PIDDIR
    chown -R ollama:ollama $PIDDIR
    # Use nohup + setsid for clean daemon behavior
    su -s /bin/sh -c "setsid nohup ollama serve >> $LOGFILE 2>&1 & echo \$! > $PIDFILE" ollama
    echo "Started with PID $(cat "$PIDFILE")"
    ;;
  stop)
    echo "Stopping Ollama..."
    if [ -f "$PIDFILE" ]; then
      kill $(cat "$PIDFILE") 2>/dev/null
      rm -f "$PIDFILE"
    else
      pkill -f "ollama serve" 2>/dev/null
    fi
    ;;
  restart)
    $0 stop
    sleep 1
    $0 start
    ;;
  status)
    if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
      echo "Ollama is running (PID $(cat "$PIDFILE"))."
    elif pgrep -f "ollama serve" >/dev/null; then
      echo "Ollama is running (but no PID file)."
    else
      echo "Ollama is not running."
    fi
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|status}"
    exit 1
    ;;
esac
exit 0
# ---

This ‘rc’ script relies on a configuration file ‘/etc/default/ollama‘ which needs the following content (you will probably change a few parameters):

# ---
OLLAMA_MODELS=${OLLAMA_MODELS:-"/var/lib/ollama/.ollama"}
OLLAMA_HOST=${OLLAMA_HOST:-"0.0.0.0:11434"}
OLLAMA_ORIGINS=*
# You can add more variables if needed, e.g.:
#OLLAMA_KEEP_ALIVE="-1" # Never unload automatically
#OLLAMA_DEBUG="1"
#OLLAMA_GPU_MEMORY_FRACTION="0.85" # Constrain VRAM usage:

# Need to export these, otherwise the ollama rc script will not pick them up:
export OLLAMA_MODELS OLLAMA_HOST OLLAMA_ORIGINS
# ---

Note in this configuration file that we instruct Ollama to listen on all interfaces (0.0.0.0), not just the loopback address (127.0.0.1). The safest way for a local Ollama which we are going to expose via an Apache Reverse Proxy would indeed be to only listen at the loopback address, but Ollama does not only want to talk to you (the user) but also it needs to be talked to! The web page via which you are going to access your local AI is provided by Open WebUI and that is going to be running inside a Docker container. The Open WebUI server inside Docker can not access the host’s loopback. That is why we tell Ollama to listen at all network interfaces.
And then we mitigate this risk by adding a firewall rule which blocks access to Ollama from anything else but the loopback address and our AI Docker network.

To bring it all together, invoke this ‘rc’ script in your ‘/etc/rc.d/rc.local’ by adding the following text block to it. There we ensure that the Ollama server port is firewalled from the outside world:

if [ -x /etc/rc.d/rc.ollama ]; then
  # Protect from outside abuse via firewall:
  # Allow established connections and loopback
  /usr/sbin/iptables -A INPUT -i lo -p tcp --dport 11434 -j ACCEPT
  # Allow Docker Ollama bridge network
  # (adjust the subnet to match 'docker network inspect localai.lan')
  /usr/sbin/iptables -A INPUT -s 172.24.1.0/24 -p tcp --dport 11434 -j ACCEPT
  # Drop everything else hitting this port
  /usr/sbin/iptables -A INPUT -p tcp --dport 11434 -j DROP
  # Start Ollama LLM offline server:
  echo "Starting Ollama LLM offline:  /etc/rc.d/rc.ollama start"
  /etc/rc.d/rc.ollama start
fi

Run the start script manually first to boot the OIlama server.

# /etc/rc.d/rc.ollama start

Note that Ollama does not offer any form of authentication mechanism. Any user or process that can access the TCP port can use it.

Test Ollama

Test from your non-root user account whether Ollama is ready for action:

$ ollama list

… or else:

$ curl http://127.0.0.1:11434/api/tags
LLM quickstart: pull and use Mistral

Let’s try to pull the ‘Mistral’ Large Language Model (this downloads ~4GB for mistral:7b):

$ ollama pull mistral

If you want to experience an interactive chat session:

$ ollama run mistral

You can also use a non-interactive single prompt which would be useful for scripting:\

$ ollama run mistral "Explain lithography in one paragraph"

… or use the REST API directly:

$ curl http://127.0.0.1:11434/api/generate -d '{"model":"mistral","prompt":"Hello, Mistral!","stream":false}' | python3 -m json.tool

Query Ollama about the AI models it has loaded. This also shows how much of the model runs in the GPU VRAM versus on the CPU:

$ ollama ps
NAME             ID            SIZE   PROCESSOR  CONTEXT  UNTIL
ministral-3:14b  4760c35aeb9d  10 GB  100% GPU   4096     Forever

Create docker network and local directories

# docker network create --driver=bridge --subnet=172.24.1.0/24 --gateway=172.24.1.1 localai.lan
# mkdir -p /usr/local/docker-localai/
# mkdir -p /opt/dockerfiles/localai/{data,open-terminal-data}/

Install Open WebUI

Open WebUI is the current best-maintained self-hosted frontend for Ollama.  This is its ‘docker-compose.yml‘ file which you should create in directory ‘/usr/local/docker-localai/‘:

# ---
name: open-webui
services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: unless-stopped
    networks:
      - localai.lan
    # host-gateway resolves to the host machine's IP on the Docker bridge,
    # allowing the container to reach host-resident services.
    extra_hosts:
      - "host.docker.internal:host-gateway"
    ports:
      # Bind ONLY to localhost. Apache will be the public-facing entry point.
      - "127.0.0.1:3456:8080"
    environment:
      # Point Open WebUI at host-resident Ollama via the bridge gateway (127.0.0.1 does NOT work here).
      - OLLAMA_BASE_URL=http://host.docker.internal:11434
      # Must match what Apache sends as the public URL. This is critical for
      # cookies, redirects, and CSRF protection once behind a reverse proxy.
      - WEBUI_URL=https://ai.darkstar.lan
      # Best practice: put this in a .env file next to docker-compose.yml
      - WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
      # Harden cookies when served over HTTPS via the proxy
      - WEBUI_SESSION_COOKIE_SECURE=true
      - WEBUI_SESSION_COOKIE_SAMESITE=lax
      # Explicitly enable WebSocket support
      - ENABLE_WEBSOCKET_SUPPORT=true
      # Socket.IO ping interval and timeout (milliseconds).
      # Ping every 20s; the client must respond within 30s.
      # This keeps the WebSocket alive through NAT devices with
      # short idle timers.
      - WEBSOCKET_PING_INTERVAL=20000
      - WEBSOCKET_PING_TIMEOUT=30000
    volumes:
      - /opt/dockerfiles/localai/data:/app/backend/data
    depends_on:
      - open-terminal
  open-terminal:
    # Use 'slim' (200 MB) instead of 'latest' (2 GB) unless you specifically
    # need Node.js, ffmpeg, or data science libraries available to the AI agent.
    image: ghcr.io/open-webui/open-terminal:${OPEN_TERMINAL_VARIANT}
    container_name: open-terminal
    restart: unless-stopped
    networks:
      - localai.lan
    # No 'ports:' section - intentionally not exposed to the host.
    # Open WebUI backend proxies to it via the Docker network.
    environment:
      - OPEN_TERMINAL_API_KEY=${OPEN_TERMINAL_API_KEY}
    volumes:
      # Persistent home directory for the terminal user.
      # Files the AI creates here survive container restarts.
      - /opt/dockerfiles/localai/open-terminal-data:/home/user
      # Only add this if you specifically want the AI to read host files
      #- /host/path/to/AI/data:/data:ro   # :ro = read-only

networks:
  localai.lan:
    external: true
    name: localai.lan
# ---

The accompanying ‘.env‘ file which should be created in the same location contains the following:

# ---
# Persistent login:
WEBUI_SECRET_KEY=eePAAjEgEnZdgAQcVKb/DA993rwU+xbBb1scG0Zz1sQ=

# Connecting Open Terminal to Open WebUI:
OPEN_TERMINAL_API_KEY=qIShpFT2IUZaglqLTX5UCw6oQSyuCuKgpgF/xViqUWA=
OPEN_TERMINAL_VARIANT=latest
# ---

Don’t forget to create and use the variable “WEBUI_SECRET_KEY”!
Without a persistent “WEBUI_SECRET_KEY”, you’ll be logged out every time the container is recreated.

Why does the “127.0.0.1” address not work from inside a container?
When Open WebUI runs in a Docker container on a bridge network (which is what any custom docker network uses), the address “127.0.0.1” inside that container refers to the container’s own loopback interface… not that of the host!
Setting OLLAMA_BASE_URL to “http://127.0.0.1:11434” would have the container talking to itself on a port where nothing is listening. You would get an immediate connection refused. The “extra_hosts” entry in the Compose file: “host.docker.internal:host-gateway” is a specific syntax meant to instruct Docker to inject a hosts-file entry into the container that maps the name “host.docker.internal” to the host’s IP address on the Docker bridge (typically something like 172.18.0.1, but you never need to hard-code that). This is Docker’s own supported mechanism for containers to reach host-resident services.
Even with “host.docker.internal” resolving correctly, there is still a firewall/bind problem. If Ollama’s OLLAMA_HOST is set to “127.0.0.1:11434”, the kernel will only accept connections arriving on the loopback interface. Traffic coming in from the Docker bridge (e.g., 172.18.0.x) arrives on a different interface and gets refused at the TCP socket level. Not by a firewall rule!

Note that I am already including the Docker configuration for Open Terminal, so that you have everything in one place from the start. I will explain about Open Terminal further down in another section of the article.

Start and configure Open WebUI

We perform the initial configuration of the Open WebUI container while still not opened up to the LAN. Just to be safe, since the very first user that is created has full admin rights. In the next step we will configure a reverse proxy to expose Open WebUI to the network.

# cd /usr/local/docker-localai
# docker compose up -d

Docker downloads (pulls) the image and then the container will start. Watch the logs during first start (DB migrations run on first boot):

# docker logs -f open-webui

Look for the line like “INFO Application startup complete” which indicates that the server is ready. Let’s login!

On the host, navigate to http://127.0.0.1:3456

The admin account

Register your first account. This user automatically becomes the admin account. Naturally you need to define a strong password…  Open WebUI’s admin user is able to control access to the AI models, user creation, and system-level settings.

  • Go to ‘Settings > Connections‘ and confirm that the Ollama URL is shown as connected (a green indicator).
  • Go to ‘Settings > Models‘, Your previously pulled models (e.g., “mistral:latest“) should appear.
  • Start a new chat, select “mistral“, and away you go!
Internet access

To give your local AI model internet access via Open WebUI, you need to enable the built-in Web Search feature in the Admin Panel.
Recent AI models are highly capable of “tool use,” and this setup allows the model to search the web, read the top results, and summarize them for you.

  • Enable Web Search in Admin Settings
    • Open the Open WebUI interface https://ai.darkstar.lan/ in your browser.
    • Click your Profile Icon (bottom-left) and select ‘Admin Panel‘.
    • Navigate to the ‘Settings‘ tab and click on ‘Web Search‘.
    • Toggle ‘Enable Web Search‘ to “ON”.
  • Choose and Configure a Search Engine
    You must select a provider to fetch the actual search results. Here are the most common options:

    • DuckDuckGo (Easiest): Works out of the box without an API key. Select “DDGS” as the search engine and “DuckDuckGo” as its backend from the dropdowns.
    • Tavily (Recommended for AI): Specifically built for LLMs to get clean, searchable data. You will need a free Tavily API Key. Paste it into the Tavily API Key field in ‘Settings‘.
    • Google PSE: Best for comprehensive results but requires creating a Google Programmable Search Engine to get a Search Engine ID and API Key.
    • SearXNG (Private/Local): If you want to stay 100% local, you can run a SearXNG instance in a separate Docker container and point Open WebUI to its local URL (e.g., http://localhost:8080).
  • Using Search in Chat
    Once configured, you can use the web search in two ways:

    • Manual Toggle: In a new chat, look for the “+” icon or the Web Search toggle (globe icon) near the message box to activate it for that session.
    • Keyword Trigger: You can often trigger a search by typing # or using a specific prefix if you have set up a “Search” tool/action in the Workspace settings.
    • To make sure the AI actually uses the retrieved data effectively:
      • Go to ‘Workspace > Models‘.
      • Click the ‘Edit‘ (pencil) icon for your AI model.
      • In the ‘Tools or Capabilities‘ section, ensure that “Web Search” is checked so the model knows it is allowed to use this external tool.

Apache reverse proxy configuration

Ensure that the following modules are loaded in httpd.conf or in a separate included configuration file below /etc/httpd/ :

# ---
LoadModule proxy_module lib64/httpd/modules/mod_proxy.so
LoadModule proxy_http_module lib64/httpd/modules/mod_proxy_http.so
LoadModule proxy_wstunnel_module lib64/httpd/modules/mod_proxy_wstunnel.so
LoadModule ssl_module lib64/httpd/modules/mod_ssl.so
LoadModule rewrite_module lib64/httpd/modules/mod_rewrite.so
LoadModule headers_module lib64/httpd/modules/mod_headers.so
# ---

A note about ‘mod_proxy_wstunnel’: people often forget to account for WebSockets. Open WebUI streams LLM responses over a WebSocket connection. Without this module, you get a UI that connects, shows the model list, but then silently fails to stream any generated text.

Therefore, these are the essential bits you need to add to your Apache HTTPD server configuration:

# --- Proxy core ---
ProxyPreserveHost On
ProxyRequests Off

# Tell Open WebUI the real client IP (used in logs and rate limiting)
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Host "ai.darkstar.lan"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"

# Increase timeouts for long LLM inference (large models can think slowly)
ProxyTimeout 300
Timeout 300

# Disable response buffering - critical for streaming LLM output.
# Without this, Apache may buffer the entire response before forwarding.
SetEnv proxy-sendchunked 1
SetEnv proxy-initial-not-buffered 1

# WebSocket upgrade support (essential for LLM streaming)
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:3456/$1" [P,L]

# Open WebUI reverse proxy, connects to an Ollama backend:
ProxyPass / http://127.0.0.1:3456/ keepalive=On
ProxyPassReverse / http://127.0.0.1:3456/

# Optionally (you can remove this if you don't care)
# Ensure that only the people you know can access the Web Interface:
<Location />
    Require all granted
    <RequireAny>
        Require host yourowndomain.com
        Require ip 192.168
        Require ip 10.10
    </RequireAny>
</Location>

After adding this configuration block to the “VirtualHost” definition of ai.darkstar.lan, run a configuration check and then restart the Apache webserver:

# apachectl configtest
# apachectl -k graceful

Check the result

Your Open WebUI page should now be accessible at https://ai.darkstar.lan/


Add Open Terminal

Open Terminal is a capability we can add to the Docker stack that gives the AI model a real computer to work on.
It connects a containerized computing environment to Open WebUI. The AI model you are using can use that sandboxed shell environment to write code, execute it, read the output, fix errors, and iterate, all without leaving the chat. It handles files, installs packages, runs servers, and returns results directly to you. Because we will run it in a Docker container it offers complete isolation from the host processes. We will give it persistent storage so that you can grab the resulting artifacts straight from a local directory.

This setup mirrors a capability that the Big Tech companies also provide with their commercial LLM’s: formulate an idea and let your AI generate working software. Ask it a question and get a functional script. Describe a website and watch it being rendered live.
When you upload a spreadsheet, CSV file or a database, you can instruct your AI to read the data, run analysis scripts and generate charts or reports.
Until here the PR text taken from the web site:-)

An important architectural consideration: Open WebUI proxies AI requests to Open Terminal. Open Terminal will never connect the other way round.  This means that the Open Terminal container never needs to be reachable from the internet or even from the browser. It only needs to be reachable from within the Docker network. This keeps it nicely isolated.

To get Open Terminal up and running, nothing is required. We already added all the code to the ‘docker-compose.yml’ and ‘.env’ files. When the stack is running you can validate that Open Terminal is ready by examining the logs:

# docker logs -f open-terminal

Verify that Open WebUI can talk to Open Terminal via a command you execute inside the Open WebUI container (the docker commmand uses the ‘open-webui’ service name as defined in the ‘docker-compose.yml’ file):

# docker exec open-webui curl -s \
    -H "Authorization: Bearer $(grep OPEN_TERMINAL_API_KEY /usr/local/docker-localai/.env | cut -d= -f2)" \
    http://open-terminal:8000/health

A healthy response looks like:
json{"status": "ok"}

If that returns successfully, the two containers can see each other on the network and the API key is accepted.

Enable Open Terminal in Open WebUI

This needs to be done through the Open WebUI admin interface. It can not be done via configuration files.

  • Navigate to https://ai.darkstar.lan/ and log in as an admin user.
  • Go to ‘Admin Settings > Integrations > Open Terminal‘ and fill in the fields:
    • URL: “http://open-terminal:8000”
    • API Key: qIShpFT2IUZaglqLTX5UCw6oQSyuCuKgpgF/xViqUWA= (which is the value of OPEN_TERMINAL_API_KEY in your .env file)
    • Click ‘Save‘, then toggle the connection ‘Enabled‘.
      Open WebUI will immediately test the connection, and a green indicator confirms success.

The URL http://open-terminal:8000 works because Docker’s internal DNS resolves the service name ‘open-terminal’ to the container’s IP on localai.lan.
This is why the container needs no exposed port. It is only ever spoken to by Open WebUI’s backend, never by your browser directly.

You have a choice to make regarding the Docker Image variant of Open Terminal. Using the ‘slim‘ tag in the Compose file above would be a deliberate choice. I prefer ‘latest‘ instead. Here is what each variant gives an AI agent to work with:

  • alpine:
    ~100 MB image. This gives: a basic shell, curl, jq, git. It’s minimal but functional.
  • slim:
    ~200 MB image. Content is identical to the ‘alpine’ image but this one is Debian-based. This guarantees better package compatibility.
  • latest:
    ~2 GB image. You will get a full Python environment, Node.js, the Docker CLI, ffmpeg and data science libraries.

For a personal server, ‘slim‘ may the pragmatic choice. The AI can run shell commands, use git, curl APIs, and manage files, which covers the vast majority of useful agent tasks without pulling a 2 GB image. But I may also need the AI to run a Python data processing task or Node.js scripts. Therefore I configured ‘latest‘ myself.


Ollama integration in Nextcloud

Official documentation for the integration of local AI into your Nextcloud server can be found here: https://docs.nextcloud.com/server/stable/admin_manual/ai/overview.html
In short, these are the steps you need to take to integrate your Ollama AI server into Nextcloud.

  • Install the Nextcloud Assistant app using the administrator account of your Nextcloud instance
  • Similarly, install OpenAI integration app
  • Click on the administrator avatar in the top right, and go to ‘Administration Settings > Administration > Artificial Intelligence
    • In ‘OpenAI and LocalAI configuration‘, set ‘http://127.0.0.1:11434/v1′ as the OpenAI-compatible ‘Service URL‘.
  • Add one line to Nextcloud’s ‘config/config.php‘ file (manually; there is no GUI to do this):
    'allow_local_remote_servers' => true,

If you want your AI to feel responsive in Nextcloud it is also imperative to implement a number (minimum 4) of local ‘AI workers’ that pick up AI requests from the queue and process them immediately in the background. Otherwise that request processing is only happening every 5 minutes via Nextcloud’s internal cron. My advice is running them inside screen (or tmux) with this command added to ‘/etc/rc.d/rc.local‘:

/usr/bin/screen -S NEXTCLOUD -t AI_1 \
  -Adm /usr/local/sbin/nextcloud_occ_backgroundworker.sh 1 && \
  sleep 1 && \
  /usr/bin/screen -S NEXTCLOUD -X screen -t AI_2 \
  -Adm /usr/local/sbin/nextcloud_occ_backgroundworker.sh 2 && \
  /usr/bin/screen -S NEXTCLOUD -X screen -t AI_3 \
  -Adm /usr/local/sbin/nextcloud_occ_backgroundworker.sh 3 && \
  /usr/bin/screen -S NEXTCLOUD -X screen -t AI_4 \
  -Adm /usr/local/sbin/nextcloud_occ_backgroundworker.sh 4

Where the executable shell script ‘/usr/local/sbin/nextcloud_occ_backgroundworker.sh‘ is something you need to create yourself with the following content:

#!/bin/bash
if [ -n "$1" ]; then
  echo "Starting Nextcloud AI Worker $1"
else
  echo "Starting Nextcloud AI Worker"
fi
cd /var/www/htdocs/nextcloud/
set -e
while true; do
  sudo -u apache php -d memory_limit=512M ./occ background-job:worker \
    -v -t 60 "OC\TaskProcessing\SynchronousBackgroundJob"
done
# ---

If you need to access these AI workers at any time, you can do so from root’s commandline via:

# screen -x NEXTCLOUD

… and cycle through the four worker screens using [Ctrl]-a-n

Tasks are run as part of the background job system in Nextcloud, which only runs jobs every 5 minutes by default.
To pick up scheduled jobs faster you can set up background job workers inside the Nextcloud main server/container that process (AI and other)
tasks as soon as they are scheduled.
If the PHP code or the Nextcloud settings values are changed while a worker is running, those changes won’t be effective inside the runner.
For that reason, the worker needs to be restarted regularly. It is done with a timeout of N seconds which means any changes to the
settings or the code will be picked up after N seconds (worst case scenario). This timeout does not, in any way, affect the processing or the timeout of AI tasks.

The result of this configuration is the appearance of a new “AI” button in the Nextcloud task bar which you can click to access the Assistant, giving you access to chat, translation, image and audio analysis, and more:


Single Sign-ON (SSO)

Open WebUI supports OpenID Connect (OIDC) out of the box. See https://docs.openwebui.com/reference/env-configuration/#openid-oidc for the variables that enable Single Sign-On and  https://docs.openwebui.com/troubleshooting/sso/ for additional troubleshooting information. You should be able to connect Open WebUI to your Cloudserver’s Keykloak Identity Provider without issues.
Unfortunately my local server that I equipped with a NVIDIA GPU is not running Keycloak or any other OIDC provider, and I could not validate this SSO capability myself.
Let me know if you were able to add SSO to your own setup!


Attribution

Many thanks to INPITO (the Indiana Non-Profit Information Technology Organization) who use Slackware as their OS and wrote the article that formed the inspiration for my own journey into local AI: https://www.inpito.org/ollama.php. I copied their Slackware boot script for Ollama.


Final thoughts

I hope this article will remove some of the resistance that many people still show towards the use of AI chatbots. The fact that you can run a Large Language Model on your gaming rig, use it to experiment with new technologies and be certain that none of that data will ever be shared externally, is great!

Leave your comments, suggestions and opinions below.
Thanks for reading. Eric

New KDE Frameworks and Plasma packages

KDE just announced the KDE Frameworks 6.25.0, and earlier this week they came with Plasma 6.6.4.

I built the packages for those new releases – targeting Slackware -current of course. Along with these updates I also refreshed KleverNotes, Krita, Kstars and Okteta to their latest versions.

You can find the packages in my ‘ktown‘ repository: https://slackware.nl/alien-kde/current/testing/ with the sources in https://slackware.nl/alien-kde/source/testing/ and the changes tracked in git: https://git.slackware.nl/ktown/log/.

If you are using the slackpkg+ extension to slackpkg and have already switched from Slackware’s Plasma5 to my Plasma6 then the upgrade to the latest set of Slackware-current packages along with the new ‘ktown‘ Plasma 6 is trivial (assuming you tagged my repository with the string “ktown” in slackpkgplus.conf):

# slackpkg update
# slackpkg install-new
# slackpkg install ktown
# slackpkg upgrade-all

Are you still running Slackware’s own KDE Plasma5 on the other hand, it must be removed first. No clean upgrade path can be provided! Do as follows:

Use the slackpkg template provided in the package directory (https://slackware.nl/alien-kde/current/testing/plasma6_remove_plasma5.template is the online version).
The template is called ‘plasma6_remove_plasma5.template‘. This file contains the name of all original Slackware packages that need to be removed.
You can do this either using slackpkg (no slackpkg+ needed):

# cp plasma6_remove_plasma5.template /etc/slackpkg/templates
# slackpkg update
# slackpkg remove-template plasma6_remove_plasma5

… or else using pkgtools:

# cat plasma6_remove_plasma5.template | while read PKGNAME; do removepkg $PKGNAME ; done

And then proceed with the upgrade steps I shared higher-up.

The next major update is going to be KDE Gear 26.04 which will be the first release of KDE Gear (i.e. Applications & PIM) without dependency on Qt5 or the KDE Frameworks 5.
This move to Gear 26.04 requires some additional changes to the packages in Slackware -current itself, for which I asked Pat to assist. Let’s see what happens in the weeks to come.

Have fun and enjoy the weekend. Eric

Slackware Cloud Server Series, Episode 11: Jukebox Audio Streaming

I am an avid music lover. My tastes are eclectic; I enjoy electronic, industrial, punk, new wave, reggae and dub but also baroque and classical music. I used to tape my own music cassettes when I was young, sharing my mixes with friends. I have hundreds of vinyl albums and at many more compact discs. But technology kept evolving and I switched to MP3 files that I could store on my computer and play using VideoLAN VLC for instance. But I also want to be able to just listen to my music in the living room without operating a laptop and for that, I setup a streaming server that acts as a jukebox, continuously picking random songs from my collection and playing them from a queue that never empties. In the living room I have a Denon AVR-X2300W which can pick up the network stream.
I have been running this audio streaming server for decades. First using OTTO, then Calliope and then coming back to OTTO after it had re-invented itself. But Calliope and OTTO are no longer maintained and  quite tricky to setup in the first place. I am not looking forward to migrate this unsupported setup to Slackware 15.1 when that gets released and I move my server to it.

I went on a search for a modern, maintained and open source alternative for my OTTO server.
I have actually setup Mopidy with Pibox extension to get the jukebox functionality. Recompiling Slackware’s gst-plugins-good package against libshout enables the libgstshout2.so library which gives us ‘shout2send‘ which then streams audio from Mopidy to my Icecast server. Setting it all up was not trivial and I did not like how the Pibox extension handled the queue autofill. I went on with my search for a good OTTO alternative and I hope I found it.

In this episode of Slackware Cloud Server I will show you how to stream your personal MP3 collection via Icecast using the open source platform AzuraCast. A worthy addition to your Slackware Cloud Server as as service to yourself, friends, family or even your local community.


Check out the list below which shows past, present and future episodes in my Slackware Cloud Server series. If the article has already been written you’ll be able to click on the subject.
The first episode also contains an introduction with some more detail about what you can expect.


Introduction


What is AzuraCast?

AzuraCast is a self-hosted, all-in-one web radio management suite consisting of multiple independent but co-operating components:

  • Liquidsoap
    This is the automation engine that fills your play queue, handles scheduling and song rotation, and feeds the stream source, re-encoding if needed.
  • Icecast-KH
    A maintained fork of Icecast that handles the actual audio streaming to listeners.
  • A PHP/Vue web application
    The management interface where you control everything: upload music, browse your library, configure playlists, handle listener requests, check analytics.
  • MariaDB server
    Stores the song metadata, play history and playlists.
  • Redis
    Runtime memory store for the session cache and queue state.

We will be running the whole stack in Docker, making it self-contained regardless of your Slackware version. The local directory tree containing your music library will be bind-mounted inside the Docker container.
We will setup the Apache HTTP server as a reverse proxy so that we can access the Management UI securely via HTTPS. We will also proxy the Icecast stream on a standard port so that listeners would not have to connect to your Icecast mount point via e.g. ‘http://yourserver:8000/radio.mp3’ but rather via a regular URL like ‘https://yourserver/yourchannel’.
Any player that speaks Icecast like VLC, mpv, foobar2000, mpc, also every browser and surely many more will be able to playback your music.

What makes AzuraCast the right solution?

When searching for a replacement I had several requirements in mind that a new program should meet. AzuraCast ticks more boxes than any of the other solutions I encountered and/or tested:

  • Should be able to handle tens of thousands of songs effortlessly
    Azuracast indexes my library in a MariaDB database. 50 000+ tracks is a documented use-case.
    I can tell you from experience that it takes a day or two to get 50K songs indexed however.
  • Continuously fills the play queue (it must never be empty)
    The Liquidsoap AutoDJ has configurable rotation and fills the queue automatically.
  • Manual song requests should be added to the head of queue
    AzuraCast has a ‘Listener Requests’ feature. It’s available via its web UI but also as a REST API.
    It should be possible to configure the AutoDJ in such a way that user requests are immediately placed at the head of the queue, unfortunately I have not yet found out how. Because the AutoDJ  pushes the tracks its queue to the Icecast server immediately, any user requests will be scheduled after that queue, not before it. I need to keep the queue length (which is configurable) to a minimum value of 2 to make the experience acceptable.
  • Web-based management interface
    AzuraCast comes with a full-featured, mobile-friendly web UI with lots of analytics, logging and debugging tools.
  • Auto-detect or manually re-scan for new music files
    It can do both: it runs an internal background task (with configurable interval) to scan for new music regularly, but there’s also a command-line option to re-scan your entire music collection.
  • Stream output via Icecast protocol
    Icecast-KH is the native output; every mount point is a standard Icecast stream.

Architecture overview


Preamble

This section describes the technical details of our setup, as well as the things which you should have prepared before trying to implement the instructions in this article.

Web Hosts

For the sake of this instruction, I will use the hostname “https://radio.darkstar.lan” as your landing page for AzuraCast.
The URL for the Icecast stream will be “https://radio.darkstar.lan/lowlands“.

Setting up your domain (which will hopefully be something else than “darkstar.lan”…) with new hostnames and then setting up web servers for the hostnames in that domain is an exercise left to the reader. Before continuing, please ensure that your equivalent for the following host has a web server running. It doesn’t have to serve any content yet but we will add some blocks of configuration to the VirtualHost definition during the steps outlined in the remainder of this article:

  • radio.darkstar.lan

Using a  Let’s Encrypt SSL certificate to provide encrypted connections (HTTPS) to your webserver is documented in an earlier blog article.

Note that I am talking about webserver “hosts” but in fact, all of these are just virtual webservers running on the same machine, at the same IP address, served by the same Apache httpd program, but with different DNS entries. There is no need at all for multiple computers when setting up your Slackware Cloud server.

Docker network

  • We assign a Docker network segment to our AzuraCast container: 172.24.0.0/24 and call it “azuracast.lan

File Locations

  • The Docker configuration goes into: /usr/local/docker-azuracast/
  • The data generated by the AzuraCast server goes into: /opt/dockerfiles/azuracast/

Port numbers

Since everything runs in a Docker container, all services listen at the localhost address 127.0.0.1.

  • AzuraCast Web UI in Docker: listens at TCP port 81
  • Icecast mount points: we will open two (for two audio streams) that listen at TCP ports 8001 and 8002

Station name

  • Our Icecast Radio Station will be called: Alien Pastures Radio
  • Inside Azuracast (primarily used to create the directory to mount the media) this name is trivially translated to: alien_pastures_radio

Installation

AzuraCast’s recommended installation method uses a helper script that downloads the Docker Compose configuration, fires the container, and allows for post-installation maintenance and management. We are not going to use that.
Still, in order for you to be able to switch effortlessly to the AzureCast officially suggested Docker setup, I will follow their recommendations to add all of the customization into a separate ‘override‘ file for Docker Compose.

Docker network

Create the network using the following command:

docker network create \
  --driver=bridge \
  --subnet=172.24.0.0/24 --gateway=172.24.0.1 \
  azuracast.lan

Docker’s gateway address in any network segment will always have the “1” number at the end.
Select a yet unused network range for this subnet. You can find out about the subnets which are already defined for Docker by running this command:

# ip route |grep -E '(docker|br-)'

The ‘azuracast.lan‘ network you created will be represented in the AzuraCastdocker-compose.yml file with the following code block:

networks:
  azuracast.lan:
    external: true
Directories

Create a directory structure for AzuraCast as a Docker container. We’ll change ownership of two of those directories: backups and storage. The UID/GID numbers I use (shown in bold green) must correspond to the values you define for AZURACAST_PUID and AZURACAST_PGID in the ‘.env‘ file you will create in the next step. If you omit that ‘chown‘ step, AzuraCast will not be able to save Station information nor will it be able to create backups of its SQL server.

# mkdir -p /opt/dockerfiles/azuracast/{backups,db_data,stations,storage}
# chown 1000:1000 /opt/dockerfiles/azuracast/{backups,stations}
# mkdir -p /usr/local/docker-azuracast
# cd /usr/local/docker-azuracast
Configuration files

Download an example Docker environment file and store it under the name ‘.env‘ from https://raw.githubusercontent.com/AzuraCast/AzuraCast/stable/sample.env

Note that I download all files from the ‘stable’ branch of the AzuraCast git repository. You could also try the ‘main’ branch if you like to live on the edge.

The container ships its own internal Nginx proxy that also takes care of the SSL certificates, but I want to use my own host server’s Apache HTTP daemon to take care of the reverse-proxying. All you need to do to disable Nginx is to change the “AZURACAST_HTTPS_PORT” value from the default “433” to something else… and of course because port “443” is already in use on our server.
Likewise, we need to change “AZURACAST_HTTP_PORT” from the default value of “80” because that’s where our own Apache server is listening next to port 443.
After making the necessary changes we end up with an ‘.env‘ file containing (use your own values for the green example values if you want that):

# Make it easier to manage the project in Docker Compose:
COMPOSE_PROJECT_NAME=azuracast
# Define network ports:
AZURACAST_HTTP_PORT=81
AZURACAST_HTTPS_PORT=8412
AZURACAST_SFTP_PORT=2022
# We stick to the 'stable' channel instead of 'latest'
AZURACAST_VERSION="stable"
# If you start docker as your own user instead of root, change these to your own UID/GID
AZURACAST_PUID=1000
AZURACAST_PGID=1000

The ‘.env‘ file above is a configuration file which is read and used by the Docker daemon to setup the container. We are now going to create a second configuration file called ‘azuracast.env‘, containing the data that AzuraCast itself needs in order to function. Get the example file from https://raw.githubusercontent.com/AzuraCast/AzuraCast/stable/azuracast.sample.env and tailor it to your needs.
In the end ‘azuracast.env‘ should look like this (lots of comments and default values removed):

APPLICATION_ENV=production
COMPOSER_PLUGIN_MODE=false
AUTO_ASSIGN_PORT_MIN=8000
AUTO_ASSIGN_PORT_MAX=8001
SHOW_DETAILED_ERRORS=false
MYSQL_PASSWORD=azur4c457
MYSQL_RANDOM_ROOT_PASSWORD=yes

The two values in bold green for the lower and upper range of the Icecast ports correspond with the values I defined earlier in the ‘Preamble‘ section. The range of two ports means that this setup supports two independent Icecast streams.

The docker-compose.yml file

Get the ‘docker-compose.yml‘ file from https://raw.githubusercontent.com/AzuraCast/AzuraCast/stable/docker-compose.sample.yml
After removing the sections I don’t need (they enable the official “docker.sh” script to perform maintenance and upgrades) the file it will look like this:

# If you need to customize this file, you can create a new file named:
# docker-compose.override.yml
# with any changes you need to make.
#
name: azuracast

services:
  web:
    container_name: azuracast
    image: "ghcr.io/azuracast/azuracast:${AZURACAST_VERSION:-latest}"
    # Want to customize the HTTP/S ports? Follow the instructions here:
    # https://www.azuracast.com/docs/administration/docker/#using-non-standard-ports
    ports:
      - '${AZURACAST_HTTP_PORT:-80}:${AZURACAST_HTTP_PORT:-80}'
      - '${AZURACAST_HTTPS_PORT:-443}:${AZURACAST_HTTPS_PORT:-443}'
      - '${AZURACAST_SFTP_PORT:-2022}:${AZURACAST_SFTP_PORT:-2022}'
      - '8000-8001:8000-8001'
    env_file:
      - azuracast.env
      - .env
    volumes:
      - station_data:/var/azuracast/stations
      - backups:/var/azuracast/backups
      - db_data:/var/lib/mysql
      - www_uploads:/var/azuracast/storage/uploads
      - shoutcast2_install:/var/azuracast/storage/shoutcast2
      - stereo_tool_install:/var/azuracast/storage/stereo_tool
      - rsas_install:/var/azuracast/storage/rsas
      - geolite_install:/var/azuracast/storage/geoip
      - sftpgo_data:/var/azuracast/storage/sftpgo
      - acme:/var/azuracast/storage/acme
    networks:
      - azuracast.lan
    restart: unless-stopped
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
    logging:
      options:
        max-size: "1m"
        max-file: "5"

volumes:
  db_data: { }
  acme: { }
  shoutcast2_install: { }
  stereo_tool_install: { }
  rsas_install: { }
  geolite_install: { }
  sftpgo_data: { }
  station_data: { }
  www_uploads: { }
  backups: { }

networks:
  azuracast.lan:
    external: true

This docker-compose.yml file defines a number of Docker Volumes.  Don’t mind those, AzuraCast needs them but we don’t. We will mount a local directory containing our music library into AzuraCast container later and  make sure that the Station data is all written to a host directory as well.

Note:
If you want, you can use the downloaded original of ‘docker-compose.yml’ instead of my truncated version above. There’s almost no difference in execution or functionality (except for the custom network and the TCP port range it opens for the hundreds of potential streaming channels), but the original file is much too large to copy into this article..

You need to be aware that the downloaded official Docker Compose configuration file opens a bunch of TCP ports in the range of 8000 to 8500. That’s one TCP port per music stream you want to create, so the default configuration allows for a total of 500 different streams or ‘stations‘. I only run a few streams myself, and I used that in the above modification which opens only 2 ports.You may want to increase the number of possible streams on one AzuraCast instance. You can do that by editing the ‘docker-compose.yml‘ file, but since we’re going to use it anyway further down, I want to draw your attention to the option of creating a new file named ‘docker-compose.override.yml‘ in the same directory next to your docker-compose.yml and .env files.

I will show you how you can increase the Icecast listen ports from 2 to a total of 100 ports aka audio streams. You can modify the port range in this file to meet your needs, such as expanding the range to port 8500 instead of 8099.
Let’s also add an override  for the Station data storage. We want to use a local directory instead of a number of Docker volumes. To the ‘docker-compose.override.yml‘ file, add a ‘web‘ service just like in the actual Docker Compose file above, and then add a ‘ports‘ and a  ‘volumes‘ section so that the file looks like this:

services:
  web:
    ## OPTIONALLY: Add more ports, each port supports one radio station:
    #ports:
    #  - "8002-8099:8002-8099"
    # Store all Station data on the host:
    volumes:
      - /opt/dockerfiles/azuracast/stations:/var/azuracast/stations
      - /opt/dockeriles/azuracast/backups:/var/azuracast/backups
      - /opt/dockerfiles/azuracast/db_data:/var/lib/mysql
      - /opt/dockerfiles/azuracast/storage:/var/azuracast/storage

You will probably have noticed that the host directories (the paths to the left of the ‘:’ colon) are the same directories that we manually created in an earlier step.

Ready for lift-off!

We have not yet added any audio library to the Docker configuration. That is because we don’t have all the required information yet, and the missing piece needs to be arranged from within AzuraCast. So let’s start it!
When you start the container for the first time, it will take a few minutes because Docker will be downloading several hundred megabytes of container layers. Subsequent updates will be much faster:

# cd /usr/local/docker-azuracast
# docker-compose up -d
# docker-compose logs -f

AzuraCast is implemented as a single Docker image which contains all the functionality (streaming server, web-UI, database etc). Historical releases used separate Docker containers for the various components of the streaming platform. This single container implementation means for instance that the internal MariaDB database is not exposed to the host at all, therefore the password for the “azuracast” database user does not need to change from the default in the configuration file.
Still, AzuraCast automatically generates a random password for the MariaDB ‘root’ user upon the first database spin-up. This password will only be visible in the container’s logs upon that first startup so be sure to look there and write it down (look for the string “GENERATED ROOT PASSWORD“). Alternatively you can set “MYSQL_RANDOM_ROOT_PASSWORD=no” in the file ‘azuracast.env‘ and then add an extra line defining “MYSQL_ROOT_PASSWORD=your_secret_dbroot_password“.

Note:
Possibly the MYSQL_ROOT_PASSWORD variable needs to be called MARIADB_ROOT_PASSWORD

Initial web user-interface setup

We need to take some necessary steps to ensure that we can complete our Docker configuration by adding a media library.

Upon the very first startup, AzuraCast will present you its Management Interface, where we will set a Station Name for the streaming server. The Station Name is what AzuraCast uses as its internal directory. For example, if the station is “Alien Pastures Radio“, the directory used inside the container will typically be “alien_pastures_radio“.

Open a browser on your host computer (the Cloud Server) and navigate to http://localhost:81/. Since we have not yet configured a reverse proxy, there’s no other way than to perform these initialization steps on the host.
You’ll be presented with the AzuraCast setup wizard that allows to:

  1. Enter your email address to create your administrator account
    Choose a strong password; this is the master key to your station.
  2. Create your first Station
    Give it a memorable name (for instance “Alien Pastures Radio“), choose your streaming format (MP3 at 192 kbit/s is a reasonable starting point), and note the station’s “short name” that AzuraCast generates (which would be “alien_pastures_radio” in this example). This is the string you need for the ‘docker-compose.override.yml‘ later on.
  3. Set your time zone
    This is relevant for scheduled playlists and analytics.

After completing the steps in the wizard you end up on the main station management page (the screenshot was taken after I mounted my local music library, added that to a new playlist and connected the playlist to the Station).

Mount your existing music library

This is the most important configuration step. AzuraCast lives inside Docker, but your big MP3 collection lives on the host. You need to tell Docker to make that directory visible inside the container as a “bind mount”.

At this point we will use the station’s internal directory name which you wrote down when  creating your station through the web UI in the previous section. Using the station name, create an override file name ‘docker-compose.override.yml‘ (or rather, merge it into the file which you created in an earlier step):

services:
  web:
    volumes:
      - /your/actual/path/to/mp3s:/var/azuracast/stations/alien_pastures_radio/media/remote/mp3:ro

Replace `/your/actual/path/to/mp3s` with the real path on your host (e.g. `/data/music`) and `alien_pastures_radio` with your actual station directory name. You’ll notice the “:ro” at the end of the internal directory. This means, that your media library is going to be mounted read-only.

Apply the change:

# cd /usr/local/docker-azuracast ; docker compose down ; docker compose up -d

Note #1:
If your music is spread across multiple directories, you can add multiple volume entries, each mounted under a different subdirectory of the station’s media path. AzuraCast will index all of them.

Note #2:
If you have symlinks inside of your media directory, AzuraCast will choke on them because AzuraCast’s filesystem abstraction library (Flysystem) does not support them. What you can do instead is remove the symlink and create a bind-mount in its place

Note #3:
If you do not want AzuraCast to edit your media files, the recommended way is to mount your files one directory deeper than the media directory and bind-mount that instead.
As in the example I show above, you could mount the container’s internal directory ‘/var/azuracast/stations/alien_pastures_radio/media/remote/mp3′ as a read-only volume. That way AzuraCast can still use the media storage location to store cached metadata about the files, but your host filesystem can remain a read-only mount.
As for whether AzureCast will actually write to media files: only when a user edits the metadata via the web UI. Those changes are written back to the file to ensure they persist, since most users expect that to be the case when editing tracks in the media editor. If you mount the filesystem as read-only though, it’ll just quietly fail that process but will still save metadata changes to its database.

Configure the AutoDJ (auto-queue)

The AutoDJ is Liquidsoap, and its most important configuration is the playlist.
A playlist in AzuraCast is not a fixed list of songs — it is a source from which Liquidsoap draws tracks to fill the queue. The simplest configuration is a single playlist pointed at your entire library, set to shuffle ad infinitum.

Step 1: create a playlist for your Station

Navigate to ‘System Administration > Stations > [Your Station] > Manage > Playlists > Add Playlist‘:

  • Name: “My Library” (or whatever name you prefer)
  • Type: ‘Standard Playlist
  • Source: ‘Song-based
  • Playlist type: ‘General rotation
  • Song Playback Order: ‘Random
  • Weight: ‘1
  • Click ‘Save Changes

The “Song-based” source is key. It automatically includes every song in your media directory tree, with no manual maintenance required. You can keep adding music to your directory on the host, and once AzuraCast’s internal media scanner task executes, the new tracks become available to the AutoDJ.
We do of course still need to add media to this empty playlist. That’s what the next step will take care of.

Step 2: Connect your media to the Playlist you just created

Go to ‘System Administration > Stations > [Your Station] > Manage > Media > Music Files

  • Select the media directory/directories which you bind-mounted into your Docker container.
  • Click on ‘Playlists‘ and select ‘My Library‘ (or whatever name you gave it)
  • Click ‘Save‘.
Step 3: Enable the AutoDJ

Go to  ‘Stations > [Your Station] > Edit > AutoDJ‘:

  • AutoDJ Service: ‘Use LiquidSoap on this server’ is checked
  • Crossfade Method: ‘Smart Mode
  • Click ‘Save Changes’

Within a few seconds Liquidsoap will start filling the queue and the stream will begin playing.
You can come back here later and experiment with the Audio Processing section to improve the listener’s experience.

Enable listener requests

The listener request feature is how you can manually perform queue management in those cases that you want to override the AutoDJ.
When a request is submitted, AzuraCast inserts that track as the next-to-play item,  ideally at the head of the randomized play queue which is maintained by the AutoDJ (but how to place the request at the actual head  is the final puzzle piece I cannot yet figure out).

To enable it, go to ‘Stations > [Your Station] > Edit

  • Under the ‘Profile‘ section,  select ‘Enable Public Pages
  • Under the ‘Song Requests‘ section, check ‘Allow Song Requests
  • Optionally set ‘Minimum Time Between Requests‘ to prevent the queue from being flooded
  • Click ‘Save Changes

Requests can be submitted in two ways:

  1. Via the public web page:
    AzuraCast generates a public-facing player page at ‘http://radio.darkstar.lan/public/alien_pastures_radio‘. This includes a search box that lets anyone (or just you) find a song and click “Request“.
  2. Via the ‘REST API’ (useful for automation or in case you want to write your own front-end):
    Use this curl commandline to search for a song (replace STATION_ID with your Station’s numerical ID. Your first Station has an ID of “1“). You can also find your station’s numeric ID in the URL when you’re on the station’s dashboard page.
    Note that this will return a long list of all your audio files in JSON format!
$ curl -s "http://radio.darkstar.lan/api/station/STATION_ID/requests" | python3 -m json.tool

Submit a specific request (replace SONG_ID with the numeric ID from the search results):

$ curl -X POST "http://radio.darkstar.lan/api/station/1/request/SONG_ID"

The full API documentation is available at ‘http://radio.darkstar.lan/api‘.

The Icecast output URL

AzuraCast automatically creates an Icecast mount point when you create a station. By default it will be accessible at: http://127.0.0.1:8001/radio.mp3

To change the default mount point name “radio.mp3” into something else, go to ‘System Administration > Stations > [Your Station] > Manage > Broadcasting (in the left sidebar) > Mount Points‘ and change the name there. As an example, we change it to “lowlands“. The “.mp3” extension is not needed at all.
To verify, go to ‘System Administration > Stations > [Your Station] > Manage > Overview (in the left sidebar)‘. In the “Streams‘ section of the overview you’ll see the mount points that Liquidsoap is publishing to Icecast, along with the listener count and current playing track.

Put it to the test

To listen to your new streaming server from the command line, use any program that supports the Icecast protocol: mpv, mplayer, vlc, mpc (if you want to feed the Icecast stream back into an MPD instance) etc:

$ mpv http://127.0.0.1:8001/lowlands

It works! Time to make this stream available outside of your host server and let family and friends enjoy your shiny new music station.

Apache reverse proxy (https)

Especially if your server is headless, you definitely want to manage AzuraCast over HTTPS using a normal URL instead of the localhost address. You may also want to expose the audio stream on port 443 instead of 8001 so that it will pass any company firewall with ease. To achieve this we turn again to our trustworthy Apache HTTP server and setup a reverse proxy.

The flow is as follows: the user connects to the reverse proxy using HTTPS (encrypted connection) and the reverse proxy connects to the AzuraCast Docker container on the client’s behalf. Traffic between the reverse proxy (Apache httpd in our case) and the AzuraCast Docker container is un-encrypted and happens on the loopback address.
A reverse proxy is capable of handling many simultaneous connections and can be configured to offer SSL-encrypted connections to the remote users even when the backend can only communicate over clear-text un-encrypted connections.

Add the following reverse proxy lines to your VirtualHost definition of the “https://radio.darkstar.lan” web site configuration and restart httpd:

# ---
# Required modules:
# mod_proxy, mod_ssl, proxy_wstunnel, http2, headers, remoteip

# No caching
Header set Cache-Control "max-age=1, no-control"

# Proxy configuration
<Proxy *>
    Allow from all
    Require all granted
</Proxy>

ProxyRequests Off
ProxyVia on
ProxyAddHeaders On
ProxyPreserveHost On
ProxyTimeout 900

# SSL configuration
<IfModule mod_ssl.c>
    SSLProxyEngine on
    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Port "443"
</IfModule>

# Allow access to everyone
<Location />
    Allow from all
    Require all granted
</Location>

# Letsencrypt places a file in this folder when updating/verifying certs.
# This line will tell apache to not to use the proxy for this folder:
ProxyPass /.well-known !

# Reverse proxy for the Web UI at http(s)://radio.darkstar.lan/
ProxyPass / http://127.0.0.1:81/
ProxyPassReverse / http://127.0.0.1:81/

# And the reverse proxy for the Icecast stream playing at http(s)://radio.darkstar.lan/lowlands
<Location /lowlands>
    ProxyPass http://127.0.0.1:8001/lowlands
    ProxyPassReverse http://127.0.0.1:8001/lowlands
</Location>

# AzuraCast requires a WebSocket proxy
RewriteEngine on
RewriteCond %{HTTP:Connection} Upgrade [NC]
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:81/$1" [P,L]

# ---

If you want to make your non-encrypted web address http://radio.darkstar.lan redirect automatically to the encrypted ‘https://‘ variant, be sure to add this block to its VirtualHost definition to ensure that Letsencrypt can still access your server’s challenge file via an un-encrypted connection:

<If "%{REQUEST_URI} !~ m#/\.well-known/acme-challenge/#">
    Redirect permanent / https://radio.darkstar.lan/
</If>

The hostname and TCP port numbers shown in bold green are defined elsewhere in this article, they should stay matching when you decide to use a different hostname and port numbers.

Test and reload the Apache webserver configuration as follows:

# apachectl configtest
# apachectl graceful

MariaDB backups

The Web User-Interface of AzuraCast allows you to schedule regular backups of the SQL database which stores meta information about your Radio Station and media:
Go to ‘Administration > System Maintenance > Backups > Autiomatic Backups > Configure‘:

  • Check “Run Automatic Nightly Backups
  • Check “Exclude Media from Backup
  • Configure a time of the day, the archive format and the storage location (the default path in the dropdown is OK).
  • Click ‘Save Changes

Backup archives will be stored in the /opt/dockerfiles/azuracast/backups directory.
You can also manually backup the AzuraCast database and configuration with the following command:

# cd /usr/local/docker-azuracast
# docker compose exec web azuracast_cli backup --exclude-media /var/azuracast/backups/my-azuracast-backup.zip

This will create the backup ZIP file in your host’s local directory /opt/dockerfiles/azuracast/backups/.

Troubleshooting

The stream is silent / Liquidsoap is not playing

Check that your playlist has the AutoDJ enabled and that the mounted directory actually contains indexed files.

  • Change to the docker directory:
    # cd /usr/local/docker-azuracast
  • Check Liquidsoap logs:
    # docker compose logs | grep -i liquidsoap | tail -50
  • Verify the media directory is visible inside the container:
    # docker compose exec web ls -la /var/azuracast/stations/alien_pastures_radio/media/remote/mp3/

AzuraCast cannot read my MP3 files

File ownership matters. The AzuraCast Docker container runs as UID 1000 by default. If your host music files are owned by a different UID, make sure that the user account inside the container can read them.
Fix this by either:

  • Make files world-readable
    # chmod -R o+r /your/actual/path/to/mp3s
  • Or: change the container’s UID to match your host user.  In the ‘.env’ file that lives next to ‘docker-compose.yml’ you will find the below two lines. Change the UID number from 1000 to that of your own user on the host:
    AZURACAST_PUID=1000
    AZURACAST_PGID=1000

Then restart the Docker stack.

The web UI shows “0 files” after mounting

If you mounted the directory after the initial station creation but before a rescan, trigger a manual rescan:

# cd /usr/local/docker-azuracast
# docker compose exec web azuracast_cli azuracast:media:reprocess

The above command triggers a rescan for all Stations. If you want to trigger the rescan only for your own Station, run this instead:

# docker compose exec web azuracast_cli azuracast:media:reprocess alien_pastures_radio

You can also start the rescan process from the User Interface.
Go to ‘System Administration > Stations > [Your Station] > Manage > Media > Music Files‘:

  • Select “remote” or whatever root directory your media library shows
  • Click ‘More > Reprocess

Port 8001 is not reachable from outside

AzuraCast binds Icecast to `0.0.0.0:8001‘ on the host. If you have a host firewall, open the port to the outside world.

Here is an iptables example:
# iptables -A INPUT -p tcp --dport 8001 -j ACCEPT

But if you implemented the Apache reverse proxy as I outlined above, you would not have to expose this port at all. Instead you can rely on Apache httpd to relay user connections to the Icecast listen port on the host. The iptables firewall rule is then not needed of course.

Final thoughts

I found that AzuraCast does most of what my (t)rusty old OTTO did, and it is capable of considerably more that I am not even touching (but you might, if you are interested in running an actual live radio show with contributers).
The AutoDJ implements my primary need which is a maintenance-free jukebox: it handles the continuous queue filling without any intervention. The listener request system gives me an on-demand control over what plays next. My only gripe is that the AutoDJ pushes its own queue out to the player and any user request will be pushed out after that already pushed-out queue.  Which means that I need to keep the AutoDJ queue length limited to 1 or 2 songs so that I don’t have to wait too long for my own requested song to play.
The scheduled library scanning handles my ever-growing MP3 collection. And my music players just need to tun into a different Icecast URL.

If anyone is interested, I can describe in a future article how I deployed  YTuner locally to revive the network audio streaming capability for my Denon AVR-X2300W tuner after Denon killed its free online VTuner service by making it subscription-based. Because that Denon tuner is what’s playing my Icecast stream right now, while I am typing this.

I hope you enjoyed the article. Leave your thoughts in the comments section below.

Cheers, Eric

© 2026 Alien Pastures

Theme by Anders NorenUp ↑