Self-Hosted AI Stack: Open WebUI + n8n + Ollama on Docker with Tailscale | IT HomeLab

This guide picks up directly from the Ollama on Proxmox video. We’re going to add two more services on top of the Ollama server: Open WebUI for a browser-based chat interface to your local models, and n8n for self-hosted workflow automation with a Postgres backend. Everything runs in Docker on the same Ubuntu VM, connected through Tailscale sidecars for HTTPS access. Nothing phones home.

๐ŸŽฅ Watch the Video Tutorial


๐Ÿ’ก What We’re Building

By the end of this guide you’ll have three services working together:
  • Open WebUI โ€” a browser-based chat interface that connects directly to your Ollama server and presents your local LLMs as a clean, familiar chat UI
  • n8n โ€” a self-hosted workflow automation platform with a visual node editor. Think Zapier or Make, but running entirely on your own hardware with no per-execution fees
  • Postgres โ€” a dedicated database backend for n8n. More reliable under load than n8n’s default SQLite storage and easier to back up
All three are deployed as a single Docker Compose stack alongside the base Docker stack from the previous video โ€” Portainer, Watchtower, and the existing containers stay untouched. The new AI stack runs in its own Compose file with its own network.
โ„น๏ธ Note: Prerequisite: Ollama must already be running on a Proxmox VM and accessible via Tailscale. See: Run AI Locally: Install Ollama on Proxmox with Ubuntu and Tailscale

๐Ÿ›  What You’ll Need

  • The Ubuntu Docker VM from the previous Docker guide โ€” with Portainer, Watchtower, and Tailscale already configured
  • Ollama running on a separate VM and accessible over Tailscale on port 11434
  • A Tailscale auth key (reusable key works for a home lab โ€” in production, use per-device keys)
  • Open WebUI โ€” free, open source
  • n8n โ€” free community edition

๐Ÿ“‹ Step-by-Step Setup

1. The Docker Compose stack overview

The new stack has four services: Postgres (n8n’s database), n8n itself, the n8n Tailscale sidecar, Open WebUI, and the Open WebUI Tailscale sidecar. All services share a dedicated bridge network on the 172.20.4.0/24 subnet. Static IP addresses are assigned to each service within that subnet. This avoids port conflicts with the existing base stack โ€” services on different networks can use the same port numbers without clashing.
๐Ÿ’ก Tip: The Postgres Alpine image is used rather than the full Postgres image. It has a significantly smaller footprint โ€” important when you’re stacking multiple services on the same host.

2. The environment files

Two environment files are used, and the order matters: the second file overrides the first. This pattern lets you keep a template .env file in version control with placeholder values, and a separate secrets.env file with the actual credentials that never gets committed:
  • /media/docker/scripts/.env โ€” base variables: config path, Tailscale key placeholder, timezone, user ID
  • /media/docker/scripts/secrets.env โ€” the real values that override the placeholders: actual Tailscale auth key, n8n database password, n8n encryption key
The key variables you need to define:
# .env (safe to commit โ€” placeholder values only)
CONFIG=/media/docker/configs
TS_KEY=tskey-auth-example
TZ=Australia/Melbourne
PUID=1000
PGID=1000

N8N_DB_PASSWORD=changeme
N8N_ENCRYPTION_KEY=changeme
# secrets.env (never commit โ€” real values)
TS_KEY=tskey-auth-xxxxxxxxxxxxx
N8N_DB_PASSWORD=your-strong-db-password
N8N_ENCRYPTION_KEY=your-generated-key
To generate the n8n encryption key, run this on your Linux server:
openssl rand -hex 32
โš ๏ธ Warning: The n8n encryption key is tied to your n8n installation. If you lose it and have to regenerate it, you lose access to all saved credentials stored in n8n. Store it somewhere safe alongside your other important credentials.

3. The Docker Compose file

The full compose file covers all five services. The key configuration points for each:

Postgres (n8n database)

  n8n-postgres:
    image: postgres:16-alpine
    container_name: n8n-postgres
    restart: unless-stopped
    env_file:
      - /media/docker/scripts/.env
      - /media/docker/scripts/secrets.env
    environment:
      - POSTGRES_DB=n8n
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
    volumes:
      - ${CONFIG}/n8n/postgres:/var/lib/postgresql/data
    networks:
      ai_network:
        ipv4_address: 172.20.4.2
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

n8n

  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    env_file:
      - /media/docker/scripts/.env
      - /media/docker/scripts/secrets.env
    user: "${PUID}:${PGID}"
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_HOST=n8n-postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - WEBHOOK_URL=https://n8n..ts.net
      - TZ=${TZ}
      - N8N_DEFAULT_BINARY_DATA_MODE=filesystem
      - N8N_BINARY_DATA_STORAGE_PATH=/home/node/.n8n/binaryData
    volumes:
      - ${CONFIG}/n8n/data:/home/node/.n8n
    network_mode: "service:n8n-tailscale"
    depends_on:
      - n8n-postgres
      - n8n-tailscale
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

n8n Tailscale sidecar

  n8n-tailscale:
    image: tailscale/tailscale:latest
    container_name: n8n-tailscale
    restart: unless-stopped
    hostname: n8n
    environment:
      - TS_AUTHKEY=${TS_KEY}
      - TS_EXTRA_ARGS=--advertise-tags=tag:container
      - TS_SERVE_CONFIG=/config/serve.json
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ${CONFIG}/n8n/tailscale:/var/lib/tailscale
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - sys_module
    networks:
      ai_network:
        ipv4_address: 172.20.4.3

Open WebUI

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: unless-stopped
    env_file:
      - /media/docker/scripts/.env
    environment:
      - OLLAMA_BASE_URL=http://:11434
    volumes:
      - ${CONFIG}/open-webui:/app/backend/data
    network_mode: "service:open-webui-tailscale"
    depends_on:
      - open-webui-tailscale
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
Replace <ollama-tailscale-ip> with the Tailscale IP of your Ollama VM. This is how Open WebUI reaches Ollama โ€” directly over the Tailscale network rather than through your local LAN.

Open WebUI Tailscale sidecar

  open-webui-tailscale:
    image: tailscale/tailscale:latest
    container_name: open-webui-tailscale
    restart: unless-stopped
    hostname: open-webui
    environment:
      - TS_AUTHKEY=${TS_KEY}
      - TS_EXTRA_ARGS=--advertise-tags=tag:container
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ${CONFIG}/open-webui/tailscale:/var/lib/tailscale
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - sys_module
    networks:
      ai_network:
        ipv4_address: 172.20.4.4

networks:
  ai_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.4.0/24

4. The build script

Rather than running docker compose up -d manually each time, a build script handles the full startup sequence including folder ownership โ€” which is the most common source of container restart loops:
#!/bin/bash
# Set env file paths
ENV_FILE=/media/docker/scripts/.env
SECRETS_FILE=/media/docker/scripts/secrets.env

# Bring down if already running
docker compose --env-file $ENV_FILE --env-file $SECRETS_FILE down

# First bring up โ€” creates the config folder structure
docker compose --env-file $ENV_FILE --env-file $SECRETS_FILE up -d

# Bring back down to fix folder ownership before n8n starts properly
docker compose --env-file $ENV_FILE --env-file $SECRETS_FILE down

# Fix ownership of n8n and open-webui config folders
sudo chown -R 1000:1000 /media/docker/configs/n8n
sudo chown -R 1000:1000 /media/docker/configs/open-webui

# Bring up for real
docker compose --env-file $ENV_FILE --env-file $SECRETS_FILE up -d

# Wait for Tailscale sidecars to register
sleep 60

# Enable HTTPS via Tailscale serve
docker exec n8n-tailscale tailscale serve --bg http://localhost:5678
docker exec open-webui-tailscale tailscale serve --bg http://localhost:8080

# Verify services are up
docker compose ps
โš ๏ธ Warning: The up โ†’ down โ†’ chown โ†’ up again sequence is essential. If n8n starts before its config folder ownership is corrected, it creates files as root and then can’t write to them as the configured user โ€” causing an infinite restart loop. The build script handles this automatically.
Make the script executable before running it:
sudo chmod +x build.sh
./build.sh

5. Approve the new Tailscale devices

While the 60-second sleep runs, open your Tailscale admin console. You’ll see n8n and open-webui appear as new devices. If you haven’t set your auth key to auto-approve, approve them now. I also disable key expiry on these devices โ€” they’re home lab services that need to stay connected indefinitely.

6. Access Open WebUI

Once the build script completes, open a browser and go to your Open WebUI Tailscale URL:
https://open-webui..ts.net
Click Get Started, create your admin account with a strong password, and you’re in. Open WebUI automatically detects the Ollama server you pointed it to โ€” your available models (e.g. llama3.2) will appear in the model selector at the top of the chat interface. Select a model and start chatting.

7. Access n8n and create your first workflow

Open n8n at its Tailscale URL:
https://n8n..ts.net
Register your admin account on first login. You can also register for a free n8n community licence key โ€” it unlocks a few extra features without any cost. Skip the setup survey and go straight to the workflow editor. To verify Ollama is connected, create a simple test workflow:
  • Click New Workflow โ†’ Start from scratch
  • Click the + to add a node โ†’ search for HTTP Request
  • Method: POST
  • URL: http://<ollama-tailscale-ip>:11434/api/generate
  • Body Content Type: JSON
  • Body: {"model": "llama3.2", "prompt": "Explain what Docker is in one sentence.", "stream": false}
  • Click Save then Execute
๐Ÿ’ก Tip: Set stream: false in the request body. By default Ollama streams responses token by token, which makes the output very difficult to work with in n8n. With streaming disabled, n8n receives the complete response as a single JSON object that’s easy to parse in subsequent workflow nodes.
The response from Ollama appears in the output panel at the bottom of the n8n editor. Response time will vary depending on your hardware โ€” on CPU-only setups expect several seconds for a short prompt. The workflow works the same way at any speed.

โœ… Conclusion

You now have a fully self-hosted AI and automation stack running in your home lab. Open WebUI gives you a browser chat interface for your local Ollama models. n8n gives you a visual workflow automation platform backed by Postgres, with access to hundreds of integrations โ€” email, Google Sheets, push notifications, APIs, and more. Everything is accessible over HTTPS via Tailscale, nothing sends data to the cloud, and the whole stack runs on a single Ubuntu VM alongside your existing Docker containers. A few things worth exploring next: connecting n8n to your Pushover or Gotify account for push notifications, scheduling daily Docker backup workflows, and adding additional Ollama models to Open WebUI for different use cases. Previous guide in this series: Run AI Locally: Install Ollama on Proxmox with Ubuntu and Tailscale ๐Ÿ“บ Watch the full video guide here: https://youtu.be/YJGbedV1Ctc If you found this helpful, like and subscribe to IT HomeLab Online on YouTube for more tutorials. โ˜• Support the channel: Patreon ยท Buy Me a Coffee

Enjoyed this guide?

Subscribe to the channel for more homelab builds, Raspberry Pi projects, and AI automation tutorials.

▶ Watch on YouTube