How to Expose Local Apps Securely with Tailscale Serve and Funnel (Free HTTPS in Minutes)

If you build or demo web apps, you have probably fought with port forwarding, firewalls, or complicated reverse proxies. Tailscale’s Serve and Funnel features make this painless: Serve exposes your local service to your tailnet (only authenticated users in your Tailscale network), and Funnel optionally publishes it to the public internet with automatic HTTPS. This guide shows a step-by-step setup in minutes on Linux, with tips for macOS and Windows.

What you will set up

You will run a local app (for example, on http://localhost:3000) and expose it securely via Tailscale in two modes:

Tailnet-only (Serve): Only people signed into your tailnet can access it (ideal for internal testing).
Public (Funnel): Anyone on the internet can access a public HTTPS URL that Tailscale provisions for you (great for demos, webhooks, or sharing temporary previews).

Prerequisites

- A Tailscale account (free is fine for personal use).
- Tailscale installed and logged in on the computer that runs your app (Linux/macOS/Windows).
- Ability to turn on the “Funnel” feature in the Tailscale admin console (if you plan to go public).
- A local service running (e.g., a dev server on port 3000).

1) Install and log in to Tailscale

On Ubuntu/Debian, the quickest way is:

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

Then bring the node online and sign in:

sudo tailscale up

Verify you are connected:

tailscale status

On macOS and Windows, install the client from the Tailscale website, sign in, and ensure the device appears in your admin console.

2) Expose a local service to your tailnet (Serve)

Let’s assume your app runs at http://localhost:3000. Use Tailscale Serve to proxy HTTPS traffic from your device’s Tailscale HTTPS endpoint to your local port:

tailscale serve https / proxy http://localhost:3000

That command maps path / to your local app. Check the status of your Serve configuration:

tailscale serve status

Now open the tailnet URL printed by the command (usually something like https://<device-name>.<tailnet>.ts.net/). Only users/authenticated devices in your tailnet can access it. This is perfect for internal reviews without exposing anything publicly.

3) Make it public with HTTPS (Funnel)

To publish your service to the public internet, first enable the Funnel feature in the Tailscale admin console (Settings → Feature preview/Settings → Funnel, depending on your account). You can restrict which devices and ports are allowed to use Funnel.

Once enabled, turn on Funnel for HTTPS (port 443) on the device:

tailscale funnel 443 on

That’s it. Tailscale will automatically provision a valid TLS certificate and a public URL (again, typically https://<device-name>.<tailnet>.ts.net/). Share this link with anyone; they do not need Tailscale to view your app.

To turn Funnel off again:

tailscale funnel 443 off

4) Useful variations

- Serve a different path: tailscale serve https /app proxy http://localhost:5173
- Serve a TCP port to your tailnet (e.g., Postgres): tailscale serve tcp 5432 127.0.0.1:5432
- Reset all Serve mappings on this device: tailscale serve reset

5) Testing and verification checklist

- Local works: Browse http://localhost:3000.
- Tailnet works: From another device on your tailnet, open https://<device-name>.<tailnet>.ts.net/. You should see a valid HTTPS certificate and your app.
- Public works: After turning on Funnel, test from a device not logged into Tailscale (cellphone on LTE, for example). The same URL should load over HTTPS.

6) Security tips

- Prefer tailnet-only Serve during development. Switch on Funnel only when you actually need a public demo or webhook endpoint.
- If your app needs authentication, keep it enabled. Funnel does not add login by default; it only provides HTTPS and routing.
- Avoid exposing admin panels, databases, or file shares via Funnel. Keep those tailnet-only or behind application-level authentication.

7) Troubleshooting

“403: Funnel not allowed” — Ensure the Funnel feature is enabled for your tailnet in the admin console and that your device/port is permitted.
Port conflicts — If something else is bound to 443 on your device, stop it or remap your Serve path (e.g., Serve at /app) and still use Funnel on 443.
Blank page or mixed content — If your app hardcodes http:// asset URLs, fix them to be relative or https://.
Service not reachable — Confirm your local app responds at the target URL (e.g., curl http://localhost:3000). Then run tailscale serve status to confirm the mapping exists.

8) Keeping it tidy

- List current mappings: tailscale serve status
- Remove a specific route: change your mapping command or reset and re-apply.
- Keep Tailscale updated: use your OS package manager or reinstall via the install script periodically. Check your version with tailscale version.

Why this approach is great

You get automatic TLS, end-to-end encrypted transport, and a clean URL without touching DNS, firewalls, or port forwards. For internal stakeholders, Serve keeps traffic private to your tailnet. For public demos, Funnel gives you one command to go live, then one command to go dark. It’s a powerful quality-of-life upgrade for anyone who ships or supports web apps.

How to Configure Traefik v3 as a Docker Reverse Proxy with Automatic Let’s Encrypt on Ubuntu 24.04

Overview

This tutorial shows how to deploy Traefik v3 as a modern Docker reverse proxy with automatic Let’s Encrypt TLS certificates on Ubuntu 24.04. You will set up secure HTTPS for any containerized web app using Docker Compose, with zero manual certificate handling. The guide uses the HTTP-01 challenge for public A-record domains and includes a note on switching to the DNS-01 challenge for wildcard certificates.

Prerequisites

- A fresh Ubuntu 24.04 server with sudo access.

- A domain name with an A record pointing to your server’s public IP (for example, whoami.example.com).

- Ports 80 and 443 open on the server firewall and any upstream firewall or cloud security group.

Step 1: Install Docker Engine and the Compose plugin

Install Docker using the official repository to get current packages. This also installs the docker 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 noble 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: Prepare directories, permissions, and a Docker network

Create a project folder for Traefik and a persistent location to store ACME data. The acme.json file must be readable only by the Traefik process to keep private keys safe.

sudo mkdir -p /opt/traefik/letsencrypt
sudo touch /opt/traefik/letsencrypt/acme.json
sudo chmod 600 /opt/traefik/letsencrypt/acme.json
sudo chown -R $USER:$USER /opt/traefik
docker network create proxy
cd /opt/traefik

Step 3: Create an .env file

Store reusable variables in a .env file. Replace the placeholders with your real email and domain. The email is used by Let’s Encrypt for certificate notices.

cat > .env << 'EOF'
[email protected]
DOMAIN=example.com
EOF

Step 4: Write the Docker Compose file

The Compose file below pulls Traefik v3, configures HTTP-to-HTTPS redirection, enables the Docker provider, and uses the HTTP-01 challenge on port 80 to issue certificates. It also deploys a sample “whoami” app behind TLS at whoami.your-domain.

cat > docker-compose.yml << 'EOF'
version: "3.9"

networks:
  proxy:
    external: true

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    command:
      - --api.dashboard=true
      - --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.email=${LETSENCRYPT_EMAIL}
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./letsencrypt:/letsencrypt
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy
    restart: unless-stopped

  whoami:
    image: traefik/whoami:v1.10
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami.rule=Host(`whoami.${DOMAIN}`)
      - traefik.http.routers.whoami.entrypoints=websecure
      - traefik.http.routers.whoami.tls.certresolver=le
      - traefik.http.services.whoami.loadbalancer.server.port=80
    networks:
      - proxy
    restart: unless-stopped
EOF

Step 5: Start the stack and verify HTTPS

Bring the services online and watch Traefik’s first-run logs. The proxy will request and store certificates in acme.json. Ensure your DNS A record for whoami.example.com points to this server before you start.

docker compose up -d
docker logs -f traefik

When the logs show that certificates were obtained, browse to https://whoami.example.com. You should see a simple page from the whoami container over HTTPS. You can also test from the terminal:

curl -I https://whoami.example.com

Optional: Use DNS-01 for wildcard certificates

If you need a wildcard like *.example.com, switch to the DNS-01 challenge. This requires a DNS provider API token. As an example, for Cloudflare you would add two flags and an environment variable. Consult Traefik’s documentation for the exact variable names supported by your DNS provider.

# In docker-compose.yml, replace the HTTP-01 lines with:
      - --certificatesresolvers.le.acme.dnschallenge=true
      - --certificatesresolvers.le.acme.dnschallenge.provider=cloudflare

# And add, under the traefik service:
    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}

# Then define it in .env (use a least-privilege token scoped to DNS edit):
CF_DNS_API_TOKEN=your_cloudflare_dns_token

After changing the challenge type, remove the old acme.json or start with a fresh file, then redeploy so Traefik can issue new certificates using DNS-01.

Security and maintenance tips

- Keep the Docker socket mount read-only, as shown, to reduce risk. Consider using a socket proxy if you expose Traefik to untrusted networks.

- Do not expose the Traefik dashboard publicly without authentication. If needed, put the dashboard behind a router with basic auth middleware and IP allowlists.

- Back up /opt/traefik/letsencrypt/acme.json. It contains your issued certificates and keys.

- Use a process manager (Compose restart policy is already set) and keep Traefik updated to the latest v3 patch release.

Troubleshooting

- If Let’s Encrypt fails, confirm that ports 80 and 443 are reachable from the internet and that your DNS A record is correct. The HTTP-01 challenge requires port 80 to reach the server.

- Check logs with docker logs -f traefik for clear error messages (rate limiting, challenge timeouts, or permission issues).

- Ensure acme.json permissions remain 600. Incorrect permissions can prevent Traefik from writing certificates.

- If labels do not seem to apply, verify that the whoami container is on the same Docker network named proxy and that exposedbydefault is false (which requires explicit traefik.enable=true).

You now have a production-ready reverse proxy on Ubuntu 24.04 with automatic HTTPS, simple app onboarding via labels, and a clean path to wildcard certificates when needed.

How to Self‑Host Open WebUI and Ollama on Ubuntu with Docker, HTTPS, and NVIDIA GPU Support

Overview

This guide shows how to self-host a private AI chatbot with Open WebUI (a clean, ChatGPT-like interface) and Ollama (for running local large language models) on Ubuntu 22.04 or 24.04. Everything runs in Docker, secured with HTTPS via Caddy and optional Basic Auth. If you have an NVIDIA GPU, you can enable GPU acceleration to speed up model inference dramatically.

What you will need

- An Ubuntu 22.04/24.04 server with at least 8 GB RAM and 20 GB free disk space. For GPU acceleration, an NVIDIA GPU with recent drivers is recommended (e.g., 8 GB VRAM or more for larger models).

- A domain name pointing to your server’s public IP (A/AAAA record). Ports 80 and 443 should be open to the internet for Let’s Encrypt.

- A non-root user with sudo privileges.

Step 1 — Install Docker and Docker Compose plugin

Update your system and install Docker from the official repository:

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 — (Optional) Enable NVIDIA GPU for containers

Install the NVIDIA driver (if not already installed) and the NVIDIA container toolkit so Docker can access your GPU.

sudo ubuntu-drivers install (or choose a specific driver, e.g., sudo apt install -y nvidia-driver-535)

sudo reboot

Install the container toolkit:

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://#' | 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

Test GPU access:

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

Step 3 — Prepare Docker Compose and Caddy

Create a project folder and move into it:

mkdir -p ~/ai-stack && cd ~/ai-stack

Create a file named docker-compose.yml with the following content (replace your.domain.com later in Caddyfile):

services:
ollama:
image: ollama/ollama:latest
container_name: ollama
volumes:
- ollama:/root/.ollama
environment:
- OLLAMA_KEEP_ALIVE=2h
ports:
- "127.0.0.1:11434:11434"
restart: unless-stopped
# Uncomment the next line if you enabled NVIDIA toolkit
# gpus: all

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

caddy:
image: caddy:2
container_name: caddy
depends_on:
- openwebui
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config

volumes:
ollama:
openwebui:
caddy_data:
caddy_config:

Create a file named Caddyfile in the same folder. Replace your.domain.com with your real domain and the email with yours:

your.domain.com {
encode zstd gzip
tls [email protected]
# Optional Basic Auth — generate a hashed password below and uncomment
# basicauth {
# admin <paste_hashed_password_here>
# }
reverse_proxy openwebui:8080
}

If you want Basic Auth, generate a hash:

docker run --rm caddy:2 caddy hash-password --plaintext "StrongPassword!"

Copy the hash output, paste it into the Caddyfile under basicauth, and uncomment the lines.

Step 4 — Start the stack and pull a model

Start the services:

docker compose up -d

Pull a model with Ollama. Llama 3.1 is a great default; you can also choose smaller variants if you have less VRAM:

docker exec -it ollama ollama pull llama3.1

For low VRAM systems, try a quantized build like llama3.1:8b-instruct-q4_0 or a compact model like mistral:7b-instruct:

docker exec -it ollama ollama pull mistral:latest

Verify Ollama is up:

curl -s http://127.0.0.1:11434/api/tags

Step 5 — Access Open WebUI over HTTPS

Wait 30–60 seconds for Caddy to obtain a Let’s Encrypt certificate. Then browse to https://your.domain.com. On the first visit, create your Open WebUI admin user. In Settings > Models, select the model you pulled with Ollama. You can now chat privately with your local LLM through a friendly web interface.

Step 6 — Security hardening (recommended)

- Keep Open WebUI behind Caddy only. We already published it on localhost (127.0.0.1) to prevent direct exposure.

- Enable Basic Auth in your Caddyfile if you plan to expose the site to the open internet. Use a long, unique password.

- Restrict admin features in Open WebUI to your own account. Disable public sign-ups if you do not need them.

- Consider a firewall rule to allow inbound 80/443 only, and block 8080/11434 from the WAN.

Step 7 — Backups and updates

Back up Open WebUI data:

docker run --rm -v openwebui:/d -v $PWD:/b busybox tar czf /b/openwebui-backup.tgz -C /d .

Back up Ollama models (can be large):

docker run --rm -v ollama:/d -v $PWD:/b busybox tar czf /b/ollama-backup.tgz -C /d .

To update containers:

docker compose pull && docker compose up -d

To remove old images:

docker image prune -f

Troubleshooting

- Check logs if something fails to start: docker compose logs -f

- Verify DNS and port 80/443 reach the server; Let’s Encrypt must connect over HTTP/HTTPS the first time.

- If certificates fail, restart the stack after DNS propagates: docker compose down && docker compose up -d

- If the GPU is not detected, confirm nvidia-smi works on the host and that you added gpus: all under the Ollama service.

- Test the Ollama API locally: curl http://127.0.0.1:11434/api/generate -d '{"model":"llama3.1","prompt":"hi"}'

Where to go next

Explore model variants optimized for your hardware (Q4 for low VRAM, Q6/Q8 for higher quality, FP16 on strong GPUs). Add embeddings and RAG features in Open WebUI to chat over your documents. With this setup, you keep your data and traffic on your own server, with clean HTTPS, optional password protection, and fast local inference.

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.

Deploy Ollama + Open WebUI on Ubuntu with Docker Compose (GPU Optional) and HTTPS

Overview

This guide shows you how to deploy Ollama and Open WebUI on Ubuntu using Docker Compose, with optional NVIDIA GPU acceleration and automatic HTTPS. You will get a clean, reproducible setup suitable for a home lab, a developer VM, or a small on-prem server. The steps are focused on Ubuntu 22.04/24.04 LTS, but will work on other modern distributions with minor changes.

What You Will Build

You will run three containers: Ollama (LLM runtime), Open WebUI (a friendly web front end), and Caddy (a reverse proxy that issues and renews free TLS certificates). Data will persist in Docker volumes so updates and restarts do not wipe your models or chat history.

Prerequisites

1) An Ubuntu server with at least 16 GB RAM recommended for medium models (more is better). 2) A domain or subdomain (e.g., ai.example.com) pointed to your server’s public IP (A/AAAA record). 3) Ports 80 and 443 open to the Internet. 4) Optional: an NVIDIA GPU with recent drivers for acceleration. 5) A non-root user with sudo.

Step 1 — Install Docker and Compose

Update the OS and install Docker Engine and the Compose plugin from Docker’s repository:

sudo apt update && sudo apt upgrade -y
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 — (Optional) Enable NVIDIA GPU for Containers

If you have an NVIDIA GPU, install the driver and the NVIDIA Container Toolkit so Ollama can use CUDA.

Install drivers: sudo ubuntu-drivers autoinstall, then reboot. Verify with nvidia-smi.

Install the container toolkit:

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-keyring.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-keyring.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

Step 3 — Prepare the Project

Create a directory for your stack and move into it:

mkdir -p ~/ollama-stack && cd ~/ollama-stack

We will create a docker-compose.yml and a Caddyfile. Replace ai.example.com and your email as needed.

Step 4 — Docker Compose File

Create docker-compose.yml with the content below. If you have a GPU, keep the deploy.resources.reservations.devices section; otherwise you can remove it.

services:
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]

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

caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- openwebui

volumes:
ollama_data:
openwebui_data:
caddy_data:
caddy_config:

Step 5 — Caddy Reverse Proxy

Create Caddyfile with your domain. Caddy will automatically issue and renew a Let’s Encrypt certificate and proxy traffic to Open WebUI.

ai.example.com {
encode gzip
reverse_proxy openwebui:8080
}

Ensure your DNS A/AAAA record points to the server before continuing. If you only need local access, you can skip Caddy and access Open WebUI on http://SERVER_IP:8080 by publishing that port; however, TLS is strongly recommended.

Step 6 — Launch the Stack

Start everything with Docker Compose:

docker compose up -d

Watch the logs for any errors, especially domain or certificate issues:

docker compose logs -f caddy

After a minute, visit https://ai.example.com and complete the initial Open WebUI setup. In Settings, verify the Ollama endpoint is http://ollama:11434 (it should be pre-set from the environment variable).

Step 7 — Pull a Model and Test

You can pull and manage models via the Open WebUI interface, or via the CLI inside the Ollama container:

docker exec -it ollama ollama pull llama3.1
docker exec -it ollama ollama run llama3.1

If you enabled GPU support, Ollama should automatically leverage CUDA. You can confirm GPU usage with nvidia-smi while running a prompt.

Security and Hardening Tips

- Create an admin user in Open WebUI and do not expose the Ollama port 11434 to the Internet unless you really need the API externally. In the Compose file above, only Caddy is published publicly on 80/443, which is safer.

- Restrict access by IP or add basic auth in Caddy if you want a quick gate. Example inside your site block: basicauth { user JDJhJDEw$... } (generate hashes with caddy hash-password).

- Keep images updated: docker compose pull && docker compose up -d. Consider enabling automatic re-deploys on a schedule.

Performance Hints

- Use models that fit your VRAM/RAM. Smaller models like q4_K_M quantizations work well on modest GPUs and CPUs. For CPU-only servers, prefer 7B or smaller models.

- Set swap if RAM is tight: sudo fallocate -l 16G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile. Add to /etc/fstab for persistence.

- Place Docker volumes on fast storage (NVMe) for quicker model load times. You can bind-mount a directory like ./ollama:/root/.ollama if you prefer easy backups.

Backup and Restore

Back up the volumes for Ollama and Open WebUI to keep models and chat history. Example quick backup of models:

docker run --rm -v ollama_data:/data -v $(pwd):/backup alpine tar czf /backup/ollama-data.tgz -C /data .

Repeat similarly for openwebui_data. To restore, reverse the process by untarring into an identically named volume.

Troubleshooting

- If Caddy fails to get a certificate, verify your DNS record, that ports 80/443 are reachable, and no other service (like another web server) is binding them.

- If GPU is not detected, confirm nvidia-smi works on the host and that the nvidia-container-toolkit is installed. Restart Docker and the containers after changes.

- If Open WebUI cannot reach Ollama, ensure the environment variable points to http://ollama:11434 and that both containers share the same default network (they do in this Compose file).

Conclusion

You now have a production-grade, self-hosted LLM stack with Ollama and Open WebUI, managed by Docker Compose and protected by automatic HTTPS via Caddy. This setup is easy to maintain, portable across servers, and ready for experimentation or internal use. With GPU acceleration, you can serve sophisticated models efficiently; without a GPU, you can still run smaller quantized models for private inference. Keep your containers updated, monitor resource usage, and iterate on models that best fit your hardware and use cases.

Self-Host an AI Chat UI: Install Ollama + Open WebUI with GPU and HTTPS on Ubuntu 22.04

Overview

This step-by-step guide shows you how to self-host a modern AI chat interface by combining Ollama (for running local large language models) with Open WebUI (a friendly web front end). We will deploy everything on Ubuntu 22.04 using Docker, enable optional NVIDIA GPU acceleration, and secure access with HTTPS via Caddy. The result is a fast, private, and maintainable AI setup for your lab, team, or home server.

Prerequisites

You will need: (1) An Ubuntu 22.04+ 64-bit server with at least 8 GB RAM; (2) Optional NVIDIA GPU for acceleration; (3) A domain name pointing to your server’s public IP if you want HTTPS; (4) A sudo-enabled user; (5) Basic firewall access to ports 22, 80, and 443.

Update the system

Run the following to update packages:
sudo apt-get update && sudo apt-get -y upgrade

Install Docker (and let your user run it)

Install Docker using the official convenience script:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Enable NVIDIA GPU support (optional but recommended)

If your server has an NVIDIA GPU, install drivers and the container toolkit so Docker can access the GPU:
sudo ubuntu-drivers autoinstall
sudo reboot

After reboot, verify:
nvidia-smi

Install the NVIDIA container toolkit and wire it to Docker:
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

Test GPU passthrough (optional):
docker run --rm --gpus all nvidia/cuda:12.3.1-base-ubuntu22.04 nvidia-smi

Create a dedicated Docker network

A user-defined network makes service-to-service communication simpler:
docker network create ai

Run Ollama (the local model runtime)

Start Ollama as a background service and persist its model data in a named volume:
docker volume create ollama
docker run -d --name ollama --restart unless-stopped --network ai -p 11434:11434 -v ollama:/root/.ollama ollama/ollama:latest

If you have a GPU, add --gpus all:
docker run -d --name ollama --restart unless-stopped --network ai -p 11434:11434 -v ollama:/root/.ollama --gpus all ollama/ollama:latest

Pull at least one model (examples include llama3.1, mistral, qwen2, phi3). For a balanced start, try an 8B parameter model:
docker exec -it ollama ollama pull llama3.1:8b

Run Open WebUI (the chat interface)

Deploy Open WebUI and link it to the Ollama API URL across the same Docker network:
docker volume create open-webui
docker run -d --name open-webui --restart unless-stopped --network ai -p 3000:8080 -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://ollama:11434 open-webui/open-webui:latest

Open a browser to http://SERVER_IP:3000 to complete the initial setup. Create the first admin user, then in Settings disable open signups if this is a private deployment.

Add HTTPS with Caddy (automatic certificates)

Caddy can obtain and renew Let’s Encrypt certificates for you. Create a simple Caddyfile in your home directory with this content (replace yourdomain.com):
yourdomain.com {
  reverse_proxy 127.0.0.1:3000
}

Run Caddy in Docker and bind ports 80/443:
docker volume create caddy-data
docker volume create caddy-config
docker run -d --name caddy --restart unless-stopped -p 80:80 -p 443:443 -v $PWD/Caddyfile:/etc/caddy/Caddyfile -v caddy-data:/data -v caddy-config:/config caddy:latest

Point your domain’s DNS A record to the server’s IP, wait for propagation, and then visit https://yourdomain.com to use Open WebUI securely.

Useful Open WebUI and Ollama tips

Inside Open WebUI, go to Models and set your default model to the one you pulled. You can pull more models anytime with:
docker exec -it ollama ollama pull mistral:7b
docker exec -it ollama ollama pull qwen2:7b
docker exec -it ollama ollama pull phi3:mini

For faster responses, enable GPU quantized models (e.g., Q4_K_M). On low-RAM VPS, pick smaller models like phi3:mini or llama3.1:8b-instruct with 4-bit quantization.

Update and maintenance

To update containers without losing data:
docker pull ollama/ollama:latest
docker pull open-webui/open-webui:latest
docker pull caddy:latest
docker stop open-webui ollama caddy
docker rm open-webui ollama caddy
Repeat the docker run commands from earlier to recreate; volumes preserve your data and models.

To back up important data:
docker run --rm -v ollama:/data -v $PWD:/backup alpine tar czf /backup/ollama-backup.tgz -C / data
docker run --rm -v open-webui:/data -v $PWD:/backup alpine tar czf /backup/openwebui-backup.tgz -C / data

Firewall and security tips

If using UFW, allow only needed ports:
sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw enable

Harden Open WebUI by turning off public signups, using strong admin passwords, and placing the service behind HTTPS. For additional isolation, restrict Open WebUI to listen only on localhost and expose it solely via Caddy (default Docker run above binds to 0.0.0.0; you can change -p 3000:8080 to -p 127.0.0.1:3000:8080).

Troubleshooting

If Open WebUI shows “Cannot reach Ollama,” verify the network and base URL:
docker logs open-webui
docker logs ollama
docker exec -it open-webui wget -qO- http://ollama:11434/api/tags

If GPU is not detected, confirm drivers and toolkit:
nvidia-smi
docker run --rm --gpus all nvidia/cuda:12.3.1-base-ubuntu22.04 nvidia-smi
If that fails, re-run:
sudo nvidia-ctk runtime configure --runtime=docker && sudo systemctl restart docker

Certificate issues? Ensure port 80/443 are reachable from the internet and that your DNS A record is correct. Check Caddy logs:
docker logs caddy

What you built

You now have a private AI chat platform that runs on your hardware, speaks to high-quality local models via Ollama, provides a clean web interface with Open WebUI, and is secured with HTTPS. This stack is simple to update, performs well with GPUs, and is flexible enough to scale with new models and plugins as your needs evolve.

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 ...