Ansible for Small Server Fleets

Introduction to Ansible

Ansible is an open-source automation tool that helps system administrators manage and configure server fleets of any size. It uses a declarative configuration model, which means that instead of writing scripts to perform specific tasks, you define the desired state of your servers and let Ansible figure out how to achieve it. This approach makes it easier to manage complex systems and ensures consistency across your fleet.

Ansible uses a agentless architecture, which means that it doesn't require any additional software to be installed on the servers it manages. Instead, it uses SSH to connect to the servers and execute tasks. This makes it a great choice for managing small to medium-sized server fleets, as it's easy to set up and requires minimal overhead.

Key Concepts in Ansible

Before diving into the configuration, it's essential to understand some key concepts in Ansible. The core components of Ansible are playbooks, plays, tasks, and modules. A playbook is the top-level configuration file that defines the desired state of your servers. A play is a set of tasks that are executed on a specific group of servers. A task is a single action that is executed on a server, such as installing a package or starting a service. A module is a reusable piece of code that performs a specific task, such as managing users or configuring network interfaces.

Another crucial concept in Ansible is inventory. The inventory is a list of servers that Ansible manages, along with their respective IP addresses, usernames, and passwords. You can define your inventory in a simple text file or use a more advanced inventory management system like AWS EC2 or OpenStack.

Creating Your First Playbook

Now that you understand the key concepts in Ansible, let's create your first playbook. A playbook is defined in a YAML file, which is easy to read and write. Create a new file called site.yml and add the following content:


---
- name: Configure web server
  hosts: web_servers
  become: yes

  tasks:
  - name: Install Apache
    apt:
      name: apache2
      state: present

  - name: Start Apache
    service:
      name: apache2
      state: started
      enabled: yes

This playbook defines a single play that targets the web_servers group. The become directive specifies that Ansible should use sudo to execute tasks with elevated privileges. The playbook contains two tasks: one to install Apache and another to start the Apache service.

Defining Your Inventory

To use this playbook, you need to define your inventory. Create a new file called hosts and add the following content:


[web_servers]
server1 ansible_host=192.168.1.100
server2 ansible_host=192.168.1.101

This inventory defines a group called web_servers that contains two servers: server1 and server2. The ansible_host directive specifies the IP address of each server.

Running Your Playbook

To run your playbook, execute the following command:

ansible-playbook -i hosts site.yml
. This command tells Ansible to use the hosts inventory file and execute the site.yml playbook. Ansible will connect to each server in the web_servers group, install Apache, and start the Apache service.

To verify that the playbook was executed successfully, you can use the

ansible -i hosts web_servers -m ping
command. This command uses the ping module to test connectivity to each server in the web_servers group.

Diagnostic Commands

Ansible provides several diagnostic commands that help you troubleshoot issues with your playbooks. The

ansible-playbook --list-tasks site.yml
command lists all the tasks defined in your playbook. The
ansible-playbook --list-tags site.yml
command lists all the tags defined in your playbook.

You can also use the

ansible-playbook -v site.yml
command to increase the verbosity of the output. This command provides more detailed information about the execution of your playbook, including the tasks that are being executed and any errors that occur.

Best Practices for Using Ansible

To get the most out of Ansible, follow these best practices: use version control to manage your playbooks and inventory files, use roles to organize your playbooks and reuse code, and use tags to selectively execute tasks. Additionally, always test your playbooks in a staging environment before deploying them to production.

By following these best practices and using Ansible to manage your server fleet, you can simplify your workflow, reduce errors, and improve the overall reliability of your systems.

Secure File Sync Between Linux Servers with Syncthing and WireGuard (Step-by-Step)

Why Syncthing + WireGuard is a smart choice

When you need reliable file synchronization between Linux servers, it’s tempting to reach for classic tools like rsync over SSH or an NFS share. Those options still work, but they can be painful when you have multiple sites, changing IP addresses, strict firewalls, or you want near real-time updates without a central storage server. A modern approach is to combine Syncthing (continuous, peer-to-peer file sync) with WireGuard (fast, secure VPN). You get encrypted transport, stable private IPs, and a sync tool that can handle intermittent connectivity gracefully.

This tutorial shows how to set up Syncthing to sync a directory between two Linux servers over a WireGuard tunnel. The result is a private “always-on” sync link that doesn’t require exposing Syncthing to the public internet.

What you’ll build

You will configure:

Server A: 10.10.10.1 (WireGuard interface: wg0)

Server B: 10.10.10.2 (WireGuard interface: wg0)

Syncthing will bind to the WireGuard interface so sync traffic stays inside the VPN. We’ll also harden firewall rules and enable Syncthing as a system service.

Prerequisites

Before you start, make sure both servers have sudo access and can reach each other on the internet (at least one side needs a reachable UDP port for WireGuard). You should also know which folder you want to sync, for example /srv/sync. This guide assumes Ubuntu/Debian-style commands; on RHEL/Fedora you can adapt package manager commands accordingly.

Step 1: Install WireGuard on both servers

On both servers, install WireGuard:

Debian/Ubuntu: sudo apt update && sudo apt install -y wireguard

Enable IP forwarding is not required for a simple point-to-point sync tunnel, but it doesn’t hurt to keep routing simple and only use the tunnel addresses for Syncthing.

Step 2: Create WireGuard keys

On each server, generate a keypair:

umask 077
wg genkey | tee ~/wg-private.key | wg pubkey > ~/wg-public.key

Copy each server’s wg-public.key to the other side. Keep private keys private.

Step 3: Configure the WireGuard tunnel

On Server A, create /etc/wireguard/wg0.conf:

[Interface]
Address = 10.10.10.1/24
PrivateKey = SERVER_A_PRIVATE_KEY
ListenPort = 51820

[Peer]
PublicKey = SERVER_B_PUBLIC_KEY
AllowedIPs = 10.10.10.2/32
PersistentKeepalive = 25

On Server B, create /etc/wireguard/wg0.conf:

[Interface]
Address = 10.10.10.2/24
PrivateKey = SERVER_B_PRIVATE_KEY

[Peer]
PublicKey = SERVER_A_PUBLIC_KEY
Endpoint = SERVER_A_PUBLIC_IP:51820
AllowedIPs = 10.10.10.1/32
PersistentKeepalive = 25

Start and enable the tunnel on both servers:

sudo systemctl enable --now wg-quick@wg0

Test connectivity:

ping -c 3 10.10.10.2 (from Server A)
ping -c 3 10.10.10.1 (from Server B)

Step 4: Install Syncthing on both servers

Install Syncthing from your distro repo or the official package source. On Debian/Ubuntu, the repo version may be older, but it still works. For a straightforward setup:

sudo apt update && sudo apt install -y syncthing

Step 5: Run Syncthing as a service (recommended)

Create a dedicated user (optional but clean) and run Syncthing under it. For a fast setup using your current user, enable the user service:

systemctl --user enable --now syncthing

If you prefer a system-wide service tied to a specific account:

sudo systemctl enable --now [email protected]

Step 6: Bind Syncthing to WireGuard only

To keep sync traffic inside the VPN, open Syncthing’s Web UI locally (or via SSH port forwarding) and adjust settings:

1) In Settings > Connections, set Listen Addresses to include the WireGuard IP, for example: tcp://10.10.10.1:22000 (Server A) and tcp://10.10.10.2:22000 (Server B).

2) Optionally disable global discovery and relays for a pure VPN setup: turn off Global Discovery and Enable Relaying. This reduces external dependencies and noise.

Step 7: Pair the devices and add a synced folder

In the Syncthing Web UI on Server A, click Add Remote Device, paste Server B’s Device ID, and save. Do the same in the other direction if it doesn’t auto-accept. Then add a folder such as /srv/sync on Server A and share it with Server B. On Server B, accept the share and choose the local path where files should land.

If you’re syncing application data, be mindful of file locks and databases. For PostgreSQL/MySQL, sync dumps or backups instead of live database files. For configs, scripts, and documents, Syncthing is a perfect fit.

Step 8: Firewall tips for a locked-down setup

At minimum, allow WireGuard UDP on the server that listens publicly (Server A in this example). With UFW:

sudo ufw allow 51820/udp

You do not need to expose Syncthing ports to the internet if it’s bound to the WireGuard IP. If you manage Syncthing’s UI remotely, use SSH port forwarding rather than opening the GUI port globally.

Troubleshooting checklist

No tunnel connection: verify public IP/port, confirm keys, and check sudo wg show for latest handshake times.

Devices don’t see each other: confirm Syncthing is listening on the WireGuard IP and that you used the correct Device IDs. Test with nc -vz 10.10.10.2 22000 across the tunnel.

Permissions problems: ensure the Syncthing service user can read/write the synced folder. Fix with ownership or ACLs.

Final notes

With Syncthing running over WireGuard, you get a clean and modern file sync stack: encrypted transport, stable addressing, and continuous synchronization without exposing extra services to the public internet. This approach scales nicely as you add more servers—just add peers to WireGuard and devices to Syncthing, then share the folders you need.

How to Build a Private AI Coding Assistant with Ollama and Open WebUI on Linux (No Cloud Required)

Running an AI assistant locally is no longer just a hobby project. With today’s open models and lightweight runtimes, you can build a private “coding helper” that works even when the internet is down, keeps your prompts off third-party servers, and still feels fast enough for daily use. In this tutorial, you’ll install Ollama (a local LLM runtime) and Open WebUI (a clean web interface) on a Linux machine, then connect them and load a practical code-focused model.

This setup is ideal for admins, developers, and helpdesk teams who want quick answers for scripting, log parsing, config explanations, and command-line guidance without sending internal details to a cloud AI provider.

What You’ll Need

Requirements: A modern Linux distro (Ubuntu/Debian/Fedora are all fine), at least 8 GB RAM (16 GB recommended), and 15–30 GB free disk space depending on the model. CPU-only is supported; if you have a compatible GPU, responses may be faster, but it is not required for a functional installation.

Step 1: Install Ollama

Ollama provides a simple way to download and run large language models locally. On most distributions, the fastest method is the official install script. Open a terminal and run:

Command:
curl -fsSL https://ollama.com/install.sh | sh

After installation, confirm the service is available:

Command:
ollama --version

If you’re on a systemd-based distro, Ollama usually starts automatically. If not, start it manually or check the service status:

Commands:
sudo systemctl status ollama
sudo systemctl enable --now ollama

Step 2: Download a Coding-Friendly Model

You can choose different models depending on your hardware. For a balanced setup on a typical workstation, start with a mid-sized instruction model. For coding tasks, many users prefer models tuned for code completion and explanations.

Download a model with:

Example command:
ollama pull deepseek-coder:latest

Then test a quick prompt:

Example command:
ollama run deepseek-coder:latest

Type something like “Explain what a reverse proxy is in simple terms” or “Write a bash script to rotate logs.” If you get a response, the runtime is working.

Step 3: Install Docker (for Open WebUI)

Open WebUI is commonly deployed as a container. If Docker is not installed, install it using your distro’s package manager. On Ubuntu/Debian, this usually works:

Commands (Ubuntu/Debian example):
sudo apt update
sudo apt install -y docker.io
sudo systemctl enable --now docker

To avoid typing sudo for every Docker command, add your user to the docker group (log out and back in afterward):

Command:
sudo usermod -aG docker $USER

Step 4: Run Open WebUI and Connect It to Ollama

Now you’ll start Open WebUI and point it to Ollama’s API. By default, Ollama listens on http://localhost:11434. The container needs to reach the host service. On many Linux systems, you can use the host network mode to keep it simple.

Command:
docker run -d --name open-webui --restart always --network=host -e OLLAMA_BASE_URL=http://127.0.0.1:11434 -v open-webui:/app/backend/data ghcr.io/open-webui/open-webui:main

When it’s running, open your browser and visit:

URL: http://localhost:8080

Create an admin user when prompted. After login, Open WebUI should automatically detect available Ollama models. If you don’t see your model, check the model list from the terminal:

Command:
ollama list

Step 5: Make It Useful for Real Work (Practical Settings)

To get consistent, “helpdesk-ready” answers, create a default system prompt in Open WebUI that matches your environment. For example, set a short instruction like: “You are a Linux and networking assistant. Ask clarifying questions before suggesting risky commands. Provide commands with brief explanations.” This reduces accidental destructive advice and keeps responses focused.

For troubleshooting and scripting, you can also create saved prompts such as:

“Analyze this error log and list likely root causes in order.”
“Suggest a safe rollback plan before applying changes.”
“Convert this one-liner into a readable bash script with comments.”

Common Problems and Fixes

Open WebUI loads, but no models appear: Verify Ollama is running: systemctl status ollama. Then confirm the base URL is correct and reachable. If you didn’t use --network=host, the container may not reach localhost on the host.

Responses are slow: Use a smaller model, close other memory-heavy apps, and consider increasing swap. On limited hardware, a 7B model often feels far more responsive than a 13B+ model.

Disk space disappears quickly: Models are large. Remove unused models with ollama rm MODELNAME, and periodically review ollama list.

Next Steps: Hardening and Remote Access

Once everything works locally, you can place Open WebUI behind a reverse proxy (like Nginx) with HTTPS, restrict access by IP, and enable authentication. If you plan to share it with a small team, consider running it on a dedicated VM and keeping a strict update routine for the container image and the host OS.

With Ollama and Open WebUI, you get a clean, private AI assistant that can help with scripts, configs, and troubleshooting—without turning your internal prompts into someone else’s training data.

Set Up a Self-Hosted GitHub Actions Runner on Linux (Securely) for Faster CI/CD

Why self-hosted runners are worth it

If your builds are slow, your workflow uses special tools, or you need access to an internal network, a self-hosted GitHub Actions runner can be a game-changer. Instead of relying on GitHub-hosted runners (which are shared and time-limited), you run jobs on your own Linux machine. This tutorial shows how to set up a runner on Ubuntu Server with a clean, secure approach: a dedicated user, a systemd service, and basic hardening tips.

What you need before you start

You’ll need: (1) an Ubuntu Server 22.04/24.04 machine (VM or bare metal), (2) outbound internet access to GitHub, (3) a GitHub repository or organization where you can register runners, and (4) sudo privileges on the Linux host. For best results, use a separate machine or VM for CI jobs—treat it as disposable infrastructure.

Step 1: Update the server and install prerequisites

First, patch the system and install common dependencies used by build pipelines. Run the following commands:

sudo apt update && sudo apt -y upgrade

sudo apt -y install curl tar git ca-certificates

If your workflows build containers, install Docker later (and consider isolating it). For now, keep the base runner simple.

Step 2: Create a dedicated runner user

Avoid running CI as your personal account or as root. Create a dedicated user and a working directory:

sudo adduser --disabled-password --gecos "" actions

sudo mkdir -p /opt/actions-runner

sudo chown -R actions:actions /opt/actions-runner

This helps with least privilege and keeps runner files in a predictable location for maintenance.

Step 3: Download the GitHub Actions runner

Switch to the runner user and download the latest Linux x64 runner package. You can find the current version on GitHub’s official runner releases page, but the process is always the same:

sudo -iu actions

cd /opt/actions-runner

curl -o actions-runner-linux-x64.tar.gz -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-2.0.0.tar.gz

tar xzf actions-runner-linux-x64.tar.gz

Note: the filename in the URL can change as new versions are released. If you get a 404 error, open the releases page and copy the exact download link for Linux x64.

Step 4: Register the runner with your repo or organization

In GitHub, go to your repository: Settings > Actions > Runners > New self-hosted runner. Choose Linux, and GitHub will display a short set of commands including a registration token.

Back on your server (still as the actions user), run the configuration script using the URL and token GitHub provides:

./config.sh --url https://github.com/OWNER/REPO --token YOUR_TOKEN

When prompted, set a clear runner name (for example, ubuntu-ci-01) and add labels that match your use case (like linux, self-hosted, docker, gpu). Labels let you target specific runners in workflows.

Step 5: Install the runner as a systemd service

Running the runner in a terminal works, but it’s not reliable. Install it as a service so it starts on boot and restarts on failure:

sudo ./svc.sh install

sudo ./svc.sh start

Then verify status:

sudo ./svc.sh status

Within a minute, the runner should show as Idle in GitHub under Actions runners.

Step 6: Update a workflow to use your self-hosted runner

In your workflow YAML, set runs-on to include self-hosted plus any labels you assigned:

runs-on: [self-hosted, linux]

If you want to guarantee the job lands on a specific capability (like Docker), use a custom label such as docker and specify it in runs-on.

Security and hardening tips (don’t skip these)

A self-hosted runner executes code from your repository, including pull requests if you allow it. Treat it like a production entry point. Use these practical safeguards: (1) run on a dedicated VM, (2) restrict which branches and events can use the runner, (3) avoid running untrusted fork PRs on self-hosted runners, and (4) keep the OS patched.

Also consider network segmentation: if the runner can reach internal services, use firewall rules so it can only access what it truly needs. If you install Docker, be careful with granting the runner user access to the Docker socket—Docker can effectively become root on the host. For higher-risk environments, run builds inside isolated containers or ephemeral VMs.

Troubleshooting common problems

Runner is offline: check service status (sudo ./svc.sh status) and logs with journalctl -u actions.runner.* -n 200 --no-pager. Reboot-safe service configuration usually fixes “works in terminal, fails after reboot” issues.

Token expired: registration tokens are short-lived. Generate a new token in GitHub and re-run ./config.sh after removing the old configuration (./config.sh remove).

Jobs stuck waiting: your workflow’s runs-on labels must match the runner labels exactly. If the workflow requires [self-hosted, linux, docker] but your runner is only labeled linux, GitHub will keep the job queued.

Conclusion

With a self-hosted GitHub Actions runner on Linux, you can speed up CI/CD, use specialized build tools, and keep deployment workflows closer to your infrastructure. The key is to set it up cleanly (dedicated user and systemd service) and to treat security as part of the installation—not an afterthought.

Deploy a Local AI Chatbot on Linux with Ollama and Open WebUI (No Cloud Required)

Why Run a Local AI Chatbot?

If you like using ChatGPT-style assistants but you work with sensitive data, cloud tools can be a non-starter. Running a local AI chatbot on your own Linux machine gives you control over privacy, lets you work offline, and can reduce ongoing costs. Thanks to modern lightweight model runners, you can now deploy an AI assistant in minutes without building anything from source.

In this tutorial, you will set up Ollama (a simple local LLM runtime) and Open WebUI (a clean web interface) on a Linux server. The result is a private, browser-based AI chatbot you can access on your LAN.

What You Need

System requirements: A modern Linux distribution (Ubuntu/Debian/RHEL-based), at least 8 GB RAM (16 GB recommended), and 15–30 GB of free disk space depending on the model you choose. A GPU is optional; CPU-only works, but responses may be slower.

Network requirements: If you want to access the chatbot from other devices, ensure you can reach the server over the network and that any firewall rules allow the chosen port.

Step 1: Install Ollama

Ollama is the engine that downloads and runs local AI models. On most Linux systems, the fastest method is the official install script. Open a terminal and run:

curl -fsSL https://ollama.com/install.sh | sh

After installation, confirm the service is working:

ollama --version

If your system uses systemd (most servers do), Ollama typically runs as a service. You can check its status with:

systemctl status ollama

Step 2: Pull a Model and Test It

Next, download a model. For a good balance between quality and speed on typical hardware, many users start with smaller variants. Example:

ollama pull llama3.2

Then run a quick interactive test:

ollama run llama3.2

Type a prompt, press Enter, and confirm you get a response. If the model feels slow, try a smaller one or ensure your server is not memory constrained.

Step 3: Install Open WebUI (Docker Method)

Open WebUI provides the familiar chat interface in your browser. The most reliable way to install it is using Docker, because updates are easy and dependencies stay isolated.

First, install Docker if you don’t already have it. On Ubuntu/Debian, this common approach works (adjust for your distro if needed):

sudo apt update && sudo apt install -y docker.io

sudo systemctl enable --now docker

Now start Open WebUI. The key is to point it to Ollama. If Ollama runs on the same machine, you can use host networking for simplicity:

sudo docker run -d --name open-webui --restart unless-stopped --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 ghcr.io/open-webui/open-webui:main

Open WebUI will typically be available on port 8080. From a browser on the server, test:

http://localhost:8080

Step 4: Access It from Another Device (LAN)

To use the chatbot from your laptop or phone on the same network, browse to:

http://SERVER_IP:8080

If it doesn’t load, check firewall rules. On Ubuntu with UFW, you can allow the port like this:

sudo ufw allow 8080/tcp

Also confirm that Docker is running and the container is healthy:

sudo docker ps

Step 5: Add and Switch Models in the Web Interface

Once logged into Open WebUI, you can select available models that Ollama has downloaded. If you want more choices, pull additional models on the server:

ollama pull mistral

ollama pull qwen2.5

Refresh the model list in the UI and switch models depending on your task. Smaller models respond faster; larger models can be better at reasoning and writing, but need more RAM and CPU.

Troubleshooting Tips

Open WebUI loads, but no models appear: Verify the environment variable points to Ollama correctly. If you used host networking, http://127.0.0.1:11434 is usually correct. Also confirm Ollama is listening:

ss -lntp | grep 11434

Model downloads are slow: Try again off-peak, confirm DNS/network stability, and ensure you have enough disk space. Model pulls can be several gigabytes.

Responses are very slow: Check RAM usage with free -h. If the system is swapping, performance will drop sharply. Consider a smaller model or upgrading memory.

Keep It Updated

To update Open WebUI, pull the latest container and recreate it:

sudo docker pull ghcr.io/open-webui/open-webui:main

sudo docker stop open-webui && sudo docker rm open-webui

sudo docker run -d --name open-webui --restart unless-stopped --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 ghcr.io/open-webui/open-webui:main

For Ollama, rerun the installer script occasionally or follow your distro’s recommended update path if you installed it through a package manager.

Conclusion

You now have a fully local AI chatbot running on Linux with Ollama and Open WebUI. This setup is practical for internal helpdesk use, drafting documentation, summarizing logs, or experimenting with prompts without sending data to third-party services. From here, you can harden access with a reverse proxy, enable HTTPS, and standardize your model choices for your team.

How to Run Rootless Containers on Ubuntu 24.04 with Podman and systemd Quadlet (Auto-Updates Included)

Overview

Running containers without root is a big security win, and Ubuntu 24.04 LTS makes it easy with Podman and systemd Quadlet. Podman is a Docker-compatible container engine that works without a daemon, and Quadlet lets you define containers as systemd units for reliable startup, health checks, logging, and auto-updates. In this tutorial, you will set up a rootless container managed by systemd, enable automatic image updates, and learn how to monitor and troubleshoot the service.

Prerequisites

You will need an Ubuntu 24.04 LTS system, a regular user with sudo rights, outbound internet access to pull images, and an open application port above 1024 (rootless containers cannot bind to privileged ports). This guide assumes your username is $USER and that you will host a simple HTTP service on port 8080.

Step 1: Install Podman

Ubuntu 24.04 includes a recent Podman package. Install it via APT and verify the version:

sudo apt update
sudo apt install -y podman
podman --version
podman info

If podman info shows cgroup version 2 and a working network backend, you are good to go. No system-wide daemon is needed.

Step 2: Enable user lingering for systemd

To let your user services keep running after logout, enable lingering and confirm that the user systemd instance is active:

sudo loginctl enable-linger "$USER"
systemctl --user status

If the second command shows a status page, the per-user systemd is ready. Otherwise, log out and back in, or start it by running any user service.

Step 3: Create a Quadlet unit for a sample web app

Quadlet reads unit files from ~/.config/containers/systemd/ and generates corresponding systemd services. Create the directory and a container unit that runs a tiny HTTP server (traefik/whoami) on port 8080:

mkdir -p ~/.config/containers/systemd

cat > ~/.config/containers/systemd/whoami.container <<'EOF'
[Unit]
Description=Rootless whoami demo (Podman + Quadlet)
After=network-online.target
Wants=network-online.target

[Container]
# Image to run
Image=traefik/whoami:latest
# Name the container consistently
ContainerName=whoami
# Map host port 8080 to container port 80
PublishPort=8080:80
# Persist simple state/logs if needed
Volume=%h/containers/whoami:/data:Z
# Health check for systemd readiness
HealthCmd=curl -fsS http://127.0.0.1:80/ || exit 1
HealthInterval=30s
# Timezone for logs
Environment=TZ=UTC
# Auto update from registry when new image is available
AutoUpdate=registry
# Pass any extra runtime args if needed
# ContainerRuntimeArguments=--pids-limit=256

[Service]
# Restart on failure
Restart=on-failure
RestartSec=3

[Install]
WantedBy=default.target
EOF

The AutoUpdate=registry directive is the Quadlet-native way to enable image updates. The PublishPort setting maps host port 8080 to the container’s port 80. The HealthCmd allows systemd to consider the service healthy only after the endpoint responds.

Step 4: Generate and start the systemd service

Reload the user systemd manager to pick up the new Quadlet file, then enable and start the service:

systemctl --user daemon-reload
systemctl --user enable --now whoami.service
systemctl --user status whoami.service

You should now see the service active. Verify the container is running and accessible:

podman ps
curl -s http://127.0.0.1:8080/

The curl output returns headers that include your container’s hostname, confirming the service is up.

Step 5: Enable automatic image updates

Podman provides a built-in timer to auto-update containers based on Quadlet’s AutoUpdate=registry or the io.containers.autoupdate label. Enable the timer:

systemctl --user enable --now podman-auto-update.timer
systemctl --user list-timers | grep podman-auto-update

You can test an update cycle without applying it:

podman auto-update --dry-run

When a new image is available, Podman will pull it and restart only the affected containers, minimizing downtime.

Step 6: Logs, troubleshooting, and lifecycle

To review live logs and diagnose issues, use journalctl and Podman’s own logs. Here are the most useful commands:

journalctl --user-unit whoami.service -f
podman logs -f whoami
systemctl --user restart whoami.service
systemctl --user stop whoami.service

If you change the .container file, always run systemctl --user daemon-reload and then systemctl --user restart whoami.service to apply changes. To remove the service completely, stop and disable it, delete the Quadlet file, and reload:

systemctl --user disable --now whoami.service
rm ~/.config/containers/systemd/whoami.container
systemctl --user daemon-reload
podman rm -f whoami

Step 7: Exposing the service on your network

If you plan to expose the service beyond localhost, open the firewall and bind to the host’s address. With UFW, allow port 8080:

sudo ufw allow 8080/tcp

For production, consider putting this behind a reverse proxy such as Caddy, Nginx, or Traefik for HTTPS termination and rate limiting. Rootless containers cannot bind to ports below 1024, so use a reverse proxy on 443/80 or use auth/stream termination upstream.

Step 8: Tips for production use

Use bind mounts or volumes for persistent data, set resource limits with ContainerRuntimeArguments (CPU/memory/pids), and prefer pinned tags or digests for image stability. For private registries, log in with podman login <registry> and consider CredentialHelpers. Finally, keep Ubuntu patched (using unattended-upgrades), and monitor your services with systemd health checks and alerts.

Conclusion

You have created a secure, rootless container on Ubuntu 24.04 using Podman and systemd Quadlet, enabled automatic image updates, and learned how to manage the service lifecycle with systemd. This pattern makes container workloads feel like first-class system services, while keeping your host safer by avoiding root privileges.

Deploy Ollama + Open WebUI on Ubuntu with GPU Acceleration using Docker Compose

Running large language models locally is now practical and fast, especially with GPU acceleration. In this tutorial, you will deploy Ollama and Open WebUI on Ubuntu 22.04/24.04 using Docker Compose. This stack gives you a private, browser-based interface for modern LLMs (Llama, Mistral, Phi, etc.) with one-click model management and secure, self-hosted inference.

Why this stack?

Ollama simplifies downloading, quantizing, and serving LLMs on your machine. Open WebUI adds a clean chat interface, prompt templates, file uploads, and multi-user access. Together, they provide a robust local AI setup that is easy to update and portable across servers.

Prerequisites

- Ubuntu Server 22.04 or 24.04 (fresh system recommended)

- An NVIDIA GPU with recent drivers (T4, RTX 20/30/40, A-series, etc.)

- sudo access and an internet connection

- Optional: a domain name for HTTPS (e.g., ai.example.com)

Step 1 — Install Docker Engine and Compose

Install Docker from the official repository to ensure up-to-date features like GPU support in Docker Compose.

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose version

Step 2 — Enable GPU with NVIDIA Container Toolkit

Install the NVIDIA Container Toolkit to pass the GPU into containers. Verify that the host can see the GPU with nvidia-smi before proceeding.

# If you don't have drivers:
# sudo ubuntu-drivers install && sudo reboot

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit.gpg] https://#g' | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt update
sudo apt install -y nvidia-container-toolkit

# Configure Docker to use the NVIDIA runtime
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

# Sanity check
nvidia-smi

Step 3 — Create the Docker Compose stack

We will run two services: Ollama (backend API on port 11434) and Open WebUI (frontend on port 3000) connected via a Docker network. The compose file also enables GPU support for Ollama.

mkdir -p ~/ollama-openwebui && cd ~/ollama-openwebui
cat > docker-compose.yml <<'YAML'
services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    ports:
      - "11434:11434"
    volumes:
      - ollama:/root/.ollama
    gpus: all
    environment:
      - OLLAMA_KEEP_ALIVE=1h
      - OLLAMA_HOST=0.0.0.0

  openwebui:
    image: ghcr.io/open-webui/open-webui:latest
    container_name: openwebui
    restart: unless-stopped
    depends_on:
      - ollama
    ports:
      - "3000:8080"
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
      - WEBUI_AUTH=True
    volumes:
      - openwebui:/app/backend/data

volumes:
  ollama:
  openwebui:
YAML

Step 4 — Launch and access Open WebUI

Start the stack and watch logs for any errors. The first launch will pull images.

docker compose up -d
docker compose logs -f --tail=100

Open your browser to http://SERVER_IP:3000. Create the first admin user when prompted. Open WebUI will automatically detect Ollama via the internal URL and list available models.

Step 5 — Pull a model and test

Use either the WebUI model manager or the CLI to fetch models. The example below pulls a popular 7B model.

# Pull from the host (proxies into the container)
docker exec -it ollama ollama pull llama3.1:8b

# Quick API smoke test
curl http://localhost:11434/api/generate -d '{
  "model": "llama3.1:8b",
  "prompt": "Say hello from a local LLM.",
  "stream": false
}'

In Open WebUI, select the model from the dropdown and start chatting. If you have enough VRAM, consider quantized larger models (e.g., 13B/70B Q4/Q5) for better reasoning.

Optional — Secure with a Caddy reverse proxy and HTTPS

If you have a domain, use Caddy to obtain and renew TLS automatically. This example exposes Open WebUI securely on port 443 and keeps Ollama private.

sudo apt install -y caddy
sudo tee /etc/caddy/Caddyfile >/dev/null <<'CADDY'
ai.example.com {
  encode zstd gzip
  reverse_proxy 127.0.0.1:3000
}
CADDY
sudo systemctl reload caddy

Point your DNS A/AAAA record to the server. Then visit https://ai.example.com. For teams, enable WebUI auth (already set) and create users from the admin settings.

Back up and update

To back up your models and chats, save the named volumes. You can also snapshot the folders from the host.

# Export volumes to tarballs
docker run --rm -v ollama:/v -v $(pwd):/b busybox tar czf /b/ollama-vol.tgz -C /v .
docker run --rm -v openwebui:/v -v $(pwd):/b busybox tar czf /b/openwebui-vol.tgz -C /v .

# Update images safely
docker compose pull
docker compose up -d

Troubleshooting

- No GPU detected: Ensure nvidia-smi works on the host. Re-run nvidia-ctk runtime configure, restart Docker, and verify the container sees the GPU:

docker exec -it ollama bash -lc 'nvidia-smi || ls -l /dev/nvidia*'

- Slow generation: Use quantized models (Q4_K_M/Q5_K_M), avoid oversize context windows, and confirm GPU is actually used (GPU utilization should rise in nvidia-smi during inference).

- Port conflicts: Change mapped ports in docker-compose.yml, e.g., "3001:8080" for Open WebUI or put a reverse proxy in front.

- Permission errors on volumes: Ensure your user is in the docker group and that the Docker daemon can write to the volume paths.

Security tips

- Keep Ollama bound to the internal network and only expose Open WebUI through TLS.

- Enable authentication (already set via WEBUI_AUTH=True). Use strong passwords and consider putting Open WebUI behind a VPN or SSO.

- Restrict firewall ports using UFW: allow 22/tcp and 443/tcp, then deny others.

Conclusion

You now have a GPU-accelerated, private AI stack with Ollama and Open WebUI on Ubuntu, orchestrated by Docker Compose. It is easy to upgrade, portable across servers, and suitable for personal research or team deployments. With this foundation, you can iterate quickly, evaluate new models as they drop, and keep your data fully on-prem.

Deploy a Docker Reverse Proxy with Traefik v3 and Automatic Let’s Encrypt on Ubuntu 22.04/24.04

This guide shows how to deploy a modern reverse proxy using Traefik v3, Docker, and Docker Compose on Ubuntu 22.04 or 24.04. You will get automatic Let’s Encrypt SSL certificates, HTTP to HTTPS redirection, and a clean way to route multiple apps on the same server under different domains or subdomains. The steps are simple, repeatable, and safe for production.

Prerequisites

Before starting, ensure you have: (1) An Ubuntu 22.04 or 24.04 server with a public IP, (2) A domain or subdomain you can edit DNS for (e.g., example.com), and (3) Access to open TCP ports 80 and 443 on the server’s firewall and network provider. You should also have a non-root user with sudo access.

Step 1 — Install Docker Engine and Compose Plugin

Install the official Docker packages. This gives you the latest stable Docker Engine, Buildx, and the Compose plugin.

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release; echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker

Step 2 — DNS and Firewall

Create DNS A records for your services. For example, point app.example.com and traefik.example.com to your server’s public IP. Then open the firewall for web traffic.

sudo ufw allow 80,443/tcp
sudo ufw reload

Step 3 — Prepare a Docker Network and Traefik Files

Create a dedicated Docker network for the proxy and a directory to hold Traefik files, including the ACME storage for certificates.

docker network create proxy
mkdir -p ~/traefik
cd ~/traefik
touch acme.json
chmod 600 acme.json

Step 4 — Create docker-compose.yml for Traefik v3

The compose file below configures Traefik v3 with automatic HTTPS via the HTTP-01 challenge (ports 80/443). It also binds the dashboard to localhost only for safety.

version: "3.9"

networks:
  proxy:
    external: true

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    command:
      - --api.dashboard=true
      - --api.insecure=false
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entryPoint.to=websecure
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      - [email protected]
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
      - --log.level=INFO
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:8080:8080" # Dashboard on localhost
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/letsencrypt/acme.json
    networks:
      - proxy
    restart: unless-stopped

Replace [email protected] with an email you control. Traefik uses it to register with Let’s Encrypt. The dashboard is available on http://localhost:8080 and can be reached via SSH tunneling when needed.

Step 5 — Start Traefik

Launch the reverse proxy and confirm it is running.

docker compose up -d
docker ps

You should see the traefik container healthy and listening on ports 80 and 443.

Step 6 — Add a Test App Behind Traefik

Deploy a simple test service (whoami) to verify automatic HTTPS, routing, and headers. Replace app.example.com with your real hostname that points to the server.

docker run -d --name whoami --network proxy --restart unless-stopped \
  -l "traefik.enable=true" \
  -l "traefik.http.routers.who.rule=Host(`app.example.com`)" \
  -l "traefik.http.routers.who.entrypoints=websecure" \
  -l "traefik.http.routers.who.tls.certresolver=le" \
  -l "traefik.http.middlewares.secHeaders.headers.stsSeconds=31536000" \
  -l "traefik.http.middlewares.secHeaders.headers.stsIncludeSubdomains=true" \
  -l "traefik.http.middlewares.secHeaders.headers.stsPreload=true" \
  -l "traefik.http.middlewares.secHeaders.headers.frameDeny=true" \
  -l "traefik.http.middlewares.ratelimit.rateLimit.average=100" \
  -l "traefik.http.middlewares.ratelimit.rateLimit.burst=50" \
  -l "traefik.http.routers.who.middlewares=secHeaders@docker,ratelimit@docker" \
  traefik/whoami:v1.10

Open https://app.example.com in a browser. The first request may take a few seconds while Traefik obtains a certificate. You should see a basic whoami page over HTTPS with a valid lock icon.

Optional: Use DNS-01 Challenge (Wildcard Certificates)

If your ISP or host blocks port 80, or you want wildcard certificates like *.example.com, switch to the DNS-01 challenge. The example below uses Cloudflare. Create a token in Cloudflare with DNS edit permissions for the zone and store it in an .env file.

cd ~/traefik
printf "CF_DNS_API_TOKEN=YOUR_CLOUDFLARE_DNS_TOKEN\n" > .env

Edit docker-compose.yml and replace the ACME lines for HTTP challenge with DNS challenge:

      - --certificatesresolvers.le.acme.dnschallenge=true
      - --certificatesresolvers.le.acme.dnschallenge.provider=cloudflare

Add the environment variable to the traefik service:

    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}

Then reload Traefik:

docker compose up -d

For a wildcard, use routers or certificate domains that match *.example.com. With DNS challenge, Traefik can issue certificates without exposing port 80.

Secure the Dashboard (Optional but Recommended)

By default we bound the dashboard to localhost. To access it securely from your workstation, use an SSH tunnel: ssh -L 8080:localhost:8080 user@your_server and then open http://localhost:8080. If you must expose it on a domain, add Basic Auth and IP allowlisting via labels and serve it over HTTPS. For most setups, keeping it local is safer.

Troubleshooting

If certificates do not issue, confirm that your DNS A/AAAA records point to the server and that ports 80 and 443 are reachable from the internet. Check logs with docker logs -f traefik. Rate limits from Let’s Encrypt can apply if you redeploy too often; use a single domain during testing and switch to the production domain when stable.

Maintenance

Traefik renews certificates automatically before expiry; no cron is required. Keep Docker images current by pulling updates periodically: docker compose pull and docker compose up -d. Back up the acme.json file; it contains your issued certificates and keys.

What You Achieved

You now have a modern, production-ready reverse proxy with Traefik v3, automatic HTTPS via Let’s Encrypt, and a clean Docker-based workflow. Adding new apps is as simple as attaching them to the proxy network and setting a few labels. This pattern scales well, stays secure, and keeps your server easy to manage.

Self-Host Open WebUI with Ollama on Ubuntu Using Docker Compose (GPU Optional)

Overview

This tutorial shows how to self-host Open WebUI with Ollama on Ubuntu using Docker Compose. You will get a clean, repeatable setup that runs on CPU or GPU, stores model data persistently, and can be upgraded with a single command. Open WebUI provides a modern interface, while Ollama runs local large language models such as Llama 3, Mistral, Phi-3, and CodeLlama.

Prerequisites

You will need a fresh Ubuntu 22.04 or 24.04 server (cloud VM or local machine), a user with sudo rights, and at least 8 GB of RAM. If you plan to use a GPU, an NVIDIA card is recommended. Open ports 3000 (Web UI) and 11434 (Ollama API) on your firewall or security group.

1) Install Docker and Compose

Install the official Docker Engine and the Compose plugin on Ubuntu. Log out and back in (or run newgrp) after adding your user to the docker group.

sudo apt update
sudo apt install -y ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose version

2) Optional: Enable NVIDIA GPU Support

If you have an NVIDIA GPU, install the NVIDIA driver and the NVIDIA Container Toolkit. This lets Ollama use your GPU for faster inference.

sudo apt install -y ubuntu-drivers-common
sudo ubuntu-drivers autoinstall
sudo reboot

After the reboot, install the container toolkit and configure Docker:

distribution=$(. /etc/os-release; echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
  sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit.gpg

curl -fsSL https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit.gpg] https://#g' | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt update
sudo apt install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

Verify GPU access with Docker:

docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi

3) Create the Docker Compose Stack

Create a project directory and a Compose file that defines two services: Ollama (LLM runtime) and Open WebUI (frontend). The volumes preserve your models and settings across restarts.

mkdir -p ~/openwebui-ollama
cd ~/openwebui-ollama
nano docker-compose.yml

Paste the following content and save:

version: "3.9"

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    ports:
      - "11434:11434"
    volumes:
      - ollama:/root/.ollama
    environment:
      - OLLAMA_KEEP_ALIVE=24h
    # Uncomment the next line if you have GPU configured:
    # gpus: all

  open-webui:
    image: ghcr.io/open-webui/open-webui:latest
    container_name: open-webui
    depends_on:
      - ollama
    restart: unless-stopped
    ports:
      - "3000:8080"
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    volumes:
      - open-webui:/app/backend/data

volumes:
  ollama:
  open-webui:

4) Start the Services and Download a Model

Bring the stack online. The first start will download container images.

docker compose up -d
docker compose ps

Pull a small model to test. You can add more models later.

docker exec -it ollama ollama pull llama3.2:3b
# Other options: mistral:7b, phi3:mini, qwen2.5:7b

Open your browser to http://SERVER-IP:3000. Create your account in Open WebUI. In the model selector, choose the model you pulled and send a prompt to verify everything works.

5) Persist Data and Backups

Docker volumes keep your models and UI data under /var/lib/docker/volumes. To back them up, stop the stack and archive the data directories. This ensures quick recovery after an OS reinstall or server migration.

docker compose down
sudo tar -czf ollama_data.tgz -C /var/lib/docker/volumes \
  $(docker volume ls -q | grep "_ollama$")/_data

sudo tar -czf openwebui_data.tgz -C /var/lib/docker/volumes \
  $(docker volume ls -q | grep "_open-webui$")/_data

docker compose up -d

6) Secure and Publish (Optional)

If you expose the service on the internet, put it behind a reverse proxy with HTTPS (Caddy, Nginx, or Traefik) and set strong authentication in Open WebUI. Use a DNS name, issue a TLS certificate (Let’s Encrypt), and restrict access with IP allowlists or an identity provider. For small teams, consider running it only on a private network or VPN.

7) Update and Maintenance

Update to the newest images regularly. This pulls security updates, new UI features, and performance improvements.

cd ~/openwebui-ollama
docker compose pull
docker compose up -d

To update models to the latest quantizations or fixes, re-pull them in Ollama. You can remove old ones you no longer need.

docker exec -it ollama ollama pull mistral:7b
docker exec -it ollama ollama list
docker exec -it ollama ollama rm modelname:tag

Troubleshooting

Port already in use: Change the host port mappings in docker-compose.yml (for example, 3001:8080 or 11435:11434) and restart.

GPU not detected: Verify nvidia-smi works on the host and in a test container. Ensure the gpus: all line is uncommented and Docker was restarted after installing the NVIDIA Toolkit.

Slow or failed model pulls: Models can be large. Check disk space (df -h), network speed, and try a smaller model first. You can also mirror models by pre-downloading on another machine and copying the volume data.

Permission errors: Ensure your user is in the docker group (id) and you have logged out/in.

What You Achieved

You now have a production-friendly, self-hosted AI chat stack powered by Open WebUI and Ollama. With Docker Compose, you can start, stop, back up, and upgrade the entire setup with a couple of commands. Add or swap models as your use cases evolve—coding assistants, knowledge chat, or creative writing—while keeping your data local and under your control.

How to Deploy Traefik v3 as a Docker Reverse Proxy with Automatic HTTPS (Let’s Encrypt)

Overview

Traefik v3 is a modern reverse proxy and load balancer that integrates smoothly with Docker, automatically discovering containers and routing traffic to them. In this guide, you will deploy Traefik v3 as a Docker reverse proxy with automatic HTTPS using Let’s Encrypt via the DNS challenge (Cloudflare as an example). The setup is fast, reproducible, and ideal for hosting multiple apps on a single server with clean domain names and valid TLS certificates.

Prerequisites

Before you start, you will need: a Linux server (Ubuntu 22.04/24.04 works great) with root or sudo access, Docker and Docker Compose installed, a domain name you control (e.g., example.com), ports 80 and 443 open to the server, and an API token from your DNS provider (Cloudflare in this tutorial) with permission to manage DNS (Zone:DNS:Edit). Create A or AAAA records for the subdomains you plan to use (e.g., traefik.example.com, app.example.com) pointing to your server’s public IP.

Step 1: Create a dedicated Docker network

Why: Keeping a shared “proxy” network lets Traefik see and route to other containers without exposing them on the host. Run:

docker network create proxy

Step 2: Prepare your working directory

Create a folder for Traefik and a place to store Let’s Encrypt certificates. The ACME store must be persistent across restarts.

mkdir -p ~/traefik/letsencrypt
cd ~/traefik

Step 3: Create a .env file for secrets

To avoid hardcoding tokens in your Compose file, use a .env file in the same directory. Replace values with your own.

CF_DNS_API_TOKEN=your_cloudflare_dns_token_here
[email protected]
TRAEFIK_DOMAIN=traefik.example.com
APP_DOMAIN=app.example.com

Step 4: Write docker-compose.yml

The Compose file below launches Traefik v3 and a sample “whoami” app to test routing and TLS. It enables the Docker provider, sets HTTP to HTTPS redirection, configures Let’s Encrypt with the Cloudflare DNS challenge, and exposes the dashboard on a secure subdomain.

version: "3.9"

services:
  traefik:
   image: traefik:v3.1
   container_name: traefik
   command:
    - --providers.docker=true
    - --providers.docker.exposedbydefault=false
    - --entryPoints.web.address=:80
    - --entryPoints.websecure.address=:443
    - --entryPoints.web.http.redirections.entryPoint.to=websecure
    - --entryPoints.web.http.redirections.entryPoint.scheme=https
    - --certificatesResolvers.le.acme.dnsChallenge.provider=cloudflare
    - --certificatesResolvers.le.acme.email=${LETSENCRYPT_EMAIL}
    - --certificatesResolvers.le.acme.storage=/letsencrypt/acme.json
    - --log.level=INFO
    - --api.dashboard=true
   environment:
    - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
   ports:
    - "80:80"
    - "443:443"
   volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - ./letsencrypt:/letsencrypt
   networks:
    - proxy
   restart: unless-stopped
   labels:
    - "traefik.enable=true"
    - "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_DOMAIN}`)"
    - "traefik.http.routers.dashboard.entrypoints=websecure"
    - "traefik.http.routers.dashboard.tls.certresolver=le"
    - "traefik.http.routers.dashboard.service=api@internal"
    - "traefik.http.middlewares.secure.headers.stsSeconds=31536000"
    - "traefik.http.middlewares.secure.headers.stsIncludeSubdomains=true"
    - "traefik.http.middlewares.secure.headers.stsPreload=true"
    - "traefik.http.middlewares.secure.headers.contentTypeNosniff=true"
    - "traefik.http.middlewares.secure.headers.browserXssFilter=true"

  whoami:
   image: traefik/whoami:latest
   networks:
    - proxy
   labels:
    - "traefik.enable=true"
    - "traefik.http.routers.who.rule=Host(`${APP_DOMAIN}`)"
    - "traefik.http.routers.who.entrypoints=websecure"
    - "traefik.http.routers.who.tls.certresolver=le"
    - "traefik.http.routers.who.middlewares=secure@docker"
    - "traefik.http.services.who.loadbalancer.server.port=80"
   restart: unless-stopped

networks:
  proxy:
   external: true

Step 5: Start Traefik and verify certificates

Bring the stack up and watch the logs for the ACME flow. Traefik will create a DNS TXT record via the API token and obtain certificates automatically.

docker compose up -d
docker logs -f traefik

On first successful issuance, an acme.json file will be created under the letsencrypt folder. Set strict permissions on it to keep private keys safe.

chmod 600 ./letsencrypt/acme.json

Step 6: Test the routes

Open https://traefik.example.com to see the dashboard and https://app.example.com to reach the whoami test application. Both should show a valid TLS lock in your browser. If you use Cloudflare proxy (orange cloud), DNS challenge still works because the validation is done via DNS records, not HTTP.

Optional: Protect the dashboard

Do not leave the dashboard open. You can add basic auth using a middleware. Create a bcrypt or Apache MD5 hash and replace the placeholder below. For a quick hash, you can use the “htpasswd” utility.

labels:
- "traefik.http.middlewares.dash-auth.basicauth.users=admin:$$apr1$$replace$$hashvaluehere"
- "traefik.http.routers.dashboard.middlewares=dash-auth@docker,secure@docker"

Adding your own apps

To publish another container behind Traefik, attach it to the “proxy” network and add three labels: a Host rule for your domain, the HTTPS entrypoint, and the certresolver. Optionally apply the “secure” headers middleware. Example:

labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`blog.example.com`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=le"
- "traefik.http.routers.blog.middlewares=secure@docker"

Troubleshooting

Port 80/443 already in use: Stop any existing web servers (e.g., Nginx, Apache) or change their ports. Traefik must bind to 80 and 443 to handle redirection and TLS termination.

Certificate issuance fails: Check Traefik logs for ACME errors. Verify the Cloudflare token has Zone:DNS:Edit on the correct zone, and that your environment variable is loaded by Compose. If you are using multiple zones, the token must cover them too.

404 or 502 errors: Ensure your container has traefik.enable=true, the Host rule matches your requested domain, DNS records resolve to your server, and the service port label points to the right container port.

Slow or failed DNS propagation: Give it a minute after creating DNS records. If using split DNS on a LAN, make sure internal resolvers return the public IP.

Why DNS challenge?

The DNS challenge avoids opening custom ports for validation and supports wildcard certificates. It is reliable behind CDNs, in NAT environments, and when you want to cover many subdomains without adding each one by hand.

What you achieved

You now have a production-ready Traefik v3 reverse proxy on Docker with automatic HTTPS, security headers, and a pattern you can reuse for any containerized app. Add services, set Host rules, and Traefik will handle routing and certificate management for you. This setup cuts down on boilerplate, centralizes TLS, and keeps your stack maintainable as it grows.

How to Deploy Traefik v3 with Docker Compose and Automatic HTTPS via Cloudflare DNS-01

Overview

This tutorial shows how to deploy Traefik v3 as a modern reverse proxy with Docker Compose and automatic HTTPS certificates using Let’s Encrypt via the Cloudflare DNS-01 challenge. The DNS-01 method works even behind NAT, on residential ISPs, and with Cloudflare’s orange cloud (proxy) enabled. By the end, you will have a secure, production-ready reverse proxy and a sample container accessible over HTTPS.

Prerequisites

- A Linux server (Ubuntu 22.04/24.04 or similar) with Docker Engine and Docker Compose v2 installed.
- A domain managed by Cloudflare (nameservers pointing to Cloudflare).
- A Cloudflare API token with Zone.DNS:Edit and Zone:Read permissions for the zone you’ll use.
- Basic terminal access and a user with Docker privileges.

Step 1 — Verify Docker and Compose

Confirm your Docker setup is ready. Run: docker --version and docker compose version. If Compose v2 is not present, install the latest Docker Engine from the official repository. On Ubuntu, ensure the docker group exists and your user is a member: sudo usermod -aG docker $USER then re-log.

Step 2 — Create a Dedicated Docker Network

Create an external network so Traefik can share it with your app containers: docker network create proxy. Using a dedicated network helps isolate traffic and makes adding new services predictable.

Step 3 — Prepare Folders and Secrets

Make a working directory for Traefik and create a place to store ACME data (certificates):

mkdir -p ~/traefik/letsencrypt
cd ~/traefik
touch ./letsencrypt/acme.json
chmod 600 ./letsencrypt/acme.json

Create an .env file to hold your Cloudflare token securely. This file will be read by Docker Compose:

echo "CF_DNS_API_TOKEN=<paste_your_cloudflare_api_token>" > .env

The API token should include Zone.DNS:Edit and Zone.Zone:Read permissions for the domain. Restrict the token to the specific zone for better security.

Step 4 — Create docker-compose.yml

Create a docker-compose.yml file in ~/traefik with the following content. Replace [email protected] with your email and whoami.example.com with a real subdomain in your zone.

services:
  traefik:
    image: traefik:v3.1
    command:
     - --providers.docker=true
     - --providers.docker.exposedbydefault=false
     - --entrypoints.web.address=:80
     - --entrypoints.websecure.address=:443
     - --entrypoints.web.http.redirections.entrypoint.to=websecure
     - --entrypoints.web.http.redirections.entrypoint.scheme=https
     - [email protected]
     - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
     - --certificatesresolvers.letsencrypt.acme.dnschallenge=true
     - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
     - --api.dashboard=true
    ports:
     - "80:80"
     - "443:443"
    environment:
     - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
    volumes:
     - /var/run/docker.sock:/var/run/docker.sock:ro
     - ./letsencrypt:/letsencrypt
    networks:
     - proxy

  whoami:
    image: traefik/whoami:v1.10
    labels:
     - "traefik.enable=true"
     - "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
     - "traefik.http.routers.whoami.entrypoints=websecure"
     - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
    networks:
     - proxy

networks:
  proxy:
    external: true

Create a DNS A/AAAA record for whoami.example.com pointing to your server’s public IP in Cloudflare. The orange cloud (proxy) can be ON or OFF; DNS-01 works either way.

Step 5 — Launch and Test

Start the stack from the ~/traefik directory: docker compose up -d. Traefik will request a certificate from Let’s Encrypt using the Cloudflare DNS-01 challenge. Check logs with docker compose logs -f traefik to confirm issuance (look for “Server responded with a certificate”).

Open https://whoami.example.com in your browser. You should see the whoami test service showing headers and IP details over HTTPS.

Optional: Secure the Traefik Dashboard

The dashboard is enabled but not published by default in this setup. To expose it safely, add labels to a new service or to Traefik itself using a distinct host like traefik.example.com, require basic auth middleware, and keep it behind TLS. Always disable --api.insecure=true in production.

Troubleshooting Tips

- If certificates do not issue, verify the API token has Zone.DNS:Edit and is scoped to the correct zone. Also confirm the .env is loaded and the environment variable name matches.
- If you see rate-limit errors, you may have requested too many certificates; wait and try again or use the Let’s Encrypt staging endpoint during testing (--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory).
- Ensure your firewall allows TCP 80 and 443 inbound to the server.

Maintaining and Adding Services

To add a new app behind Traefik, place it on the proxy network and attach labels for rule, entrypoint, and TLS resolver. Example labels: traefik.enable=true, traefik.http.routers.app.rule=Host(`app.example.com`), traefik.http.routers.app.entrypoints=websecure, and traefik.http.routers.app.tls.certresolver=letsencrypt. Restart only the new service; Traefik hot-reloads routes automatically.

Conclusion

With Traefik v3, Docker Compose, and Cloudflare’s DNS-01 challenge, you can ship secure containers with automatic HTTPS and minimal friction. This setup scales cleanly, keeps certificates current, and works in challenging network environments. Add your apps with labels, keep tokens scoped and secret, and enjoy a tidy, TLS-by-default container platform.

3.

Deploy a Private WireGuard VPN with Docker Compose (QR Codes for Mobile)

Why this guide

WireGuard is a modern VPN that is fast, secure, and simple to manage. Running it in Docker keeps your host clean, makes upgrades trivial, and allows you to back up your configuration as plain files. In this tutorial, you will deploy a production-ready WireGuard VPN with Docker Compose on an Ubuntu server, generate QR codes for easy mobile onboarding, and enable best-practice settings like IPv6 forwarding and DNS control.

Prerequisites

You need an Ubuntu 22.04/24.04 host (cloud VM or home server), a public DNS name for the server (e.g., vpn.example.com), and permission to forward UDP port 51820 on your router if you are behind NAT. You will also need a non-root user with sudo privileges. Windows or macOS clients can connect too, but we will demonstrate mobile setup using QR codes as it is the quickest way to get started.

Step 1 — Install Docker and Compose Plugin

Update your host, install Docker, and add your user to the docker group so you can run it without sudo.

sudo apt update && sudo apt -y upgrade
sudo apt -y install docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
# Re-log or run: newgrp docker

Step 2 — Create the project structure

We will store compose files in /srv/wireguard and persist configuration in /srv/wireguard/config. The container will keep keys and peer files under this directory, which makes backups easy.

sudo mkdir -p /srv/wireguard/config
sudo chown -R $USER:$USER /srv/wireguard

Step 3 — Write docker-compose.yml

Create a Compose file that uses the well-maintained LinuxServer.io WireGuard image. Replace vpn.example.com with your DNS name and adjust timezone and peer names to your needs.

nano /srv/wireguard/docker-compose.yml

version: "3.8"
services:
  wireguard:
    image: lscr.io/linuxserver/wireguard:latest
    container_name: wireguard
    cap_add:
     - NET_ADMIN
     - SYS_MODULE
    ports:
     - 51820:51820/udp
    volumes:
     - ./config:/config
     - /lib/modules:/lib/modules:ro
    environment:
     - PUID=1000
     - PGID=1000
     - TZ=Etc/UTC
     - SERVERURL=vpn.example.com
     - SERVERPORT=51820
     - PEERS=phone,laptop
     - PEERDNS=1.1.1.1
     - INTERNAL_SUBNET=10.13.13.0
     - ALLOWEDIPS=0.0.0.0/0,::/0
    sysctls:
     - net.ipv4.conf.all.src_valid_mark=1
     - net.ipv4.ip_forward=1
     - net.ipv6.conf.all.forwarding=1
    restart: unless-stopped

A few notes: PEERS seeds the initial clients; you can add more later. ALLOWEDIPS controls routing. With 0.0.0.0/0,::/0 the client routes all traffic through the VPN (full tunnel). For a split tunnel to only reach the VPN subnet, set 10.13.13.0/24 (and optionally fd00:13:13::/64 if you use IPv6).

Step 4 — Open the firewall and forward the port

If UFW is enabled on the host, allow UDP 51820. Also forward UDP 51820 on your router to the server’s LAN IP. If you use a cloud VM, open the UDP port in your provider’s security group.

sudo ufw allow 51820/udp

Step 5 — Start the stack

Bring the container up and follow logs. On the first start, it creates server keys and peer files under config/.

cd /srv/wireguard
docker compose up -d
docker compose logs -f

Step 6 — Get peer configs and QR codes

The image includes helper scripts. To display a peer config and its QR code, run:

docker exec -it wireguard /app/show-peer phone

Install the WireGuard app on iOS or Android, tap the plus button, choose “Scan from QR code,” and scan the code from your terminal. For Windows/macOS/Linux clients, copy the text config printed by the command above into a file like phone.conf and import it in the WireGuard desktop app.

To add a new peer at any time, use:

docker exec -it wireguard /app/add-peer tablet

Step 7 — Verify the connection

Activate the tunnel on your device. From the server, confirm the handshake:

docker exec wireguard wg show

You should see latest handshake times and transfer counters increase as you pass traffic. From the client, visit https://ifconfig.io to confirm your public IP matches the server and that DNS resolves as expected.

Optional: Tune routing, MTU, and DNS

If you only want to reach resources on your home network and keep general browsing on the local internet, change ALLOWEDIPS in the peer config to the private ranges you care about (for example, 10.13.13.0/24,192.168.1.0/24). For mobile networks with strict NAT, enable a keepalive in the peer config by adding PersistentKeepalive = 25. If you notice slow speeds, set MTU = 1280 in the peer config to avoid fragmentation on cellular carriers.

For ad blocking, set PEERDNS to your Pi-hole or AdGuard Home address reachable through the tunnel, e.g., 10.13.13.2. You can also use privacy resolvers like 1.1.1.1 or 9.9.9.9.

Backups and updates

The critical state lives in /srv/wireguard/config. Back it up regularly with your favorite tool (rsync, Restic, Borg). To upgrade safely, pull the new image and recreate the container; your config remains intact.

cd /srv/wireguard
docker compose pull
docker compose up -d

Troubleshooting

No handshake? Verify the UDP port forward and make sure your DNS record points to the right public IP. On mobile networks behind Carrier Grade NAT, incoming connections may be blocked—host the server on a cloud VM or use a home ISP with a public IP. If the tunnel connects but no traffic flows, confirm IP forwarding is enabled (the Compose file includes sysctls) and that ALLOWEDIPS is correct on both ends. For double NAT routers, enable a full-cone/endpoint-independent NAT if available, or use an alternate UDP port like 51821.

You are done

With Docker Compose and WireGuard, you now have a lightweight, fast VPN that you can maintain in minutes. Add peers with one command, scan a QR code on your phone, and enjoy a private, encrypted tunnel wherever you are. Keep your system updated, back up the config folder, and you will have a reliable VPN for the long run.

Build Your Own Encrypted S3 Backup with Restic and MinIO (Docker Compose, 2025 Guide)

If you want fast, encrypted, and deduplicated backups without paying for a public cloud, pairing Restic with MinIO is a powerful option. Restic handles client-side encryption and incremental backups; MinIO provides an S3-compatible object store you fully control. In this hands-on guide, you will deploy MinIO using Docker Compose, initialize a Restic repository, automate backups with a retention policy, and verify restores. The steps work on modern Linux distributions (Ubuntu/Debian/RHEL) and on any host that can run Docker.

Prerequisites

- A Linux server with at least 2 CPU cores, 4 GB RAM, and storage sized to your backup needs.
- Docker and Docker Compose installed.
- A shell user with sudo rights.
- Optional but recommended: a hostname and TLS if exposing MinIO beyond localhost.

Step 1 — Create a Docker Compose file for MinIO

We will run MinIO on ports 9000 (S3 API) and 9001 (web console). Use a strong root user and password. Put the credentials into a .env file so they are not hard-coded in the Compose file.

# .env
MINIO_ROOT_USER=minioadmin_change_me_now
MINIO_ROOT_PASSWORD=SuperStrong_Passw0rd_change_me
MINIO_DATA_PATH=./minio-data
# docker-compose.yml
services:
  minio:
    image: minio/minio:latest
    container_name: minio
    command: server /data --console-address ":9001"
    ports:
      - "9000:9000"   # S3 API
      - "9001:9001"   # Web Console
    environment:
      - MINIO_ROOT_USER=${MINIO_ROOT_USER}
      - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
    volumes:
      - ${MINIO_DATA_PATH}:/data
    restart: unless-stopped

Start MinIO:

docker compose up -d

Open the web console at http://<server-ip>:9001 and sign in with the root user credentials from the .env file.

Step 2 — Create a bucket and a dedicated access key

For clear separation, create a bucket named restic and generate a dedicated access key limited to that bucket.

Option A (Web Console):

- Storage → Create bucket → Name: restic.
- Identity → Service Accounts → Create access key → Scope: restrict to the restic bucket (read/write). Save the Access Key and Secret Key.

Option B (CLI, quick start):

# Uses MinIO Client without permanent install
docker run --rm --network host \
  -e MC_HOST_local="http://${MINIO_ROOT_USER}:${MINIO_ROOT_PASSWORD}@127.0.0.1:9000" \
  minio/mc mb local/restic

For production, prefer a limited service account rather than using the root keys in automation.

Step 3 — Initialize a Restic repository

Install Restic from your distribution or from the official releases. Then export environment variables for the repository URL and credentials. Replace placeholders with your actual keys.

export RESTIC_REPOSITORY="s3:http://127.0.0.1:9000/restic"
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"
export RESTIC_PASSWORD="Choose_A_Strong_Repo_Password"

Initialize the repository (first time only):

restic init

Restic encrypts data with the password you set, before uploading. Keep this password safe; without it, restores are impossible.

Step 4 — Create a backup script with retention

Let’s script a daily backup with sensible retention (7 daily, 4 weekly, 6 monthly) and basic integrity checks.

sudo tee /usr/local/bin/restic-backup.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# Environment (consider moving to /etc/restic/.env and source it)
export RESTIC_REPOSITORY="s3:http://127.0.0.1:9000/restic"
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"
export RESTIC_PASSWORD="Choose_A_Strong_Repo_Password"

# What to back up
INCLUDE_DIRS=(
  "/etc"
  "/var/www"
  "/var/lib/docker/volumes/project_data/_data"
  "$HOME"
)

# Exclusions
EXCLUDES=( 
  "--exclude-file=/etc/restic-excludes.txt"
)

# Run backup
restic backup "${EXCLUDES[@]}" "${INCLUDE_DIRS[@]}"

# Retention and pruning
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

# Lightweight consistency check (5% of data)
restic check --read-data-subset=5%
EOF
sudo chmod +x /usr/local/bin/restic-backup.sh

Create a simple exclusions file to skip caches and temporary data:

sudo tee /etc/restic-excludes.txt >/dev/null <<'EOF'
/home/*/.cache
/var/cache
/var/tmp
*.iso
*.qcow2
node_modules
EOF

Schedule the backup with cron:

(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/restic-backup.sh >> /var/log/restic.log 2>&1") | crontab -

Tip: For long-running servers, a systemd timer is often more reliable than cron. Also consider moving credentials into a protected file (chmod 600) and sourcing it inside the script.

Step 5 — Test listing and restoring

Verify snapshots:

restic snapshots

Restore a file or directory to a safe location:

# Find a snapshot ID from 'restic snapshots'
restic restore latest --target "$HOME/restore-test" --include /etc/hosts

Always test a restore. A backup you cannot restore is not a backup.

Step 6 — Secure access and expose safely

- Use HTTPS for MinIO. Either terminate TLS directly in MinIO (via certificates at /data/.minio/certs) or put MinIO behind a reverse proxy like Caddy or Nginx with automatic Let’s Encrypt.
- If backing up from remote machines, prefer private networks (Tailscale/WireGuard) rather than exposing port 9000 to the internet.
- Set strong passwords and rotate access keys periodically.

Step 7 — Useful maintenance and troubleshooting

- Verify integrity periodically: restic check (run monthly with full read if you can).
- Remove stale locks: restic unlock if a job was interrupted.
- Monitor logs: tail -f /var/log/restic.log for failures or timeouts.
- Storage growth: run restic prune occasionally (already included with forget --prune) and review your exclude list.

Why this stack works in 2025

Restic remains one of the most efficient open-source backup tools thanks to encryption-by-default, deduplication, and built-in S3 support. MinIO provides a modern, high-performance, and S3-compatible backend you can run on-prem or at the edge. With Docker Compose, you can upgrade easily, keep state on mapped volumes, and move the stack between servers without changing your backup commands.

With the steps above, you now have a repeatable, scriptable, and secure backup workflow: data is encrypted before it leaves the host, stored in an S3 bucket you control, and cleaned up automatically with a clear retention policy. Don’t forget to document your RESTIC_PASSWORD location and your MinIO access keys, and test restores on a schedule.

How to Securely Publish Self‑Hosted Apps with Cloudflare Tunnel and Zero Trust (Docker How‑To)

Overview

Cloudflare Tunnel lets you expose private services to the internet without opening inbound firewall ports or managing a reverse proxy. It establishes an outbound-only, encrypted connection from your host to Cloudflare’s edge, and pairs perfectly with Cloudflare Zero Trust Access for SSO, device checks, and detailed auditing. In this tutorial, you will deploy Cloudflare Tunnel with Docker, route multiple apps under different subdomains, and protect them with Zero Trust policies. The steps are simple, reproducible, and friendly to environments behind NAT or CGNAT.

Prerequisites

- A Cloudflare account with a domain managed by Cloudflare DNS.

- Docker and Docker Compose v2 on a Linux host (Ubuntu/Debian/Alpine are fine).

- At least one internal web service running in Docker or on the host (e.g., Grafana on port 3000, Nextcloud on 80, Syncthing on 8384).

- Optional but recommended: an identity provider (Google, GitHub, Azure AD, Okta) to enforce SSO via Zero Trust Access.

Step 1 — Create a Tunnel in Cloudflare Zero Trust

1) Go to Cloudflare dashboard > Zero Trust > Access > Tunnels and click “Create a tunnel.” Name it (e.g., home-net).

2) Choose the Docker option. Cloudflare will generate a TUNNEL_TOKEN for you. Keep this token safe; it lets cloudflared authenticate the tunnel without storing local credentials.

3) You can add “Public Hostnames” later in the dashboard, or define routing via a local config file. This guide shows both approaches, starting with the fast token-only method.

Step 2 — Fast Deploy with Docker Compose (Token Method)

Create a directory (e.g., /opt/cloudflared) and a Docker Compose file. Replace TUNNEL_TOKEN_VALUE with the token you copied in Step 1.

version: "3.8"
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=TUNNEL_TOKEN_VALUE
    # Optional metrics for monitoring
    # ports:
    #   - "2000:2000"
    # command: tunnel --no-autoupdate run --metrics 0.0.0.0:2000

Bring it up:

docker compose up -d

Cloudflared will dial out to Cloudflare’s edge over QUIC/TLS. No inbound ports are needed. Next, from the Zero Trust dashboard, add “Public Hostnames” for each app:

- grafana.example.com → http://localhost:3000 (or your internal service URL)

- files.example.com → http://localhost:80

- sync.example.com → http://localhost:8384

Cloudflare automatically creates DNS records for these hostnames and routes traffic through the tunnel.

Step 3 — Config-Driven Ingress (Advanced, Reproducible)

If you prefer everything as code, create a named tunnel and a local config file. This offers better portability and version control. First, create the tunnel credentials once on any machine (can be the same host):

# Authenticate and create a named tunnel (locally)
docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel login

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel create home-net

# Show tunnels (note the Tunnel UUID)
docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel list

This creates a credentials file named after the tunnel UUID. Now write config.yml in the same directory:

# ~/.cloudflared/config.yml
tunnel: HOME_NET_TUNNEL_UUID
credentials-file: /home/nonroot/.cloudflared/HOME_NET_TUNNEL_UUID.json
ingress:
  - hostname: grafana.example.com
    service: http://grafana:3000
  - hostname: files.example.com
    service: http://nextcloud:80
  - hostname: sync.example.com
    service: http://syncthing:8384
  - service: http_status:404
protocol: quic
warp-routing:
  enabled: false

If your apps run in Docker, place cloudflared on the same user-defined network so it can reach them by container name. Example Compose:

version: "3.8"
networks:
  apps:
    driver: bridge

services:
  grafana:
    image: grafana/grafana:latest
    networks: [apps]
    expose:
      - "3000"
    environment:
      - GF_SERVER_ROOT_URL=http://grafana.example.com

  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    networks: [apps]
    volumes:
      - ~/.cloudflared:/home/nonroot/.cloudflared:ro

Finally, register DNS routes (one-time):

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel route dns home-net grafana.example.com

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel route dns home-net files.example.com

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel route dns home-net sync.example.com

Step 4 — Protect with Zero Trust Access

Go to Zero Trust > Access > Applications > Add an application > Self-hosted. For each hostname you exposed, create an app and a policy:

- Choose your subdomain (e.g., grafana.example.com) and path (/*).

- Set a policy that requires SSO (Google/GitHub/Azure AD/Okta) or One-Time Pin.

- Optionally restrict to emails ending with @yourcompany.com or specific GitHub teams.

- Enable device posture checks if you use Cloudflare WARP (e.g., only allow managed devices).

With Access policies enforced, even if a URL leaks, visitors must authenticate before the origin sees any traffic.

Troubleshooting Tips

- 502/504 on a hostname: verify the upstream address in config points to a reachable service (container name + port or localhost + port). If using Docker networks, ensure both services share the same network.

- DNS not resolving: confirm the tunnel is “Healthy” and that the DNS record exists in your Cloudflare DNS zone. Propagation is usually instant because the record is proxied.

- Large uploads: consider enabling chunked uploads or tuning upstream app limits (e.g., client_max_body_size for Nginx-based apps). Cloudflare supports HTTP/2/3 on the edge; the tunnel runs over QUIC by default.

- Conflicting ports: cloudflared does not require inbound ports. If you exposed metrics on 2000, make sure it doesn’t collide with other services.

Security and Operations Best Practices

- Principle of least privilege: restrict Access policies to known users or groups; avoid wildcard “Allow all.”

- Separate hostnames for admin panels and public apps. Apply stricter policies (MFA, device checks) to admin endpoints.

- Use config as code where possible. Commit your cloudflared config.yml to a private repo and use environment-specific files for staging/production.

- Monitor tunnel health: expose metrics (–metrics 0.0.0.0:2000) and scrape with Prometheus. Alert on disconnects or high reconnect counts.

- Keep images updated: pin to a recent cloudflared tag and schedule updates. Test changes in staging first.

Wrap-Up

You deployed Cloudflare Tunnel with Docker, routed multiple internal services under friendly subdomains, and enforced Zero Trust Access in front of them—without opening a single inbound port. This pattern scales from homelabs to production, simplifies TLS and DNS, and raises your security baseline with SSO, device posture, logging, and revocation. If you outgrow manual steps, codify everything with config.yml, Compose files, and IaC for a repeatable, auditable setup.

Popular Posts

Install Ollama and Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install Ollama + Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install a Local AI Chatbot on Ubuntu 24.04 with Ollama and Open WebUI (Step-by-Step)

Trending Now

Recovering from Btrfs Boot Failures Using GUI Tools on Fedora

By the end of this guide the reader will be able to identify a Btrfs‑based Fedora installation, boot from a live USB, list and restore snapshots using the graphical utilities btrfs‑assistant and snapper, and verify that the system returns to a functional state without resorting to the command line. Understanding the Btrfs Layout Used by Fedora Fedora Workstation and Fedora KDE install the root filesystem as a single Btrfs partition that contains two default sub‑volumes. One sub‑volume holds the traditional “/” hierarchy, while the second is dedicated to /var/lib/machines . The latter exists to keep container images out of snapshot operations; it remains empty on systems that do not run virtual machines. Because Btrfs stores data in sub‑volumes rather than separate partitions, a snapshot captures the state of an entire sub‑volume at a point in time. The installer (Anaconda) automatically registers these sub‑volumes with the snapper service. Snapper maintains a series of read‑only ...