# Yassine Fathi
> Yassine Fathi | Senior Software Engineer
Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts.
Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`).
## Pages
### About
URL: https://fathi.me/about/
Last updated: 2026-04-06T09:18:08.000Z
I’m Yassine Fathi, a Software Engineer specializing in building scalable, distributed systems and developer tooling. I work across the full stack—from crafting clean user interfaces to designing resilient back-end architectures and automating infrastructure.

My day-to-day involves writing Go, Ruby, and TypeScript, but I care more about solving the right problem than about any particular language. I’ve built and maintained microservice architectures, real-time data pipelines, and internal platforms—always with an emphasis on reliability, observability, and clean interfaces between components.
---
## What I Do
- **Back-end & API design** — Microservices, REST and event-driven architectures in Go and Ruby on Rails.
- **Infrastructure & DevOps** — Docker, Traefik, Tailscale, CI/CD pipelines, and cloud-native deployments on Hetzner, AWS, and GCP.
- **Front-end development** — React, TypeScript, and modern JavaScript tooling.
- **Open-source & tooling** — Author of Traeflare (automated Cloudflare DNS sync for Traefik) and other developer utilities.
- **Homelab & self-hosting** — Encrypted backup pipelines, VPN routing, network monitoring, and infrastructure automation.
---
## How I Work
I gravitate toward pragmatic engineering: choosing the simplest solution that meets the requirements, writing code that is easy to reason about, and automating anything that can be automated. I value clear documentation, thorough code review, and shipping work in small, verifiable increments.
Outside of work, I run a homelab where I experiment with self-hosted services, network security, and infrastructure patterns before applying them professionally. I also enjoy photography and documenting what I learn through this blog.
---
## Get in Touch
I’m always open to discussing interesting engineering challenges, potential collaborations, or open-source work. You can find me on:
- [LinkedIn](https://l.fathi.me/linkedin?ref=fathi.me)
- [GitHub](https://l.fathi.me/github?ref=fathi.me)
- [Instagram](https://l.fathi.me/instagram?ref=fathi.me)
### Privacy & Cookies Policy
URL: https://fathi.me/privacy-policy/
Last updated: 2025-02-21T23:39:17.000Z
**Last Updated: 10/02/2025**
### **Your Privacy Matters**
At [**fathi.me**](https://fathi.me/), we prioritize your privacy and transparency. This page explains our approach to cookies and how we handle data.
### **Do We Use Cookies?**
**No, this website does not use cookies for tracking or analytics.**
We do not store any cookies in your browser that track your activity across different sites.
### **What Information Do We Collect?**
- If you voluntarily provide information (e.g., via a contact form or subscription), we store it securely.
- We may collect anonymized usage data that does not personally identify you, solely for improving the website’s functionality.
### **Third-Party Services**
If we use any third-party services (e.g., embedded videos, social media buttons), those services may collect data independently. We recommend checking their privacy policies.
### **Your Choices**
You don’t need to take any action—since we don’t use cookies, there’s nothing to disable.
## Posts
### Dual NVIDIA GPU Passthrough on Linux: What Every Guide Gets Wrong
URL: https://fathi.me/dual-nvidia-gpu-passthrough-on-linux-what-every-guide-gets-wrong/
Last updated: 2026-07-28T10:19:16.000Z
I spent two weeks getting a Windows 11 VM with GPU passthrough working on CachyOS. I started with the usual plan: dynamically detach my second NVIDIA GPU when the VM starts, stream the guest with Apollo and Moonlight, and keep both cards available to Linux when Windows is off.
Almost none of that survived contact with the actual machine.
Dynamic binding hard-froze the host. Apollo connected but never produced a frame. Adding Looking Glass shared memory made QEMU abort with a DMA mapping error. Once that was fixed, KDE Wayland introduced three more bugs.
It works now, reliably. This is the complete setup I ended up with, including the dead ends and the fixes that mattered.
---
## The hardware and the goal
| Component | Configuration |
| ----------------- | --------------------------------------- |
| Host OS | CachyOS, KDE Wayland, GRUB, mkinitcpio |
| CPU | Intel i7-13700K, 8 P-cores + 8 E-cores |
| Memory | 64 GB |
| Host GPU | RTX 5070 Ti, Linux desktop and local AI |
| Guest GPU | RTX 4060 Ti 16 GB, dedicated to Windows |
| Guest | Windows 11, 32 GB RAM, 8 vCPUs |
| Display transport | Looking Glass over IVSHMEM |
The split is deliberate. Both cards have roughly 16 GB VRAM, but the 5070 Ti has much higher memory bandwidth, so it stays with Linux for local LLM work. The 4060 Ti is dedicated to the Windows VM. Both can run at the same time with no contention.
Before copying anything below, substitute your own PCI addresses, device IDs, CPU topology, memory allocation, and IOMMU address width. The values here are specific to this machine.
---
## 1\. Enable virtualization in the BIOS
Enable:
- Intel VT-x
- Intel VT-d
- Above 4G Decoding
Resizable BAR can usually stay enabled, but it isn't required for this setup.
---
## 2\. Enable IOMMU in Linux
Add these parameters to `GRUB_CMDLINE_LINUX_DEFAULT` in `/etc/default/grub`:
```
intel_iommu=on iommu=pt
```
Then rebuild GRUB:
```bash
sudo grub-mkconfig -o /boot/grub/grub.cfg
```
`intel_iommu=on` enables Intel's IOMMU. `iommu=pt` identity-maps host devices so ordinary host DMA doesn't pay a translation penalty.
---
## 3\. Identify the guest GPU and its IOMMU group
Find both GPU functions:
```bash
lspci -nn | grep -E 'VGA|3D|Audio'
```
On this machine, the guest card is:
```
07:00.0 VGA compatible controller [10de:2805]
07:00.1 Audio device [10de:22bd]
```
Pass both the video and HDMI audio functions. They should also be isolated from unrelated devices in their IOMMU group:
```bash
for d in /sys/kernel/iommu_groups/*/devices/*; do
group=${d#*/iommu_groups/}; group=${group%%/*}
printf 'group %s: %s\n' "$group" "${d##*/}"
done | sort -V
```
My 4060 Ti's video and audio functions sit alone in one group, so I didn't need an ACS override. If your group contains storage or another device the host needs, stop here and fix the hardware topology first.
---
## 4\. Bind the guest GPU to vfio-pci at boot
This is the most important part of the setup.
I initially tried dynamic binding: leave the 4060 Ti on the NVIDIA driver, then let libvirt detach it when Windows starts. That froze the host every time. The VM never started, the libvirt process entered uninterruptible D state, and shutdown hung until I forced the machine off.
The reason is subtle. NVIDIA's userspace GL and Vulkan stack opens every `/dev/nvidiaN` device on the machine, not only the card currently rendering the desktop. KWin, Xwayland, browsers, terminals, and other GPU applications all had the second card memory-mapped. Libvirt could not detach it from the driver.
Static binding makes that failure impossible. The 4060 Ti is claimed by vfio-pci during boot, before NVIDIA sees it. Linux never creates `/dev/nvidia1`, so no desktop process can hold the card.
Create `/etc/modprobe.d/vfio.conf` using your GPU's PCI IDs:
```
# Guest GPU video + HDMI audio
options vfio-pci ids=10de:2805,10de:22bd
# Ensure vfio-pci gets first chance at the devices
softdep nvidia pre: vfio-pci
softdep nvidia_drm pre: vfio-pci
softdep nvidia_modeset pre: vfio-pci
softdep nvidia_uvm pre: vfio-pci
```
In `/etc/mkinitcpio.conf`, add the VFIO modules:
```
MODULES=(vfio_pci vfio vfio_iommu_type1)
```
Then rebuild and reboot:
```bash
sudo mkinitcpio -P
sudo reboot
```
Do not add `vfio_virqfd`. It was merged into `vfio` in kernel 6.2 and no longer exists as a separate module.
Also remember that mkinitcpio's `modconf` hook copies `/etc/modprobe.d` into the initramfs. If you later change or remove `vfio.conf`, run `mkinitcpio -P` again. Deleting the file alone doesn't remove the old IDs from the boot image.
Verify after reboot:
```bash
# Both guest GPU functions should use vfio-pci
lspci -nnk -s 07:00 | grep -i 'driver in use'
# Host GPU should still use nvidia
lspci -nnk -s 01:00.0 | grep -i 'driver in use'
# Only the host GPU should appear
nvidia-smi --query-gpu=index,name,pci.bus_id --format=csv
# This machine should have only /dev/nvidia0
ls /dev/nvidia[0-9]*
```
The passthrough GPU disappearing from `nvidia-smi` is success.
---
## 5\. Install the virtualization stack
```bash
sudo pacman -S qemu-full libvirt virt-manager edk2-ovmf swtpm dnsmasq virtio-win
sudo systemctl enable --now virtqemud.socket
sudo usermod -aG libvirt,kvm "$USER"
```
Log out and back in after changing group membership.
Current libvirt installations often use modular daemons. Check which one is active:
```bash
systemctl is-active virtqemud libvirtd
```
On my system, `virtqemud` is active and `libvirtd` is not. Restarting `libvirtd` therefore does nothing, despite what older guides say.
---
## 6\. Create the Windows 11 VM
In virt-manager:
1. Create a VM from a Windows 11 ISO.
2. Allocate 32 GB RAM and 8 vCPUs.
3. Create a roughly 250 GB qcow2 disk on fast storage.
4. Use Q35 as the chipset.
5. Select an OVMF Secure Boot firmware image.
6. Add an emulated TPM 2.0 device using the CRB model.
7. Use VirtIO for the disk and network device.
8. Add the virtio-win ISO as a second CD-ROM.
9. Tick "Customize configuration before install".
Windows 11 Secure Boot inside OVMF is independent of the host's Secure Boot setting. My host Secure Boot is disabled, while the guest uses Secure Boot normally.
---
## 7\. Fix the VM XML
Back it up first:
```bash
virsh -c qemu:///system dumpxml win11 > ~/win11.xml.backup
virsh -c qemu:///system edit win11
```
### Hyper-V features and hidden KVM
```xml
```
`smm` is required for UEFI Secure Boot. Hiding KVM also avoids older NVIDIA Code 43 behaviour and other software that needlessly rejects virtual machines.
### CPU topology and pinning
The 13700K has 8 hyperthreaded P-cores (CPU 0-15) and 8 single-threaded E-cores (CPU 16-23). I give Windows P-cores 4-7, which are logical CPUs 8-15:
```xml
81
```
The topology is 4 cores × 2 threads, not 8 cores × 1 thread. The vCPUs are pinned to four physical P-cores and their siblings. You can check the real topology with:
```bash
cat /sys/devices/system/cpu/cpu8/topology/thread_siblings_list
```
### Hugepage-backed memory
```xml
3355443233554432
```
This allocates 32 GB and prevents it from being swapped. I use libvirt lifecycle hooks to allocate 16,384 2 MB hugepages before startup and release them after shutdown. Static hugepages at boot also work, but they permanently reserve half the machine even when the VM is off.
The hooks only manage hugepages. They do not bind GPUs and they do not change the CPU governor. `power-profiles-daemon` already owns the governor, and writing to it behind the daemon's back creates misleading state.
---
## 8\. Attach both GPU functions
Use virt-manager's "Add Hardware → PCI Host Device" and add both the GPU and its audio function. Or add this to the XML with your addresses:
```xml
```
`managed='yes'` is safe with static binding. Libvirt sees that vfio-pci already owned the card before startup, so it doesn't reattach it to NVIDIA when the VM stops.
---
## 9\. Add Looking Glass shared memory
[Looking Glass](https://l.fathi.me/FKJ7X?ref=fathi.me) copies the guest framebuffer through shared memory. There is no network stream, encoder, or decoder. For a VM running on the same machine, that's exactly what I wanted.
Add an IVSHMEM device:
```xml
128
```
Size it using `width × height × 4 × 2 + 10 MB`, rounded up to a power of two. 64 MB is enough for 2560×1440\. I use 128 MB so 4K also fits.
### The DMA mapping failure
As soon as I added shared memory, QEMU died with:
```
qemu: hardware error: vfio: DMA mapping failed, unable to continue
```
The CPU exposes 46 physical address bits. In `host-passthrough` mode, OVMF may place the IVSHMEM BAR high in that address space. But my Intel IOMMU has a 39-bit Maximum Guest Address Width (MGAW). The GPU can't DMA to the high BAR, so VFIO rejects the mapping.
Read your own MGAW:
```bash
for d in /sys/class/iommu/dmar*/intel-iommu/cap; do
cap=$(cat "$d")
echo "$d: MGAW=$(( ( (0x$cap >> 16) & 0x3f ) + 1 )) bits"
done
```
Then cap the guest with the `maxphysaddr` line in the CPU XML:
```xml
```
Don't copy `limit='40'` from a forum. It fails on hardware whose IOMMU reports 39 bits. Use the value your machine reports.
### Shared memory permissions
QEMU and your user both need access to the shared memory file. Create a tmpfiles rule:
```bash
sudo tee /etc/tmpfiles.d/10-looking-glass.conf >/dev/null <<'EOF'
f /dev/shm/looking-glass 0660 your-user kvm -
EOF
sudo systemd-tmpfiles --create /etc/tmpfiles.d/10-looking-glass.conf
```
Replace `your-user`. The file is recreated at boot because `/dev/shm` is tmpfs.
---
## 10\. Install Windows and the guest drivers
Install Windows through the temporary SPICE/QXL console. At the disk selection screen, load `viostor\w11\amd64` from the virtio-win ISO so the installer can see the VirtIO disk.
After installation:
1. Install `virtio-win-gt-x64.msi`.
2. Install the QEMU guest agent.
3. Install the normal NVIDIA GeForce driver.
4. Check Device Manager for the passed-through GPU and confirm there is no Code 43.
5. Install a maintained virtual display driver and set it to your monitor's resolution.
6. Install the IVSHMEM driver from the Looking Glass download page.
7. Install the Looking Glass Windows host application.
The virtio-win ISO does not include the IVSHMEM driver. It comes from Looking Glass. In Device Manager, the shared memory device initially appears as a yellow-flagged "PCI standard RAM Controller".
Use matching Looking Glass releases on both sides. A B7 client talking to a different host release may connect but never show a frame.
---
## 11\. Remove the emulated display, but keep SPICE
Once the virtual display and Looking Glass host work, disable QXL:
```xml
```
Keep SPICE bound to localhost:
```xml
```
Looking Glass has no input transport of its own. Keyboard and mouse go through SPICE. If QXL remains attached, Windows may make it the primary screen, and your pointer will move on an invisible display while Looking Glass shows the other one. That looks like a missing cursor, but it's really two displays disagreeing.
After QXL is removed, virt-manager's console becomes a black rectangle. That's expected. Looking Glass is now the console.
---
## 12\. Install and configure the Looking Glass client
Use the package from your distribution or build a stable release that matches the Windows host. On a recent Arch/CachyOS toolchain, I had to use the B7 tag rather than master. Master built, but it did not communicate reliably with the B7 Windows host.
My `~/.looking-glass-client.ini`:
```ini
[win]
fullScreen=yes
size=2560x1440
ignoreQuit=yes
[egl]
noBufferAge=yes
```
Don't put comments in this file. Looking Glass's ini parser treats comment lines as option names.
The three non-obvious settings are all there because of real KDE Wayland issues:
- `size` must match the monitor. The xdg backend ignores the first fullscreen configure size, so a mismatched initial buffer causes KWin to drop fullscreen.
- `ignoreQuit=yes` prevents KWin close events such as Alt+F4 from silently killing the window and leaving the SPICE thread stuck.
- `noBufferAge=yes` works around stale frames and the "mouse cursor paints the new image" artifact caused by incorrect EGL buffer age reporting on NVIDIA.
Run the client:
```bash
looking-glass-client
```
Looking Glass uses Scroll Lock as its escape key by default. Tap it to toggle input capture. Scroll Lock + F toggles fullscreen, and Scroll Lock + Q quits.
---
## Why I didn't use Apollo and Moonlight
Apollo and Moonlight were my original plan. They connected, but I never received a frame. The Windows logs eventually showed the cause:
```
Microsoft Basic Render Driver ... 0 MiB
Failed to create encoder D3D11 device [0x887A0004]
libx264 [software]
```
Windows had no EDID-backed output on the NVIDIA GPU. Apollo selected the Basic Render Driver, NVENC failed to initialize, and it silently fell back to software x264\. Moonlight sat there waiting for video that never became usable.
I tried firewall changes, different clients, auto-login, and a physical dummy plug. None fixed it. Looking Glass avoids the entire class of problem because it doesn't encode or stream anything.
The tradeoff is that Looking Glass is local to the host. It can't stream the VM to a TV or phone. For this machine, low-latency local access mattered more.
---
## Optional: allocate hugepages only while the VM runs
You can reserve hugepages permanently at boot, or use libvirt hooks. I use hooks so Linux gets the memory back when Windows is off.
The start hook calculates the required count from the VM memory. For 32 GB using 2 MB pages:
```
32768 MB / 2 MB = 16384 hugepages
```
A minimal start hook can compact memory and allocate the pages:
```bash
#!/bin/bash
set -euo pipefail
PAGES=16384
echo 1 > /proc/sys/vm/compact_memory
echo "$PAGES" > /proc/sys/vm/nr_hugepages
actual=$(cat /proc/sys/vm/nr_hugepages)
if [ "$actual" -ne "$PAGES" ]; then
echo "Could only allocate $actual/$PAGES hugepages" >&2
exit 1
fi
```
The release hook returns them:
```bash
#!/bin/bash
set -euo pipefail
echo 0 > /proc/sys/vm/nr_hugepages
```
Place them under the lifecycle paths used by your libvirt hook dispatcher. Don't add GPU bind/unbind hooks; static binding means nothing should ever move between NVIDIA and vfio-pci.
---
## Verification checklist
With the VM off:
```bash
# Guest GPU remains on vfio-pci
lspci -nnk -s 07:00 | grep -i 'driver in use'
# Host sees only the 5070 Ti
nvidia-smi --query-gpu=index,name --format=csv
# Shared memory permissions
stat -c '%U:%G %a %s' /dev/shm/looking-glass
```
With the VM running:
- Device Manager shows the RTX 4060 Ti without Code 43.
- The Looking Glass client reports the same version as the Windows host.
- The client reports a real BGRA frame format and your expected resolution.
- SPICE INPUTS and PLAYBACK channels connect.
- The host still sees only the 5070 Ti through `nvidia-smi`.
- The VM survives shutdown, force-off, and immediate restart.
---
## What actually mattered
The working setup came down to four decisions:
1. Bind the guest NVIDIA GPU to vfio-pci at boot. Don't dynamically detach it from a live NVIDIA desktop.
2. Use Looking Glass for local display instead of fighting a headless NVENC setup.
3. Read the IOMMU's real MGAW and cap `maxphysaddr` before adding IVSHMEM.
4. Configure Looking Glass around the actual KDE Wayland and NVIDIA EGL behaviour.
Nothing moves when the VM starts. The 4060 Ti is always owned by vfio-pci, whether Windows is running or not. The 5070 Ti is always owned by NVIDIA. That sounds less flexible than dynamic binding, but it is predictable, and predictable is what you want when the alternative is holding down the power button.
The result is exactly what I wanted: CachyOS remains the real workstation, the faster GPU stays available for Linux gaming and local AI, and Windows is a VM I can open in a low-latency fullscreen window when I need it.
### Self-Hosting a Chat UI for Your LLMs with OpenWebUI
URL: https://fathi.me/self-hosting-a-chat-ui-for-your-llms-with-openwebui/
Last updated: 2026-07-20T07:00:00.000Z
For a while I was using LLMs through a terminal. curl commands, API keys in environment variables, JSON in and JSON out. It worked, but it felt like reading a book through a keyhole. No conversation history worth looking at, no way to attach a file, no way to share a chat with someone who doesn't live in a terminal.
I tried a few chat UIs. Most of them fell into two camps: too simple to be useful, or too opinionated about which provider you should use. Then I found [OpenWebUI](https://l.fathi.me/HzLEQ?ref=fathi.me), and it's been my daily driver ever since.
This is how I set it up, what makes it different from other options, and the few things I had to figure out the hard way.
---
## What OpenWebUI actually is
Most people compare it to ChatGPT. That's roughly right but undersells it. OpenWebUI is a self-hosted web interface for LLMs that doesn't care where your models live. You point it at an OpenAI-compatible endpoint, and it gives you a polished chat experience with conversation history, file uploads, document search, web browsing, and a model picker.
The "OpenAI-compatible endpoint" part is doing a lot of work there. It means you can point it at OpenAI directly, or at a local [Ollama](https://l.fathi.me/xVAn6?ref=fathi.me) instance, or at an LLM gateway like the [LiteLLM setup I wrote about earlier](https://fathi.me/self-hosting-an-llm-gateway-with-litellm/). OpenWebUI doesn't need to know the difference.
In my case, I point it at my gateway. Every model I've configured there, local and cloud, shows up in the dropdown. One UI, all my models, no provider lock-in.
---
## The stack
Three containers in the compose:
| Container | What it does |
| ------------------- | --------------------------------------------------------- |
| OpenWebUI | The web app itself |
| Playwright sidecar | Headless browser for the "browse the web" feature |
| Apache Tika sidecar | Document parsing for PDF, Office files, and other formats |
The sidecars are optional but worth it. Without Tika, file uploads are limited to plain text and images. Without Playwright, the web search feature can't render JavaScript-heavy pages.
---
## Deployment
```yaml
services:
openwebui:
image: ghcr.io/open-webui/open-webui:latest
container_name: openwebui
restart: always
security_opt:
- no-new-privileges:true
depends_on:
- openwebui-playwright
ports:
- "3000:8080"
environment:
ENABLE_SIGNUP: "False"
ENABLE_LOGIN_FORM: "False"
ENABLE_OAUTH_SIGNUP: "True"
# OIDC config (see authentication section below)
OPENID_PROVIDER_URL: https://your-idp/.well-known/openid-configuration
OAUTH_CLIENT_ID: your-client-id
OAUTH_CLIENT_SECRET: ${OAUTH_CLIENT_SECRET}
OAUTH_PROVIDER_NAME: SSO
WEBUI_URL: https://openwebui.example.com
STORAGE_PROVIDER: local
WEB_LOADER_ENGINE: playwright
PLAYWRIGHT_WS_URL: ws://openwebui-playwright:3000
volumes:
- openwebui-data:/app/backend/data
openwebui-playwright:
image: mcr.microsoft.com/playwright:latest
container_name: openwebui-playwright
restart: always
command: npx -y playwright run-server --port 3000 --host 0.0.0.0
tika:
image: apache/tika:latest
container_name: openwebui-tika
restart: always
volumes:
openwebui-data:
```
Same rules as my other posts: pin by SHA digest in production. `:latest` here is for readability.
`ENABLE_SIGNUP: "False"` and `ENABLE_LOGIN_FORM: "False"` are important. You don't want random people creating accounts on your LLM UI. All authentication goes through OIDC.
`STORAGE_PROVIDER: local` means uploaded files and chat history live on the filesystem inside the container. If you want S3-compatible storage, OpenWebUI supports that too. Local is simpler and works fine for a personal or small-team setup.
---
## Connecting to a gateway
If you read my [LiteLLM article](https://fathi.me/self-hosting-an-llm-gateway-with-litellm/), this is where it pays off. OpenWebUI doesn't need direct access to any provider. It just talks to the gateway.
In the OpenWebUI admin settings, under Connections, add your gateway as an OpenAI API connection:
```
URL: http://your-gateway:4000/v1
API Key: sk-your-gateway-key
```
That's it. Every model you configured in the gateway appears in the model dropdown. Users can pick between a cheap local model for quick questions and a cloud model for complex tasks, all from the same chat interface. They have no idea what's behind it, and they shouldn't need to.
One thing to watch for: OpenWebUI caches the model list. If you add a new model to your gateway, you may need to refresh the connection in the admin panel before it shows up.
---
## Authentication with OIDC
This is where things get interesting if you're running more than one self-hosted service. I don't want separate logins for every tool. OpenWebUI supports OAuth/OIDC natively, which means you can wire it to a unified identity provider and get SSO across your entire homelab.
I wrote about this approach in my [Pocket ID article](https://fathi.me/pocket-id-unified-identity-self-hosted-services/). The short version: run an OIDC provider, create a client for OpenWebUI, and set these environment variables:
```
ENABLE_OAUTH_SIGNUP: "True"
OAUTH_MERGE_ACCOUNTS_BY_EMAIL: "True"
OPENID_PROVIDER_URL: https://your-idp/.well-known/openid-configuration
OAUTH_CLIENT_ID: your-client-id
OAUTH_CLIENT_SECRET: ${OAUTH_CLIENT_SECRET}
OAUTH_PROVIDER_NAME: Your IdP Name
WEBUI_URL: https://openwebui.example.com
```
`OAUTH_MERGE_ACCOUNTS_BY_EMAIL: "True"` is useful if you ever migrate from password auth to OIDC. Existing accounts get linked to their OIDC identity by email, so nobody loses their chat history.
`OAUTH_UPDATE_PICTURE_ON_LOGIN: "True"` is a nice touch. It pulls the avatar from your identity provider on each login, so profile pictures stay in sync across all your services.
The result: visiting OpenWebUI redirects to your identity provider. Authenticate once, and you're in. If you're already logged in from another service using the same provider, it's a single click.
---
## Document search (RAG)
This is the feature I didn't know I needed. OpenWebUI has built-in retrieval-augmented generation. You upload documents (PDFs, text files, code), and when you ask a question, the model can search through them to find relevant context before answering.
The Tika sidecar handles document parsing. It extracts text from PDFs, Word documents, spreadsheets, and a long list of other formats. Without it, you're limited to plain text and images.
I use this for technical documentation. Instead of pasting chunks of a spec into a chat window, I upload the whole document, tag it in a collection, and ask questions. The model pulls the relevant sections automatically. It's not perfect, and you still need to verify answers against the source, but it's dramatically faster than ctrl+F through a 200-page PDF.
---
## Web search
The Playwright sidecar gives OpenWebUI the ability to browse the web during a conversation. When the model needs current information, it can search, read pages, and cite what it found.
This is why the Playwright container exists. Headless Chrome renders the page properly, including JavaScript-heavy sites that a simple HTTP fetch would miss. It's slower than a plain text search but more reliable.
You can also configure OpenWebUI to use a self-hosted search engine like SearXNG as its search backend. That keeps your searches private and avoids rate limits on public APIs.
---
## Multi-user setup
OpenWebUI was designed for multiple users from the start. Each person gets their own workspace, chat history, and document collections. Admins can control which models are available to which users.
If you're running this for a team, the model access controls matter. You might want everyone to have access to the cheap local model but restrict the expensive cloud models to specific people. OpenWebUI's admin panel handles this through role-based access.
With OIDC authentication, user management is automatic. When someone logs in through your identity provider, an account is created. When they're removed from the provider, they lose access. No manual provisioning.
---
## Things I learned
**The model list cache got me once.** I added a new model to the gateway and kept refreshing the OpenWebUI dropdown. Nothing showed up. You need to go into admin settings, edit the connection, and let it re-fetch. Once you know this, it's a five-second fix. Until you know it, it's confusing.
**Tika is memory-hungry.** The container will happily eat 2GB+ when processing large documents. Give it room or it'll OOM on you. I started with 512MB and it crashed on a 50-page PDF.
**Local storage is fine until it isn't.** If you're running this for more than a handful of people, watch the disk usage. Chat history and uploaded files add up. S3-compatible storage is the better long-term play if you have the infrastructure for it.
**The web browser feature is slow by design.** It's spinning up a real browser, rendering a page, and extracting content. Don't expect sub-second responses when browsing is involved. The quality makes up for it.
---
## Wrapping up
OpenWebUI is the front door to my LLM setup. Behind it sits a [gateway](https://fathi.me/self-hosting-an-llm-gateway-with-litellm/) that handles routing and caching. In front of it sits [SSO](https://fathi.me/pocket-id-unified-identity-self-hosted-services/) so I don't manage passwords. Each piece does one job well, and they connect through standard interfaces: OpenAI-compatible API for models, OIDC for auth.
That's the thing about this stack. Nothing is custom. Everything speaks a standard protocol. If I wanted to swap OpenWebUI for a different UI tomorrow, the gateway wouldn't care. If I wanted to swap the identity provider, OpenWebUI wouldn't care. The modularity is the value.
If you're running LLMs locally, you need a good interface. OpenWebUI is the best one I've found, and the gap between it and the alternatives is significant.
### Self-Hosting an LLM Gateway with LiteLLM
URL: https://fathi.me/self-hosting-an-llm-gateway-with-litellm/
Last updated: 2026-07-17T19:03:20.000Z
At some point I noticed I was pasting the same API key into a fourth tool. Different config file, same key, and no real idea what any of the other tools were spending. One was calling GPT-4o for simple formatting tasks. Another had no caching, so identical prompts were hitting the API fresh every time. A runaway workflow almost wiped out a month's credits in a weekend.
I needed something between my tools and the providers. Not another library to import, but an actual service running on my network that I could point everything at. One endpoint, one set of keys, one place to see what was happening.
[LiteLLM](https://l.fathi.me/pSnBJ?ref=fathi.me) is what I ended up with. This is how I set it up and why each piece matters.
---
## The problem
The naive approach is fine for one tool. You paste your API key, you call the provider, it works. Then you add a second tool, a third, maybe an automation pipeline, and things get messy:
- Every service stores its own copy of your API key. Rotating a key means tracking down every config file.
- There's no cache. Two tools sending the same prompt both pay for it.
- There's no spend visibility. Each tool burns credits on its own.
- If your provider rate-limits you, everything breaks until the window resets.
- You can't route cheap tasks to a cheap model and expensive tasks to a smart one without configuring each tool separately.
A gateway fixes this. Tools talk to the gateway. The gateway holds the keys, routes requests, caches when it can, tracks what things cost, and fails over if a provider goes down.
---
## The stack
Three containers:
| Container | What it does | Persistent? |
| ---------- | ------------------------------------------------------------ | ----------------------------- |
| LiteLLM | The gateway itself | No (config lives in Postgres) |
| PostgreSQL | Model config, API keys (encrypted), spend logs, virtual keys | Yes |
| Redis | Prompt cache | No (by design) |
The gateway exposes a single OpenAI-compatible endpoint. Every consumer points at it. Behind it, you can mix cloud providers and local inference however you want.
Keep it behind your reverse proxy on your internal network. There's no good reason to expose an LLM gateway to the public internet.
---
## Deployment
```yaml
services:
litellm:
image: ghcr.io/berriai/litellm:latest
container_name: litellm
restart: always
security_opt:
- no-new-privileges:true
command: ["--config", "/app/config.yaml", "--port", "4000"]
ports:
- "4000:4000"
environment:
- DATABASE_URL=postgresql://litellm:password@litellm-db:5432/litellm
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
- LITELLM_SALT_KEY=${LITELLM_SALT_KEY}
- STORE_MODEL_IN_DB=True
depends_on:
litellm-db:
condition: service_healthy
litellm-redis:
condition: service_healthy
volumes:
- ./config.yaml:/app/config.yaml:ro
healthcheck:
test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')\""]
interval: 30s
timeout: 10s
retries: 3
litellm-db:
image: postgres:17-alpine
container_name: litellm-db
restart: always
security_opt:
- no-new-privileges:true
environment:
POSTGRES_DB: litellm
POSTGRES_USER: litellm
POSTGRES_PASSWORD: ${LITELLM_DB_PASSWORD}
volumes:
- litellm-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U litellm -d litellm"]
interval: 10s
litellm-redis:
image: redis:8-alpine
container_name: litellm-redis
restart: always
security_opt:
- no-new-privileges:true
command: ["redis-server", "--maxmemory", "512mb", "--maxmemory-policy", "allkeys-lru", "--save", ""]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
volumes:
litellm-db:
```
I'm using `:latest` here for readability, but in production I pin every image by SHA digest. A bad upstream release shouldn't be able to break your gateway overnight.
A couple of things in this compose that took me a minute to figure out:
`STORE_MODEL_IN_DB=True` moves model definitions and API keys into Postgres, so you manage them through the admin UI instead of editing YAML. The config file only holds gateway-level settings like caching and timeouts.
Redis runs without persistence (`--save ""`) on purpose. It's a cache. If it clears on reboot, you get a few cold calls on warmup. No data is lost.
Generate the master key and salt with `openssl rand -hex 32`. They're the root of the gateway's security, so don't reuse something from another service.
---
## Gateway config
The config file mounted into the container handles behavior that applies to every request:
```yaml
general_settings:
store_model_in_db: true
store_prompts_in_spend_logs: true
litellm_settings:
drop_params: true
num_retries: 0
request_timeout: 600
cache: true
cache_params:
type: redis
host: litellm-redis
port: 6379
ttl: 3600
mode: default_on
```
`drop_params: true` is worth explaining. When you route a request to a provider that doesn't support a parameter you sent (say, `temperature` to a model that ignores it), LiteLLM drops it instead of erroring. This matters more than you'd think once you start mixing providers.
`request_timeout: 600` gives requests 10 minutes. Some LLM calls are long, especially with large context windows. The default timeout will cut those off.
`store_prompts_in_spend_logs: true` logs the actual prompt text alongside cost data. Indispensable when you're trying to figure out which request burned through budget. Turn it off if you don't want prompts in your logs.
---
## Adding providers
With the DB-based config, you add providers through the admin UI. The structure looks like this if you're doing it in YAML:
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: gemini-2.5-pro
litellm_params:
model: gemini/gemini-2.5-pro
api_key: os.environ/GEMINI_API_KEY
- model_name: deepseek-v3
litellm_params:
model: openrouter/deepseek/deepseek-chat
api_key: os.environ/OPENROUTER_API_KEY
# Local inference, zero cost
- model_name: llama-local
litellm_params:
model: ollama/llama3.2
api_base: http://ollama:11434
router_settings:
routing_strategy: simple-shuffle
```
You can list multiple deployments under the same `model_name`. LiteLLM load-balances between them. That's how failover works: two entries for the same model pointing at different providers. If one goes down, traffic shifts to the other.
Once providers are set up, every consumer gets the same two values:
```
Base URL: http://your-gateway:4000/v1
API Key: sk-your-litellm-master-key
```
Anything that speaks OpenAI can use it. Chat UIs, automation tools, the OpenAI SDK in a script. They don't know or care which provider actually serves the request.
---
## Caching
Caching was the biggest cost saver for me. With `mode: default_on`, every request gets checked against Redis before hitting the provider. Same prompt, same model, same parameters, within the TTL window (1 hour default)? Cached response, no provider call, no charge.
You'd be surprised how often this hits. System prompts are identical across requests, so the opening tokens are frequently cacheable. Scheduled workflows that summarize or classify often see identical inputs. And debugging? Running the same prompt five times while iterating on something used to cost five API calls.
The cache lives in Redis with a 512MB cap and LRU eviction. It survives container restarts but not host reboots, which is fine. A cache miss is a cold call, not data loss.
If you need a fresh response, add `"cache": {"no-cache": true}` to the request body.
---
## Authentication
Two layers to think about.
The `LITELLM_MASTER_KEY` is the root credential. It can do everything: create keys, set budgets, view all logs. Don't hand it out to tools. Generate virtual keys from the admin UI instead, each scoped to specific models and budgets. A script that only needs the local model shouldn't have access to your most expensive cloud provider.
For the admin UI itself, LiteLLM supports OIDC. If you're running a self-hosted identity provider (Pocket ID, Authentik, Authelia, Keycloak), wire it up:
```
GENERIC_CLIENT_ID=your-client-id
GENERIC_CLIENT_SECRET=your-client-secret
GENERIC_AUTHORIZATION_ENDPOINT=https://your-idp/authorize
GENERIC_TOKEN_ENDPOINT=https://your-idp/token
GENERIC_USERINFO_ENDPOINT=https://your-idp/userinfo
GENERIC_SCOPE=openid email profile
AUTO_REDIRECT_UI_LOGIN_TO_SSO=true
PROXY_ADMIN_ID=your-oidc-identity-id
```
With this, the admin UI bounces through your identity provider. `PROXY_ADMIN_ID` maps your OIDC identity to full admin access.
On the network side, keep the gateway internal. Reverse proxy, gated access, no public exposure without strong auth in front. If something outside your network needs in, route it through a VPN or tunnel.
---
## Budgets and spend tracking
LiteLLM tracks spend per model, per key, per team. You can set daily budgets in USD, and when the gateway hits the limit, it stops accepting requests and sends a notification through whatever channel you've configured (webhook, Slack, Discord, ntfy).
This saved me from a runaway workflow once. An automation got stuck in a loop, making API calls every few seconds. The budget cap kicked in before I even noticed something was wrong. Without it, I would have found out when the credit card charge came through.
The admin dashboard breaks down spend by model and by day. When you can see that a model eating 80% of your budget is being used for tasks a local model could handle, you adjust routing. It's not about being cheap. It's about having enough information to make decisions.
---
## Connecting tools
This part is easy. Every tool just needs the gateway URL and a key:
```
Base URL: http://your-gateway:4000/v1
API Key: sk-your-litellm-key
```
For a chat interface, I use [OpenWebUI](https://l.fathi.me/63KuI?ref=fathi.me) pointed at the gateway. It looks like any other chat app to the user. Every model in the dropdown is served through LiteLLM, some local, some cloud, and the user can't tell the difference.
Automation works the same way. Tools like [n8n](https://l.fathi.me/EZqDr?ref=fathi.me) or [Activepieces](https://l.fathi.me/S9ErO?ref=fathi.me) support custom OpenAI-compatible endpoints, so they plug right in. Workflows that summarize emails, classify tickets, or generate reports all go through the gateway now, with caching and budget protection.
Scripts are the simplest. If you're using the OpenAI Python or JS SDK, swap the base URL and you're done. Same code, same interface.
---
## Wrapping up
The thing I like about this setup is that it gets out of the way. I stopped thinking about which API key goes where. I stopped worrying about a workflow going rogue overnight. I stopped running the same prompt twice and paying for both.
The caching pays for itself. The budget alerts are a safety net. The spend tracking actually changed how I route traffic between models, because for the first time I could see what was costing what.
If you're using LLMs in more than one place, put a gateway in front of them. One compose file, and you don't have to think about any of this stuff again.
### Zerobyte: A Web UI for restic That Replaces Your Backup Scripts
URL: https://fathi.me/zerobyte-restic-web-ui-backup-automation/
Last updated: 2026-04-06T10:35:43.000Z
A while back I wrote about [building an encrypted backup pipeline with restic, rest‑server, Hetzner Storage Box, and Tailscale](https://fathi.me/building-a-fast-encrypted-private-backup-pipeline-with-restic-rest-server-hetzner-storage-box-tailscale-vpn/). That setup has served me well—it’s reliable, fast, and entirely under my control. But it comes with overhead: Bash scripts, cron jobs, Telegram notification glue, and enough moving parts that onboarding a new machine means copying files around and hoping you remembered every environment variable.
I recently discovered [Zerobyte](https://github.com/nicotsx/zerobyte?ref=fathi.me), an open-source project that wraps restic in a clean web UI. It keeps everything I care about—client-side encryption, deduplication, flexible backends—and replaces the scripts and cron with a visual dashboard. Here’s how it works and why I think it’s worth your attention.
---
## What Is Zerobyte?
Zerobyte is a self-hosted backup automation tool built on top of restic. It runs as a single Docker container and gives you a web interface to:
- **Define volumes** — The source directories you want to back up (local paths, NFS, SMB, WebDAV, SFTP).
- **Create repositories** — Encrypted backup destinations: local disk, S3-compatible storage (AWS, MinIO, Wasabi), Google Cloud, Azure, or 40+ cloud providers via rclone.
- **Schedule backup jobs** — Cron-like scheduling with visual configuration, include/exclude patterns, and retention policies.
- **Monitor and restore** — Browse snapshots, check job history, and restore files—all from the browser.
The key point: restic does all the heavy lifting underneath. Zerobyte doesn’t reinvent encryption or deduplication. It’s a management layer, and a good one.
---
## Why Not Just Keep the CLI Pipeline?
My [existing setup](https://fathi.me/building-a-fast-encrypted-private-backup-pipeline-with-restic-rest-server-hetzner-storage-box-tailscale-vpn/) works. So why consider Zerobyte?
| | CLI Pipeline | Zerobyte |
| ------------------------ | ----------------------------------- | ------------------------------ |
| **Setup per host** | Copy script, set env vars, add cron | Point UI at source directory |
| **Scheduling** | Crontab entries | Built-in visual scheduler |
| **Monitoring** | Telegram bot + log files | Web dashboard with job history |
| **Restoring files** | SSH in, run restic restore | Point-and-click in browser |
| **Adding a new backend** | Edit script, test manually | Select from dropdown |
| **Multi-host overview** | Check each host individually | Single dashboard |
| **Flexibility** | Unlimited (it’s a shell script) | Covers \~90% of use cases |
The CLI approach gives you maximum control. If you enjoy writing shell scripts and want to customize every detail, it’s the right choice. But if you find yourself managing backups for multiple machines and wanting a single pane of glass, Zerobyte removes a lot of friction.
---
## Deploying Zerobyte
### Docker Compose
```yaml
services:
zerobyte:
image: ghcr.io/nicotsx/zerobyte:latest
restart: unless-stopped
ports:
- "4096:4096"
cap_add:
- SYS_ADMIN
devices:
- /dev/fuse:/dev/fuse
environment:
- BASE_URL=https://zerobyte.example.com
- APP_SECRET=
- TZ=Europe/Paris
volumes:
- zerobyte-data:/var/lib/zerobyte
# Mount source directories read-only:
- /tank/photos:/mnt/photos:ro
- /tank/cloud:/mnt/cloud:ro
- /tank/proxmox-backups:/mnt/proxmox:ro
volumes:
zerobyte-data:
```
A few notes:
- `SYS_ADMIN` and `/dev/fuse` are required for mounting remote volumes (NFS, SMB) inside the container. If you’re only backing up local directories, you can drop both.
- `APP_SECRET` must be at least 32 characters. Generate it with `openssl rand -hex 32`.
- `BASE_URL` determines whether cookies use the Secure flag. Set it to your actual HTTPS URL.
- Keep `/var/lib/zerobyte` on local storage, not a network share. This is where Zerobyte stores its database and configuration.
If you’re already running Traefik (as I described in my [Traeflare post](https://fathi.me/automating-cloudflare-dns-with-traefik-using-traeflare-2/)), just add the appropriate labels and drop the `ports` section.
---
## Setting Up Your First Backup
Once the container is running, open the web UI and:
1. **Create an admin account** on first launch.
2. **Add a Volume** — Point to a mounted source directory (e.g., `/mnt/photos`). This is the data you want to protect.
3. **Create a Repository** — Choose your storage backend. For a setup similar to my existing pipeline, use an S3-compatible backend or a rest-server URL. Set a strong encryption password—this is your restic repository password.
4. **Configure a Backup Job** — Link the volume to the repository. Set your schedule (e.g., daily at 03:00) and retention policy (7 daily, 4 weekly, 12 monthly, 2 yearly—same as my CLI setup).
5. **Run it** — Trigger the first backup manually to verify everything works. Watch the progress in the dashboard.
That’s it. No scripts, no cron, no Telegram bot setup. The dashboard shows you job status, last run time, snapshot count, and storage usage.
---
## Using Zerobyte With Your Existing Restic Repos
This is the part that sold me: Zerobyte uses restic under the hood, so you can point it at repositories you’ve already created. If you followed my [previous guide](https://fathi.me/building-a-fast-encrypted-private-backup-pipeline-with-restic-rest-server-hetzner-storage-box-tailscale-vpn/) and have a rest-server running on your Tailscale network, just create a new repository in Zerobyte with the REST backend URL and your existing password. Your snapshot history, deduplication data—everything is preserved.
This means migration isn’t a rip-and-replace. You can run both side by side, verify Zerobyte is working correctly, then retire the cron jobs at your own pace.
---
## Backed Destinations Worth Considering
Zerobyte supports more backends than a typical CLI restic setup out of the box:
- **Hetzner Storage Box** — Via SMB/SFTP volume or through a rest-server (my current approach).
- **S3-compatible** — AWS S3, MinIO, Wasabi, Backblaze B2\. Great if you want object storage pricing.
- **Google Cloud Storage / Azure Blob** — Native support, no rclone needed.
- **rclone remotes** — Google Drive, Dropbox, OneDrive, and 40+ other providers. Useful for free-tier backup destinations.
- **Local disk** — An attached USB drive or a second NAS. Simple and fast for a local recovery copy.
---
## Things to Keep in Mind
- **Zerobyte is still pre-1.0** (currently v0.x). The developer is actively collecting feedback and expects breaking changes between versions. It’s solid for homelab use, but I wouldn’t bet a production environment on it just yet.
- **Don’t expose the UI to the internet without authentication.** Put it behind Traefik with Pocket ID, Authelia, or at minimum HTTP basic auth. Or just keep it on your Tailscale network.
- **Test your restores.** This advice hasn’t changed since my last post: backups you never test are just archives. Zerobyte makes restoring easier, but you still need to verify the output.
---
## Wrapping Up
My [CLI-based restic pipeline](https://fathi.me/building-a-fast-encrypted-private-backup-pipeline-with-restic-rest-server-hetzner-storage-box-tailscale-vpn/) isn’t going anywhere—it’s battle-tested and I trust it. But Zerobyte is exactly the kind of tool I wish existed when I first set it up. It takes the same engine, wraps it in a UI that makes scheduling and monitoring trivial, and lowers the barrier for anyone who doesn’t want to maintain shell scripts.
If you’re starting fresh with homelab backups, Zerobyte is the easier on-ramp. If you already have a restic setup, it’s a smooth upgrade. Either way, the fundamentals haven’t changed: encrypt everything, store it off-site, and test your restores.
### Build Private AI Agents in Your Homelab with Ollama and n8n
URL: https://fathi.me/private-ai-agents-homelab-ollama-n8n/
Last updated: 2026-04-06T10:28:54.000Z
You’re running a dozen self-hosted services. You’ve got dashboards, alerts, RSS feeds, logs, maybe a photo library and a media server. Everything works, but nothing talks to each other intelligently. You’re still the glue—reading logs, triaging notifications, manually kicking off tasks.
What if your homelab could think for itself?
In 2026, that’s no longer a hypothetical. With [Ollama](https://ollama.com/?ref=fathi.me) running large language models locally and [n8n](https://n8n.io/?ref=fathi.me) orchestrating workflows across 400+ integrations, you can build private AI agents that automate real work—without sending a single byte to the cloud and at zero ongoing cost.
---
## Why Self-Hosted AI Matters
You can already use ChatGPT or Claude to answer questions. But hosted APIs have three problems for homelab automation:
- **Privacy** — Every prompt you send includes your data. Server logs, email content, personal notes—all leaving your network.
- **Cost** — API calls add up fast when you’re running automated workflows 24/7\. A log monitoring agent that fires every 5 minutes will drain your credits.
- **Latency and availability** — Your automation breaks when the API is down or rate-limited. Local inference has no such dependency.
Running your own LLM on your own hardware solves all three. And with the current generation of open models (Llama 4, Qwen 3, DeepSeek V3, Gemma 4), local doesn’t mean compromising on quality anymore.
---
## The Stack
We’re building with four components:
- **Ollama** — Runs LLMs locally with a single command. No Python environment, no dependency hell. Just pull a model and go.
- **n8n** — Open-source workflow automation with a visual editor and 400+ integrations. Think Zapier, but self-hosted and with first-class AI agent support.
- **Qdrant** — Vector database for semantic search. Needed if you want your agent to query your own documents (RAG).
- **PostgreSQL** — Persistent storage for n8n workflows and execution history.
n8n maintains an official [Self-Hosted AI Starter Kit](https://github.com/n8n-io/self-hosted-ai-starter-kit?ref=fathi.me) that bundles all of this into a single Docker Compose file. We’ll use that as our starting point.
---
## Hardware Requirements
You don’t need a server rack. Here’s what actually works:
| RAM | Model Size | Example Models |
| ------ | -------------- | --------------------------- |
| 8 GB | 7B parameters | Llama 3.2 7B, Gemma 4 7B |
| 16 GB | 14B parameters | Qwen 3 14B, DeepSeek V3 14B |
| 24 GB | 32B parameters | Llama 4 32B |
| 32 GB+ | 70B parameters | Llama 4 70B, Qwen 3 72B |
For most homelab workflows—summarization, classification, log analysis—a 14B model on 16 GB of RAM is the sweet spot. If you have an NVIDIA GPU, inference will be significantly faster, but CPU-only works fine for non-interactive tasks like scheduled automations.
An M4 Mac mini with 24 GB of unified memory (\~$800) is arguably the best value right now: silent, 30W idle, and fast enough for 32B models.
---
## Deployment
### Option 1: The Official Starter Kit
```bash
git clone https://github.com/n8n-io/self-hosted-ai-starter-kit.git
cd self-hosted-ai-starter-kit
cp .env.example .env
# GPU (NVIDIA)
docker compose --profile gpu-nvidia up -d
# CPU only
docker compose --profile cpu up -d
# AMD GPU (Linux)
docker compose --profile gpu-amd up -d
```
This gets you n8n, Ollama, Qdrant, and PostgreSQL in one shot. Access n8n at `http://localhost:5678` and complete the initial setup.
### Option 2: Minimal Custom Compose
If you already run PostgreSQL or prefer a leaner setup:
```yaml
services:
ollama:
image: ollama/ollama:latest
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
# Uncomment for NVIDIA GPU support:
# deploy:
# resources:
# reservations:
# devices:
# - capabilities: [gpu]
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
ports:
- "5678:5678"
environment:
- N8N_HOST=localhost
- N8N_PORT=5678
- N8N_PROTOCOL=http
volumes:
- n8n-data:/home/node/.local/share/n8n
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
ollama-data:
n8n-data:
```
The `extra_hosts` directive lets n8n reach Ollama via `host.docker.internal:11434`. If both containers share a Docker network, you can use the service name `ollama:11434` directly instead.
### Pulling Your First Model
```bash
# Pull a model
docker exec -it ollama ollama pull llama3.2
# Verify it works
docker exec -it ollama ollama run llama3.2 "Summarize what a reverse proxy does in one sentence."
```
---
## Connecting n8n to Ollama
Once both services are running:
- Open n8n at `http://localhost:5678`
- Go to **Credentials → Add Credential → Ollama**
- Set the base URL to `http://ollama:11434` (or `http://host.docker.internal:11434` if running Ollama on the host)
- Save, and the connection is live
You now have a local LLM available as a node in any n8n workflow. Drag it in, pick your model, write a prompt, and wire it to triggers and actions.
---
## Practical Workflows
Here’s where it gets useful. These are real automations you can build in minutes.
### 1\. Automated Log Analysis
A workflow that runs every 15 minutes, tails your server logs, sends them to Ollama for analysis, and alerts you on Discord or email only when something looks wrong.
**Nodes:**
1. **Cron Trigger** — Every 15 minutes
2. **Execute Command** — `tail -n 200 /var/log/syslog`
3. **Ollama** — Prompt: *"Analyze these server logs. Identify any errors, warnings, or unusual patterns. If everything looks normal, respond with CLEAR. Otherwise, summarize the issues."*
4. **IF** — Check if response contains "CLEAR"
5. **Discord/Email** — Send alert with the summary (only if not CLEAR)
You’ve just built an AI-powered log monitor. No SaaS subscription, no data leaving your network.
### 2\. Email Triage Agent
Connect your email (IMAP or Gmail node) to an Ollama-powered classifier:
**Nodes:**
1. **Email Trigger** — New incoming email
2. **Ollama** — Prompt: *"Classify this email into one of: URGENT, ACTION\_REQUIRED, FYI, SPAM. Then write a one-line summary."*
3. **Switch** — Route by classification
4. **Slack/Notification** — URGENT → immediate ping; ACTION\_REQUIRED → daily digest; SPAM → archive
Your inbox is now triaged by AI, running entirely on your own machine.
### 3\. RSS Feed Summarizer
Stay on top of news without doomscrolling:
**Nodes:**
1. **RSS Feed Trigger** — Monitor your favorite feeds
2. **Ollama** — Prompt: *"Summarize this article in 2–3 bullet points. Flag if it’s relevant to: \[your topics\]."*
3. **Filter** — Only keep relevant articles
4. **Notion/Markdown File** — Append to a daily digest document
### 4\. Document Q&A with RAG
This is where Qdrant comes in. Ingest your documents (PDFs, notes, manuals) into the vector database, then ask questions in natural language:
**Nodes:**
1. **Chat Trigger** — You ask a question
2. **Vector Store Retriever** — Find relevant document chunks from Qdrant
3. **Ollama** — Prompt: *"Based on the following context, answer the user’s question. If the context doesn’t contain the answer, say so."*
4. **Response** — Return the answer
Your private, searchable knowledge base—powered by your own hardware.
---
## The AI Agent Node: Autonomous Workflows
Everything above uses Ollama as a processing step in a linear workflow. But n8n’s **AI Agent node** goes further: it lets the LLM decide what to do next.
You give the agent:
- A goal (e.g., “Investigate why disk usage spiked”)
- A set of tools (shell commands, API calls, database queries)
- Access to your local LLM via Ollama
The agent then reasons about the problem, selects the right tools, executes them, analyzes the results, and loops until the task is complete. This is the same agentic pattern that powers tools like Claude Code and GitHub Copilot Workspace—except it’s running on your hardware, with your data, under your control.
---
## Tips for Production Use
- **Use quantized models** — 4-bit or 6-bit quantizations give you 80–90% of full-precision quality at a fraction of the memory. For automated tasks (not creative writing), this is a no-brainer.
- **Pin model versions** — Use `llama3.2:7b-q4_K_M` instead of `llama3.2:latest`. You don’t want a model update to silently change your workflow’s behavior.
- **Set timeouts** — LLM inference on CPU can be slow for large inputs. Configure n8n node timeouts to avoid hanging workflows.
- **Monitor with Uptime Kuma** — Point it at Ollama’s `/api/tags` endpoint to ensure the service is healthy.
- **Put it behind Traefik** — If you want to access n8n remotely, route it through your existing reverse proxy with HTTPS and authentication.
---
## Wrapping Up
The combination of Ollama and n8n is the most practical AI setup for a homelab in 2026\. It’s not a toy—it’s a genuinely useful system that automates real work, runs on modest hardware, costs nothing after the initial setup, and keeps your data entirely under your control.
Start with one workflow. The log analyzer or RSS summarizer are good first projects—simple enough to build in 15 minutes, useful enough that you’ll actually keep them running. Once you see what’s possible, you’ll find yourself wiring AI into everything.
### Unified Identity for Your Homelab: Using Pocket ID to Authenticate Every Self-Hosted Service
URL: https://fathi.me/pocket-id-unified-identity-self-hosted-services/
Last updated: 2026-04-06T10:04:41.000Z
If you self-host more than a handful of services, you’ve probably hit the same wall: a different username and password for Immich, another for Proxmox, yet another for Grafana, and so on. Each service has its own user database, its own password-reset flow, and its own session management. It doesn’t scale, and it’s a security liability.
[Pocket ID](https://pocket-id.org/?ref=fathi.me) solves this by acting as a lightweight, self-hosted OpenID Connect (OIDC) identity provider built entirely around **passkeys**. No passwords, no TOTP codes, no external dependencies—just a single source of truth for authentication across your entire homelab.
---
## Why Pocket ID Over Keycloak or Authelia?
The self-hosted identity space already has established players. Here’s why Pocket ID is worth considering:
- **Passkey-first design** — Authentication uses WebAuthn/FIDO2 exclusively. There are no passwords to leak, phish, or brute-force.
- **Minimal footprint** — A single container with SQLite by default. No Java runtime, no external database required. Starts in seconds.
- **Standard OIDC** — Any service that supports OpenID Connect works out of the box. Pocket ID already has documented integrations for 80+ services.
- **Simple admin UI** — Create OIDC clients, manage users, and configure groups from a clean web interface.
If you run a large organization with complex RBAC, SAML requirements, or federation across multiple IdPs, Keycloak is still the right choice. But for a homelab or small team, Pocket ID gives you SSO in minutes, not hours.
---
## Deploying Pocket ID
Pocket ID requires HTTPS (WebAuthn mandates a secure context). If you already run a reverse proxy like Traefik or Caddy, you’re set.
### Docker Compose
```yaml
services:
pocket-id:
image: ghcr.io/pocket-id/pocket-id:latest
restart: unless-stopped
ports:
- "80:80"
environment:
- APP_URL=https://id.example.com
- TRUST_PROXY=true
volumes:
- pocket-id-data:/data
volumes:
pocket-id-data:
```
Spin it up, then navigate to `https://id.example.com/setup` to register your first admin passkey. That’s the entire installation.
---
## Integrating with Your Services
The pattern is always the same: create an OIDC client in Pocket ID, then point your service to the discovery URL. Let’s walk through three common homelab services.
### Immich (Photo Management)
In Pocket ID, create a new OIDC client named `immich` and set the callback URLs:
```
https://photos.example.com/auth/login
https://photos.example.com/user-settings
app.immich:///oauth-callback
```
The third callback enables the Immich mobile app to complete the OAuth flow.
In Immich, go to **Administration → Settings → Authentication Settings → OAuth** and configure:
- **Issuer URL**: your Pocket ID OIDC discovery URL
- **Client ID** and **Client Secret**: from the OIDC client you just created
- **Button Text**: "Login with Pocket ID" (optional)
Save, and your Immich instance now authenticates through Pocket ID.
### Proxmox VE (Virtualization)
Create an OIDC client named `proxmox` in Pocket ID. Set the callback URL to your Proxmox host (e.g., `https://proxmox.example.com`).
In Proxmox, navigate to **Datacenter → Permissions → Realms** and add a new **OpenID Connect Server** realm:
- **Issuer URL**: `https://id.example.com`
- **Realm**: `PocketID`
- **Client ID** and **Client Key**: from Pocket ID
- **Username Claim**: `username`
- **Autocreate Users**: enabled
For group-based permissions, set the scope to `openid profile email groups` and the groups claim to `groups`. Then map Pocket ID groups (e.g., "Proxmox Admins") to Proxmox roles.
### Grafana (Monitoring)
Create an OIDC client named `grafana` with the callback URL `https://grafana.example.com/login/generic_oauth`.
Add the following to your Grafana configuration:
```ini
[auth.generic_oauth]
enabled = true
name = Pocket ID
client_id =
client_secret =
auth_url = https://id.example.com/authorize
token_url = https://id.example.com/api/oidc/token
api_url = https://id.example.com/api/oidc/userinfo
scopes = openid profile email
```
Restart Grafana, and the login page will show a "Login with Pocket ID" button.
---
## Beyond Individual Services: Protecting Any App with OAuth2 Proxy
Not every service supports OIDC natively. For those, you can place an [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/?ref=fathi.me) in front of them. This gives you Pocket ID authentication for applications that have no built-in auth at all—dashboards, internal tools, static sites, you name it.
The setup is straightforward: deploy OAuth2 Proxy as a sidecar or standalone container, point it at your Pocket ID instance, and configure your reverse proxy to require authentication through it before forwarding requests to the upstream service.
---
## The Full Picture
With Pocket ID at the center of your homelab, your authentication topology looks like this:
- **One identity provider** — Pocket ID manages all users and passkeys.
- **One login flow** — Tap your fingerprint or hardware key, and you’re in.
- **Per-service OIDC clients** — Each service gets its own client ID/secret pair with scoped callback URLs.
- **Group-based access control** — Define groups once in Pocket ID, map them to roles in each service.
- **No passwords anywhere** — Nothing to rotate, nothing to leak.
Pocket ID currently supports integrations with 80+ services out of the box, including Nextcloud, Portainer, Gitea, Jellyfin, Paperless-ngx, Vaultwarden, Outline, and many more. Check the [full list in the official docs](https://pocket-id.org/docs/client-examples?ref=fathi.me).
---
## Wrapping Up
Managing identity across self-hosted services doesn’t have to involve a heavyweight IdP. Pocket ID gives you passwordless SSO with minimal operational overhead: one container, passkey-only auth, and standard OIDC that works with virtually everything.
If you’re tired of juggling credentials across your homelab, give it a try. You can be up and running in under ten minutes.
### Building a Fast, Encrypted & Private Backup Pipeline with restic, rest‑server, Hetzner Storage Box & Tailscale VPN
URL: https://fathi.me/building-a-fast-encrypted-private-backup-pipeline-with-restic-rest-server-hetzner-storage-box-tailscale-vpn/
Last updated: 2026-04-06T10:42:52.000Z
**Updated April 2026** — Refreshed for restic 0.18, improved Docker Compose (no deprecated `version` key), added append-only mode and Prometheus monitoring, and tightened security recommendations. Looking for a UI-based approach instead? Check out [Zerobyte: A Web UI for restic That Replaces Your Backup Scripts](https://fathi.me/zerobyte-restic-web-ui-backup-automation/).
> A step‑by‑step guide to reproducing (and understanding) the exact setup I use to keep my Proxmox homelab and personal data safe—updated for 2026.
### 1\. Why this stack?
| Layer | What it does | Why I chose it |
| ----------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **restic** | CLI backup tool with client‑side encryption, deduplication & fast incremental snapshots | Small binary, no root needed, battle‑tested, easy restores |
| **rest‑server** | Lightweight HTTP backend that speaks restic’s REST protocol | Lower latency than SFTP/SSH when the repository lives on a remote CIFS/NFS mount (no double encryption, stream‑oriented) |
| **Hetzner Storage Box** | Cheap off‑site storage available over SMB / Samba, SFTP, WebDAV & more | Flat pricing, runs in the same DC as my (tiny) VPS, unlimited traffic |
| **Tailscale** | WireGuard‑based overlay network | End‑to‑end encryption, avoids public exposure & lets every client “see” the repo via an internal IP |
| **Docker Compose** | Deploy rest‑server & its CIFS mount reproducibly | One‑file infrastructure, easy upgrades & rollbacks |
The result is a **pull‑based** model: every device (Proxmox node, NAS, laptop, …) runs restic and **pushes** its encrypted chunks to a central rest‑server that lives on my [Hetzner VPS](https://l.fathi.me/hetzner?ref=fathi.me). The VPS itself merely **forwards bytes** to a CIFS share on the Storage Box; it never sees unencrypted data.
---
### 2\. Provision your Storage Box & sub‑account
1. Order a Storage Box (BX line is enough for backups).
2. Inside the Hetzner Cloud console create a **sub‑account** with:
- **SMB/CIFS** enabled
- Strong, unique password
Note the UNC path; it looks like
```
//.your-storagebox.de/
```
Hetzner’s docs confirm the same syntax for CIFS mounting
---
### 3\. Spin up a tiny VPS & join it to Tailscale
```bash
# 1 vCPU, 2 GB RAM is plenty
hcloud server create --type cpx11 --image debian-13 --datacenter fsn1-dc14 \
--name backup-vps
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-routes --hostname restic-hub
```
Record the **Tailscale IPv4** that the node receives (e.g. `100.x.y.z`). Only this IP will be exposed from Docker.
---
### 4\. Deploy rest‑server with Docker Compose
```yaml
volumes:
data:
driver: local
driver_opts:
type: cifs
device: "//.your-storagebox.de/"
o: "username=,password=,vers=3.0,uid=1000,gid=1000"
services:
restic-server:
image: restic/rest-server:latest
restart: always
user: "1000:1000"
environment:
- OPTIONS=--append-only --prometheus --log -
ports:
- ":8000:8000"
volumes:
- data:/data
- ./htpasswd:/htpasswd:ro
```
**What’s changed and why:**
- `No more version: "3.9"` — Docker Compose V2 no longer requires or recommends the `version` key. Drop it.
- `--append-only` — Clients can create new backups but cannot delete or modify existing ones. This protects against ransomware or a compromised client wiping your snapshots. Run `forget` and `prune` from the server side only.
- `--prometheus` — Exposes metrics at `/metrics` for scraping with Prometheus + Grafana. Free observability into repo size, request counts, and error rates.
- **HTTP basic auth via htpasswd** — Instead of `DISABLE_AUTHENTICATION=1`, we now mount a `.htpasswd` file. Each client gets its own credentials. Generate with: `htpasswd -B -c ./htpasswd/credentials `
- **Tailscale IP binding** — Still binding to the Tailscale IP instead of `0.0.0.0` to prevent accidental public exposure. This is your primary network-level control.
Bring it up:
```bash
docker compose up -d
```
Verify the server is running:
```bash
curl -u : http://:8000/
```
A `404 page not found` response means the server is up but the repository directory is empty—time to initialize it from a client.
---
### 5\. Install restic on your Proxmox host
```bash
apt update && apt install -y restic
# Verify you have 0.18+
restic version
# Store the repo password securely
pwgen -s 32 1 > /etc/restic-key
chmod 600 /etc/restic-key
```
Restic 0.18 (March 2025) added cold storage support for S3 backends, improved `prune` performance for repacking small files, and compression for `dump` output. If your distro ships an older version, grab the latest binary from the [GitHub releases](https://github.com/restic/restic/releases?ref=fathi.me).
---
### 6\. The backup script
Below is the full Bash helper I use. It does three jobs:
1. **Initializes** the repo if it’s the first run.
2. Runs `restic backup` with a chosen tag.
3. Enforces a **retention policy** (7 daily, 4 weekly, 12 monthly, 2 yearly) via `restic forget --prune`.
It also ships Telegram alerts on success/failure so I don’t have to tail logs at 3 am.
```bash
#!/usr/bin/env bash
# backup.sh - Backup script using restic
set -euo pipefail
# Repository (adjust to your rest-server)
export RESTIC_REPOSITORY=rest:http://:@:8000
export RESTIC_PASSWORD_FILE=/etc/restic-key
# Retention policy
KEEP_DAILY=7
KEEP_WEEKLY=4
KEEP_MONTHLY=12
KEEP_YEARLY=2
# Telegram notifications
TELEGRAM_CHAT_ID=
TELEGRAM_BOT_TOKEN=
telegram_notify() {
local message="$1"
curl -sf -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="${TELEGRAM_CHAT_ID}" \
-d parse_mode="Markdown" \
--data-urlencode text="${message}" >/dev/null 2>&1 || true
}
initialize_repo() {
if restic snapshots >/dev/null 2>&1; then
echo "Repository already initialized."
else
echo "Initializing repository..."
restic init
telegram_notify "\xE2\x84\xB9\xEF\xB8\x8F Initialized restic repo on host: \`$(hostname)\`"
fi
}
backup() {
local source_dir="$1"
local tag="$2"
echo "Backing up ${source_dir} (tag: ${tag})..."
restic backup "${source_dir}" \
--tag "${tag}" \
--exclude-caches \
--verbose
telegram_notify "\xE2\x9C\x85 Backup of \`${source_dir}\` (${tag}) *completed* on \`$(hostname)\`"
}
cleanup() {
echo "Applying retention policy..."
restic forget \
--keep-daily "${KEEP_DAILY}" \
--keep-weekly "${KEEP_WEEKLY}" \
--keep-monthly "${KEEP_MONTHLY}" \
--keep-yearly "${KEEP_YEARLY}" \
--prune
telegram_notify "\xF0\x9F\xA7\xB9 Cleanup completed on \`$(hostname)\`"
}
error_handler() {
telegram_notify "\xE2\x9D\x8C Backup *FAILED* on \`$(hostname)\`: \`${1:-unknown error}\`"
exit 1
}
trap 'error_handler "$BASH_COMMAND"' ERR
# --- Main ---
case "${1:-}" in
backup)
[[ $# -ne 3 ]] && { echo "Usage: $0 backup "; exit 1; }
initialize_repo
backup "$2" "$3"
;;
cleanup)
cleanup
;;
*)
echo "Usage: $0 {backup |cleanup}"
exit 1
;;
esac
```
**Changes from the original script:**
- HTTP basic auth credentials are now embedded in the repository URL (`rest:http://user:pass@host:port`).
- `--exclude-caches` skips directories containing a `CACHEDIR.TAG` file.
- The Telegram function has `|| true` to prevent a notification failure from aborting the entire backup.
- Replaced `if/elif` chain with a `case` statement for cleaner argument handling.
- The error trap now captures the failing command in the alert message.
---
### 7\. Scheduling with crontab
Because the script bundles both the backup and retention logic, the crontab ends up extremely simple.
Here’s a practical example (`sudo crontab -e` on the Proxmox host):
```bash
# ┌─ minute (0‑59)
# │ ┌─ hour (0‑23)
# │ │ ┌─ day‑of‑month (1‑31)
# │ │ │ ┌─ month (1‑12)
# │ │ │ │ ┌─ day‑of‑week (0‑7) (0|7 = Sunday)
# │ │ │ │ │
# │ │ │ │ │ command
# │ │ │ │ │
15 3 * * * /usr/local/bin/backup.sh backup /tank/cloud cloud >>/var/log/restic-cloud.log 2>&1
45 3 * * * /usr/local/bin/backup.sh backup /tank/photos photos >>/var/log/restic-photos.log 2>&1
00 4 * * * /usr/local/bin/backup.sh backup /tank/proxmox-backups proxmox >>/var/log/restic-pbs.log 2>&1
# Weekly prune every Sunday at 05:00
00 5 * * 0 /usr/local/bin/backup.sh cleanup >>/var/log/restic-cleanup.log 2>&1
```
**Why cron, not Systemd?**
- ✅ Less moving parts — everyone understands crontab.
- ✅ No unit files to manage; one‑liner edits are enough.
- ✅ Logs are redirected to plain files **and** Telegram, so I still get alerts if something fails.
*(If you prefer Systemd, just swap the cron entries for timers — the script itself doesn’t care.)*
---
### 8\. Restoration test (don’t skip this!)
```bash
# Verify repository integrity
restic -r rest:http://:@:8000 \
-p /etc/restic-key \
check
# Restore the latest snapshot to a test directory
restic -r rest:http://:@:8000 \
-p /etc/restic-key \
restore latest --target /tmp/restore-test
```
Run `restic check` regularly—it verifies that all data blobs are present and intact. Then do a real restore and verify the output. Boot a VM from the restored `vzdump`, diff a directory, spot-check a few files. **Backups you never test are merely archives.**
---
### 9\. Hardening & extras
- **Append-only mode** — Already enabled in our Compose file. This is your strongest defense against a compromised client deleting backups. Run `forget` and `prune` only from the server.
- **HTTP basic auth** — One user per client (`htpasswd -B ./htpasswd/credentials `). Easier to audit and revoke.
- **Read-only sub-account** on the Hetzner Storage Box to prevent deletion at the storage layer too.
- **Prometheus + Grafana** — Scrape `/metrics` from rest-server. The official repo includes a [ready-made Grafana dashboard](https://github.com/restic/rest-server/tree/master/examples/compose-with-grafana?ref=fathi.me).
- **Rotate the restic password** periodically with `restic key passwd`. Keep the old key in a secure location until you’ve verified the rotation.
- **Automatic updates** — Use Watchtower or a scheduled `apt upgrade` to keep restic and rest-server current.
- **Firewall** — Even with Tailscale, configure `ufw` or `nftables` on the VPS to only allow traffic from the Tailscale interface.
---
### 10\. TL;DR checklist
1. 🔐 Generate a strong repository password (`pwgen -s 32`).
2. 🗄️ [Order Storage Box](https://l.fathi.me/hetzner?ref=fathi.me) → enable SMB → create sub‑account.
3. ☁️ Boot a 1 vCPU VPS in the same DC → join Tailscale.
4. 🐳 Deploy the `docker‑compose.yml` above.
5. 💻 Install restic on every client and drop the Bash script.
6. 📅 Schedule with cron (hourly, nightly, weekly).
7. 🧪 Restore a random file every m*onth.*
8. *📈 Add Telegram to get eyes on the process.*
Congratulations—you now have an **encrypted, versioned, off‑site** backup system that costs a few euros a month and scales from single‑board computers to entire datastores.
If you’d prefer a web UI over shell scripts and cron jobs, check out my follow-up post: [Zerobyte: A Web UI for restic That Replaces Your Backup Scripts](https://fathi.me/zerobyte-restic-web-ui-backup-automation/). It uses the same restic engine and can even connect to repositories you’ve already created with this guide.
Happy backing up!
### Automating Cloudflare DNS with Traefik Using Traeflare
URL: https://fathi.me/automating-cloudflare-dns-with-traefik-using-traeflare-2/
Last updated: 2026-04-06T10:42:53.000Z
Managing DNS records for multiple services can become tedious when running a modern infrastructure. Services often come and go, subdomains change, and you spend a lot of time hopping between your reverse proxy (like Traefik) and DNS providers (like Cloudflare) to keep everything in sync.
[**Traeflare**](https://github.com/m4tt72/traeflare?ref=fathi.me) is a simple yet powerful tool designed to automate this process. It automatically updates your Cloudflare DNS records based on the existing routes you’ve set up in Traefik. No more manual copying and pasting of hostnames or subdomains—Traeflare does the heavy lifting for you.
In this article, we’ll explore:
- **Why you need** [**Traeflare**](https://github.com/m4tt72/traeflare?ref=fathi.me)
- **How** [**Traeflare**](https://github.com/m4tt72/traeflare?ref=fathi.me) **works**
- **Setting up** [**Traeflare**](https://github.com/m4tt72/traeflare?ref=fathi.me) **with Docker**
- **Key environment variables**
- **Pruning old records**
- **Best practices**
By the end, you’ll have a clear understanding of how to use [Traeflare](https://github.com/m4tt72/traeflare?ref=fathi.me) to streamline DNS management for your Traefik-based infrastructure.
---
## Why Use Traeflare?
### 1\. Automatic DNS Updates
If you’re frequently adding or removing services behind Traefik, updating DNS records in Cloudflare can become repetitive. Traeflare listens to your Traefik configuration and **automatically** syncs DNS entries to Cloudflare, saving you time and reducing the risk of typos or misconfigurations.
### 2\. Single Source of Truth
Traefik is already the central entry point for your services—why not make it your source of truth for DNS as well? By extracting the hostnames/routes from Traefik and mapping them to Cloudflare records, you can keep everything consistent in one place.
### 3\. Fewer Configuration Mistakes
Manual DNS updates are prone to human error. A single character off in your domain or subdomain could lead to confusion, downtime, or connectivity problems. Traeflare eliminates these mistakes by programmatically handling updates based on your existing Traefik config.
---
## How Traeflare Works
1. **Fetch Traefik Routes**
Traeflare queries the Traefik API (exposed on port `8080` by default) to retrieve configured routes (hostnames).
2. **Compare with Cloudflare**
It then contacts the Cloudflare API to see which DNS entries are already present in your chosen domain’s zone.
3. **Update or Create**
If a subdomain (from Traefik routes) doesn’t exist in Cloudflare, Traeflare creates a new DNS record for you. If it already exists, Traeflare ensures it’s updated with the correct IP address or CNAME as needed.
4. **Prune Old Records** *(Optional)*
With `PRUNE_RECORDS` set to `true`, any outdated or unused records in Cloudflare can be removed automatically, keeping your DNS zone clean and up to date.
---
## Getting Started: Docker Setup
Most users will run Traeflare as a Docker container alongside Traefik. Below is a sample `docker-compose.yml` snippet:
```yaml
version: "3"
services:
traefik:
image: traefik:latest
container_name: traefik
# ... your traefik config goes here ...
ports:
- "80:80"
- "443:443"
- "8080:8080" # Traefik dashboard/API
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
traeflare:
image: ghcr.io/m4tt72/traeflare:main
container_name: traeflare
env_file: .env
depends_on:
- traefik
restart: unless-stopped
```
### `.env` File Configuration
Create a `.env` file in the same directory:
```bash
TRAEFIK_API_URL=http://traefik:8080
CF_API_URL=https://api.cloudflare.com/client/v4
CF_ZONE_ID=your_zone_id
CF_API_EMAIL=your_cloudflare_email
CF_API_KEY=your_cloudflare_global_api_key
CF_DNS_API_TOKEN=your_cloudflare_dns_api_token
DOMAIN_NAME=example.com
RECORD_TYPE=A
PROXIED=true
PRUNE_RECORDS=true
```
You only need either `CF_API_KEY` **or** `CF_DNS_API_TOKEN`, depending on how you’ve set up Cloudflare authentication. If you use `CF_DNS_API_TOKEN`, it must have appropriate permissions to edit DNS records for the specified zone.
---
## Key Environment Variables
| Variable | Description | Default |
| ------------------- | --------------------------------------------------------------------- | ------------------------------------ |
| TRAEFIK\_API\_URL | The URL where Traefik’s API is exposed. Typically http://traefik:8080 | http://traefik:8080 |
| CF\_API\_URL | The base URL for the Cloudflare API | https://api.cloudflare.com/client/v4 |
| CF\_ZONE\_ID | Unique Zone ID for the domain in Cloudflare | *(none)* |
| CF\_API\_EMAIL | Cloudflare account email (if using Global API Key) | *(none)* |
| CF\_API\_KEY | Global API Key from Cloudflare | *(none)* |
| CF\_DNS\_API\_TOKEN | DNS-specific API token with edit permissions | *(none)* |
| DOMAIN\_NAME | The domain name you want to update (e.g. example.com) | *(none)* |
| RECORD\_TYPE | DNS record type (A or CNAME) | A |
| PROXIED | Whether to enable Cloudflare’s proxying feature (true/false) | true |
| PRUNE\_RECORDS | Remove DNS records not matching current Traefik routes (true/false) | true |
### Choosing the Right Authentication Method
- **Global API Key (using `CF_API_KEY` \+ `CF_API_EMAIL`)**: This key has full account privileges. This can be easier to set up but less secure if you only need DNS changes.
- **Scoped API Token (using `CF_DNS_API_TOKEN`)**: This token can be restricted to DNS operations for a single zone, which is a more secure practice.
---
## Usage & Workflow
1. **Start Traefik**
Ensure your Traefik container is running with the routes you want to expose.
2. **Run Traeflare**
Start the Traeflare container. It will connect to the Traefik API, list all hostnames, and then create or update DNS records on Cloudflare.
3. **Check Cloudflare**
Log in to your Cloudflare dashboard and confirm the newly created or updated DNS records. If `PRUNE_RECORDS` is set to `true`, old or unused records will automatically be removed.
4. **Add/Remove Services**
Whenever you add or remove a service in Traefik, simply wait for the Traeflare container to pick up the changes. It’s designed to continuously watch for updates (depending on your configuration or how often you restart/refresh the container).
---
## Pruning Records
By default, Traeflare is set to prune old records (`PRUNE_RECORDS=true`). This means that if you remove a route from Traefik, Traeflare will detect it’s no longer present and remove the corresponding DNS entry from Cloudflare.
- **Pros**: Keeps DNS zone neat; no leftover subdomains.
- **Cons**: If you manually created DNS entries in the same zone (e.g., for email or external services), make sure they aren’t named identically to something Traeflare might handle. You could disable pruning or maintain those records with a separate approach.
---
## Best Practices
1. **Use a Separate Cloudflare API Token**
Limit your token’s scope to DNS changes for a single domain. This follows the principle of least privilege.
2. **Monitor Logs**
Check container logs for errors, especially in the initial setup phase. You’ll see logs for each DNS record it attempts to create or update.
3. **Version Control Your Docker Compose**
Keep your `docker-compose.yml` and `.env` in a private repository to maintain a record of changes and credentials (make sure `.env` is in `.gitignore`).
4. **Set Up Alerts**
Use monitoring tools to alert you if DNS updates fail or if the Traeflare container stops unexpectedly.
5. **Testing**
Test in a staging environment or with a subdomain before deploying to production. Confirm that DNS changes propagate correctly.
---
## Conclusion
**Traeflare** removes much of the manual overhead involved in managing DNS records for services behind Traefik. By leveraging the Traefik API, Traeflare updates (and optionally prunes) DNS entries in Cloudflare automatically, ensuring that your public-facing routes remain accurate and up to date.
With minimal configuration—just a Docker container and a few environment variables—you can unify your reverse proxy and DNS management strategy. This streamlined approach offers significant time savings, fewer errors, and a more resilient infrastructure overall.
---
### Further Reading
- [Traeflare GitHub Repository](https://github.com/m4tt72/traeflare?ref=fathi.me)
- [Traefik Documentation](https://doc.traefik.io/traefik/?ref=fathi.me)
- [Cloudflare API Documentation](https://api.cloudflare.com/?ref=fathi.me)
Have questions or feedback? Feel free to open an issue on the [GitHub repo](https://github.com/m4tt72/traeflare/issues?ref=fathi.me) or reach out directly. We’d love to hear about your experience with Traeflare in production!
### Decrypting Rails Cookies in Go
URL: https://fathi.me/decrypting-rails-cookies-in-go/
Last updated: 2026-04-06T10:42:53.000Z
Modern web applications often rely on cookies to manage sessions, maintain user state, and store important data that needs to persist across multiple requests. In Ruby on Rails applications, cookies are typically encrypted and signed to protect against tampering. This ensures that sensitive information stored in cookies remains secure and that malicious actors cannot easily forge or alter data.
However, in a microservices architecture—where a Rails application might need to share data with services written in other languages (such as Go)—you could find yourself needing to decrypt and verify Rails-encrypted cookies in non-Rails environments. That’s where [**rails-cookie-decrypt-go**](https://github.com/m4tt72/rails-cookie-decrypt-go?ref=fathi.me) comes in. This lightweight library allows you to decrypt and access the contents of a Rails cookie in a Go application, enabling seamless interoperability across different services.
In this article, we will dive into:
- **Why decrypting Rails cookies in Go is useful**
- **Key features of the `rails-cookie-decrypt-go` library**
- **Detailed usage examples**
- **Real-world use cases and best practices**
By the end, you’ll have a full understanding of how to incorporate `rails-cookie-decrypt-go` into your projects and what it can offer for your multi-language, micro-services-based architectures.
---
## Why Decrypt Rails Cookies in Go?
### 1\. Micro-services Interoperability
In many modern infrastructures, services are written in different programming languages. A Rails application might handle authentication and session management, but downstream services—like analytics, logging, or custom business logic—could be written in Go. If you need to verify user sessions or retrieve data stored in the Rails session cookie, you need a reliable way to decrypt that cookie on the Go side.
### 2\. Improved Performance
Go is well-known for its speed and concurrency features. By offloading certain tasks (like cookie decryption and parsing) to a Go service, you might see performance improvements, especially under high load. This library is designed to be lightweight, so it can quickly decrypt Rails cookies in a Go micro-service.
### 3\. Code Re-usability & Simpler Workflows
Handling cookie decryption in a single place can simplify your micro-service architecture. Rather than building out an entire HTTP request back to Rails for each decryption request, you can process the cookies directly within your Go service. This reduces complexity and cuts down on network overhead.
---
## Understanding Rails Cookie Encryption
Before we get into how `rails-cookie-decrypt-go` works, let’s do a quick refresher on how Rails handles cookie encryption:
- **`secret_key_base`**: Rails uses this secret for generating encrypted and signed cookies. It’s typically stored in your app’s encrypted credentials or an environment variable.
- **Encryption & Signing**: Rails can use algorithms like `sha1` or `sha256` (for generating the message digest), combined with AES-based encryption.
- **Cookie Structure**: A typical encrypted cookie in Rails includes the ciphertext and the authentication tag. Rails verifies and decrypts the cookie on each request to restore the session data.
`rails-cookie-decrypt-go` replicates the decryption mechanism that Rails uses. It allows your Go application to verify the cookie’s authenticity and decrypt the payload, giving you the original data as a Go data structure (often a string or JSON, depending on how the Rails cookie is structured).
---
## Getting Started
### 1\. Installation
To install `rails-cookie-decrypt-go`, use the standard Go package manager:
```bash
go get github.com/m4tt72/rails-cookie-decrypt-go
```
### 2\. Importing in Your Code
```go
import "github.com/m4tt72/rails-cookie-decrypt-go"
```
---
## Basic Usage
Here’s a simple example that demonstrates how to use the library to decrypt a Rails cookie:
```go
package main
import (
"fmt"
rails_cookie_decrypt "github.com/m4tt72/rails-cookie-decrypt-go"
)
func main() {
// This is an example encrypted cookie from a Rails app
cookieValue := "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkltZ3Rvc2V2Y..." // truncated for brevity
// Options for the decryption process
options := rails_cookie_decrypt.Options{
SecretKeyBase: "your-rails-secret-key-base",
Digest: "sha256", // can be sha1 or sha256, based on your Rails config
Unescape: true, // whether to unescape the cookie value before decryption
}
// Decrypt the cookie
decryptedValue, err := rails_cookie_decrypt.Decrypt(cookieValue, options)
if err != nil {
fmt.Println("Error decrypting cookie:", err)
return
}
fmt.Println("Decrypted cookie value:", decryptedValue)
}
```
### Key Points
1. `**SecretKeyBase**`: Must match the `secret_key_base` your Rails application uses.
2. `**Digest**`: Rails, by default in more recent versions, uses `sha256`. Older versions might use `sha1`. Confirm your Rails configuration.
3. `**Unescape**`: Some cookies come URL-escaped. Set this to `true` if your cookie is escaped.
---
## Handling Different Configurations
Rails can be configured to use different message digest algorithms (`sha1` vs. `sha256`). By default, new Rails applications tend to use `sha256`. If you’re working on an older Rails app, you might need `sha1`.
**Example for an older Rails app**:
```go
options := rails_cookie_decrypt.Options{
SecretKeyBase: "legacy-rails-secret-key-base",
Digest: "sha1",
Unescape: false,
}
```
---
## Parsing the Decrypted Payload
Once you decrypt a cookie, the returned value can be a variety of formats:
- **Raw String**: Sometimes, the session data may simply be a serialized string.
- **JSON**: If your Rails app stores JSON or a serialized hash in the cookie, you can unmarshal that in Go.
- **Marshalled Data**: Older Rails versions might use Ruby’s Marshal format. If you run into that, you may need additional parsing logic.
For JSON-encoded cookies, you can do:
```go
// after decryption
var data map[string]interface{}
err := json.Unmarshal([]byte(decryptedValue), &data)
if err != nil {
fmt.Println("Invalid JSON:", err)
return
}
fmt.Println("JSON Data:", data)
```
---
## Advanced Examples & Real-World Usage
### 1\. Authenticating Users in a Go Micro-service
Imagine you have a Rails front-end (or any Rails-based back-end) that stores user authentication data in a session cookie. In a micro-services architecture, you have a separate Go service that needs to identify who the user is. One approach:
1. **User requests** a resource from the Go service, attaching the cookie (originating from Rails).
2. **Go service** decrypts the cookie using `rails-cookie-decrypt-go`, checks the user ID or session data inside it.
3. **Authorize or reject** the user based on the decrypted data.
This avoids making an internal API request back to the Rails app just to verify user identity, saving time and resources.
### 2\. Logging & Monitoring
You might store user or session metadata in the cookie, such as time of login or user preferences. A separate Go-based logging or analytics service could decrypt the cookie and:
- **Record user activity** in a time-series database.
- **Aggregate usage metrics** by user or session.
- **Generate real-time dashboards** without involving the main Rails codebase.
### 3\. Feature Flags & A/B Testing
If you store feature flag or A/B testing group assignments in the cookie, a Go-based micro-service that handles background job processing or user segmentation can directly read the cookie data. This helps keep front-end and back-end logic consistent across multiple languages.
---
## Troubleshooting & Best Practices
1. **Keep Secrets Secure**
Make sure your `secret_key_base` is protected. Do not commit secrets to version control and use environment variables or secure secret managers.
2. **Know Your Rails Version**
Confirm whether your Rails application uses `sha1` or `sha256` (or another algorithm) for its message digest. This should match in your `rails-cookie-decrypt-go` options.
3. **Watch out for Marshalling**
Older Rails apps may store session data in Ruby’s Marshal format. If you see weird gibberish after decryption, you might need to parse a Marshal format or update your Rails app to use JSON-based cookies (via `ActionDispatch::Cookies::JSONSerializer`).
4. **Validate Your Cookie**
Cookie tampering can still occur if you’re not careful. The library verifies the signature, but always double-check that the data you’re expecting is present. Implement additional guardrails as needed for your use case.
5. **Performance Considerations**
While decrypting a single cookie is typically fast, if you’re processing large volumes, consider caching or reusing decrypted session data. Also ensure that your micro-service has the computational resources to handle bursts of requests.
---
## Conclusion
`rails-cookie-decrypt-go` is a straightforward yet powerful tool that allows Go applications to decrypt and verify Rails cookies. It’s particularly useful in micro-services architectures where different components of the system might be written in different languages but need to share session or user data.
### Key Takeaways
- **Interoperability**: Easily handle Rails cookies in Go without complex workarounds.
- **Performance**: Offload cookie decryption to a fast, concurrent service.
- **Flexibility**: Works for various Rails configurations (`sha1` or `sha256`, escaped or unescaped cookies).
- **Simplicity**: A few lines of code are all you need to decrypt and read the cookie’s payload.
If your infrastructure demands a cross-language approach to session management, or you simply want the speed and simplicity of Go when working with Rails cookies, give `rails-cookie-decrypt-go` a try. With its minimal setup and robust capabilities, you’ll be decrypting and verifying cookies in no time.
**Happy coding!**
---
### Further Reading & Resources
- [GitHub Repository: rails-cookie-decrypt-go](https://github.com/m4tt72/rails-cookie-decrypt-go?ref=fathi.me)
- [Rails Security Guide: Encrypting & Signing Cookies](https://guides.rubyonrails.org/security.html?ref=fathi.me#sessions)
- [Action Dispatch Cookies in Rails](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html?ref=fathi.me)
- [Go Documentation: JSON](https://pkg.go.dev/encoding/json?ref=fathi.me)
If you have any questions or run into issues, feel free to open an issue on the GitHub repository or reach out to me directly. Feedback and contributions to the project are always welcome!
### Unlock Secure Freedom - Route All Traffic Through Tailscale + Gluetun
URL: https://fathi.me/unlock-secure-freedom-route-all-traffic-through-tailscale-gluetun/
Last updated: 2026-04-06T10:42:54.000Z
## Introduction
Ever wished you could route all your devices’ traffic through a single, secure VPN—without jumping through too many hoops? Meet Tailscale, a powerful yet user-friendly VPN service that easily links all your devices under one private network. In this guide, we’ll pair Tailscale with Gluetun on a VPS, creating a robust setup that routes your entire connection securely through another VPN provider.
## Prerequisites
Make sure you have the following on hand before you start:
- \[ \] A [Tailscale account](https://tailscale.com/?ref=fathi.me)
- \[ \] A VPS running Docker (no worries, we won’t use this VPS as an exit node; it’s just your secure hub)
- **Recommendation**: [Hetzner](https://l.fathi.me/hetzner?ref=fathi.me) offers excellent performance at a great price. (This is an affiliate link.)
- \[ \] A VPN provider account (we’ll use Gluetun for this guide)
- **Recommendation**: [Windscribe](https://l.fathi.me/windscribe?ref=fathi.me) is a reliable, privacy-focused VPN provider. (This is an affiliate link.)
## Step-by-Step Instructions
### 1\. Install Docker and Docker Compose on Your VPS
First, install Docker with a handy one-liner, then add your current user to the Docker group:
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
```
### 2\. Use This Docker Compose Configuration
Create a docker-compose.yml with the following content. This configuration pulls in Gluetun for VPN functionality and Tailscale for seamless, private networking. Tailscale will advertise itself as an exit node, allowing you to route all traffic through it.
```yaml
volumes:
ts-data:
services:
# For additional VPN service providers, see: https://github.com/qdm12/gluetun-wiki
gluetun:
image: qmcgaw/gluetun
restart: unless-stopped
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=${PROVIDER}
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${VPN_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${VPN_ADDRESSES}
- WIREGUARD_PRESHARED_KEY=${VPN_PRESHARED_KEY}
- SERVER_REGIONS=${SERVER_REGIONS}
tailscale-vpn-exit-node:
image: tailscale/tailscale:latest
container_name: tailscale-vpn-exit-node
network_mode: service:gluetun
environment:
- TS_AUTHKEY=${TAILSCALE_AUTHKEY}
- TS_EXTRA_ARGS=--advertise-exit-node # or --advertise-tags=tag:vpn
- TS_STATE_DIR=/var/lib/tailscale
- TS_HOSTNAME=vpn-${SERVER_REGIONS}
volumes:
- ts-data:/var/lib/tailscale
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- NET_ADMIN
- NET_RAW
restart: unless-stopped
depends_on:
gluetun:
condition: service_healthy
```
### 3\. Spin It All Up
Fire up your new setup:
```bash
docker-compose up -d
```
### 4\. Connect Your Devices
- Install Tailscale on each device you want to secure.
- In the Tailscale admin panel, enable your newly created exit node.
- Connect to Tailscale—now your traffic will be safely routed through Gluetun.
### 5\. Bask in Secure Browsing
That’s it! Your devices are now shielded behind Gluetun’s VPN, with Tailscale delivering a seamless private connection experience. Browse, stream, and work securely on any network.
Conclusion
By pairing Tailscale and Gluetun, you’ve built a reliable, encrypted path for all your internet traffic. No complicated configurations or manual networks—just straightforward, powerful privacy.
# Further Reading
- [tailscale](https://tailscale.com/?ref=fathi.me)
- [hetzner](https://l.fathi.me/hetzner?ref=fathi.me)
- [windscribe](https://l.fathi.me/windscribe?ref=fathi.me)
- [gluetun-wiki](https://github.com/qdm12/gluetun-wiki?ref=fathi.me)
### Monitor a website for changes
URL: https://fathi.me/monitor-a-website-for-changes/
Last updated: 2026-04-06T10:42:54.000Z
## Introduction
This article will show you how to monitor a website for changes. This is useful if you want to be notified when a website changes, for example, when a new blog post is published, a black friday sale starts, or an item is back in stock.
## Prerequisites
- \[ \] A [Telegram](https://telegram.org/?ref=fathi.me) account
- \[ \] An ubuntu server with [Docker](https://docs.docker.com/engine/install/ubuntu/?ref=fathi.me) installed
- \[ \] [Docker compose](https://docs.docker.com/compose/install/?ref=fathi.me) installed
## Steps
### 1\. Create a Telegram bot
1. Open Telegram and search for `@BotFather`
2. Send `/newbot` to `@BotFather`
3. Enter a name for your bot
4. Enter a username for your bot
5. Copy the token that `@BotFather` gives you
### 2\. Create a Telegram chat
1. Open Telegram and search for `@userinfobot`
2. Send `/start` to `@userinfobot`
3. Send `/my_id` to `@userinfobot`
4. Copy the chat id that `@userinfobot` gives you
### 3\. Create a Docker compose file
1. On your ubuntu server, create a file called `docker-compose.yml` with the following content:
```yaml
version: "3.9"
volumes:
data:
services:
changedetection:
image: dgtlmoon/changedetection.io:dev
container_name: changedetection
hostname: changedetection
volumes:
- data:/datastore
environment:
- PORT=5000
- PUID=1000
- PGID=1000
- WEBDRIVER_URL=http://browser-chrome:4444/wd/hub
- PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000/?stealth=1&--disable-web-security=true
- BASE_URL=http://localhost:5000
restart: unless-stopped
ports:
- 5000:5000
browser-chrome:
hostname: browser-chrome
image: selenium/standalone-chrome-debug:3.141.59
environment:
- VNC_NO_PASSWORD=1
- SCREEN_WIDTH=1920
- SCREEN_HEIGHT=1080
- SCREEN_DEPTH=24
volumes:
- /dev/shm:/dev/shm
restart: unless-stopped
playwright-chrome:
hostname: playwright-chrome
image: browserless/chrome
restart: unless-stopped
environment:
- SCREEN_WIDTH=1920
- SCREEN_HEIGHT=1024
- SCREEN_DEPTH=16
- ENABLE_DEBUGGER=false
- PREBOOT_CHROME=true
- CONNECTION_TIMEOUT=300000
- MAX_CONCURRENT_SESSIONS=10
- CHROME_REFRESH_TIME=600000
- DEFAULT_BLOCK_ADS=true
- DEFAULT_STEALTH=true
```
### 4\. Start the Docker containers
1. On your ubuntu server, run the following command to start the Docker containers:
```bash
docker-compose up -d
```
### 5\. Add a website to monitor
1. Open a web browser and navigate to `http://localhost:5000`

1. Paste the URL of the website you want to monitor in the `URL` field

1. Click `Edit > Watch`
2. Under time between checks, select your preferred interval

1. Under the `Request` tab, set `Fetching method` to `Playwright Chromium/Javascript via 'ws://playwright-chrome:3000/?stealth=1&--disable-web-security=true'` and `Wait seconds before extracting text` to a number higher than 15
2. Under the `Notifications` tab, enter the Telegram chat id and token you copied earlier as the following format:
```text
tgram://:
```
1. Click `Save`
You should now receive a notification when the website changes.
## Conclusion
In this article, you learned how to monitor a website for changes. This is useful if you want to be notified when a website changes, for example, when a new blog post is published, a black friday sale starts, or an item is back in stock.
## Resources
- \[ \] [changedetection.io](https://changedetection.io/?ref=fathi.me)
### Automatically sync Traefik records with Cloudflare DNS using Traeflare
URL: https://fathi.me/automatically-sync-traefik-records-with-cloudflare-dns-using-traeflare/
Last updated: 2026-04-06T10:44:02.000Z
**This article has been superseded.** A more comprehensive version is available: [Automating Cloudflare DNS with Traefik Using Traeflare](https://fathi.me/automating-cloudflare-dns-with-traefik-using-traeflare-2/).
This was the original guide for setting up Traeflare. It has been replaced by a [more detailed, updated version](https://fathi.me/automating-cloudflare-dns-with-traefik-using-traeflare-2/) that covers the full configuration, environment variables, and integration in depth.
Please head over to the [updated article](https://fathi.me/automating-cloudflare-dns-with-traefik-using-traeflare-2/) for the latest instructions.