Expose Your Home Lab Securely: Cloudflare Tunnel + Nginx Proxy Manager (Docker Guide)

Publishing self-hosted apps on the internet without opening ports is now simple and safe with Cloudflare Tunnel and Nginx Proxy Manager (NPM). This guide shows you how to route traffic from your domain through Cloudflare’s global network to your internal services, using Docker on Linux. You will get HTTPS by default, Zero Trust access, and simple management for multiple apps.

Why use Cloudflare Tunnel + Nginx Proxy Manager?

Cloudflare Tunnel (cloudflared) creates an outbound-only connection from your server to Cloudflare, so you do not need port forwarding or a public IP. Nginx Proxy Manager provides a friendly UI to reverse-proxy multiple services, manage SSL, and handle redirects and headers. Together, they offer a secure and flexible edge-to-origin pipeline for home labs and small businesses.

Prerequisites

- A domain managed by Cloudflare (nameservers must point to Cloudflare)
- A Linux server (Ubuntu 22.04+ recommended) with Docker and Docker Compose installed
- Basic familiarity with terminal and Docker

Architecture overview

Cloudflare edge terminates TLS and forwards requests over an encrypted tunnel to the cloudflared container on your server. The cloudflared service forwards hostnames to Nginx Proxy Manager, which then routes requests to internal applications (e.g., Home Assistant, Portainer, Jellyfin) based on hostnames. This design centralizes access, certificates, logging, and security rules.

Step 1 — Install Docker and Compose (Ubuntu)

Run the following commands to set up Docker:

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 $(lsb_release -cs) 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 — Create a Cloudflare Tunnel

1) In the Cloudflare dashboard, open Zero Trust (or Tunnels) and create a new Tunnel named “homelab”.
2) Choose “Docker” as the environment and copy the token-based command or credentials JSON for cloudflared. We will use the token method for simplicity.
3) Do not add routes yet—we will define them in the docker-compose file.

Step 3 — Create docker-compose.yml

Create a working directory (e.g., /opt/homelab) and add this compose file. Replace example.com with your domain and paste your Cloudflare tunnel token.

version: "3.8"
services:
  cloudflared:
    image: cloudflare/cloudflared:2024.8.3
    command: tunnel run
    environment:
      - TUNNEL_TOKEN=<PASTE_YOUR_TUNNEL_TOKEN>
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "cloudflared", "version"]
      interval: 30s
      timeout: 10s
      retries: 3

  npm:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    ports:
      - "81:81"        # NPM admin UI
      - "80:80"        # HTTP
      - "443:443"      # HTTPS
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - cloudflared

networks:
  default:
    name: homelab

Start the stack:

docker compose up -d
docker compose logs -f cloudflared

Step 4 — Map hostnames to Nginx Proxy Manager

Back in the Cloudflare dashboard, open your Tunnel and add public hostnames to route traffic to NPM. For each app, create a route like this:

Example routes:
- app1.example.com → http://npm:80
- media.example.com → http://npm:80
- portainer.example.com → http://npm:80

This means Cloudflare forwards incoming requests for those hostnames down the tunnel to the NPM container.

Step 5 — Configure Nginx Proxy Manager

1) Open http://YOUR_SERVER_IP:81 and log in (default admin credentials are shown on first boot; change them immediately).
2) For each internal service, create a “Proxy Host”:
- Domain Names: app1.example.com
- Scheme: http
- Forward Hostname/IP: the internal container name or IP (e.g., homeassistant or 127.0.0.1)
- Forward Port: the app port (e.g., 8123)
- Block Common Exploits: enabled
- Websockets Support: enabled (for apps like Home Assistant, Portainer, or anything real-time)

3) Under the SSL tab, select “Request a new SSL Certificate” and choose Let’s Encrypt. Enable “Force SSL” and “HTTP/2 Support”. NPM will fetch and auto-renew certificates for the hostname.

Step 6 — Enforce Zero Trust access (optional but recommended)

Use Cloudflare Access to protect sensitive apps with identity-aware policies. In Zero Trust → Access → Applications, add an app for app1.example.com, choose “Self-hosted”, and require login with your IdP (Google, GitHub, Microsoft, etc.). You can restrict by email domain, group, or country. Access will challenge users at the edge before requests hit your tunnel.

Step 7 — Security hardening tips

- In Cloudflare DNS, keep your A/AAAA records orange-cloud (proxied).
- Enable WAF and Bot Fight Mode where appropriate.
- For APIs or admin panels, add Access policies and IP allowlists in Cloudflare, and enable “Block Common Exploits” in NPM.
- If you need end-to-end encryption to NPM, set up a trusted origin certificate from Cloudflare and configure NPM to use HTTPS upstreams.

Troubleshooting

502 Bad Gateway: Check the NPM “Forward Hostname/IP” and port. Ensure the target app is reachable from the NPM container network.

403 from Cloudflare Access: Confirm your email is allowed by the Access policy and that your device clock is correct.

WebSockets not working: Enable “Websockets Support” in NPM and verify the app uses the correct path. Most real-time dashboards require this.

Large uploads fail: In NPM, add a Custom Nginx config snippet such as client_max_body_size 100m; for the specific host.

SSL mismatch or loops: Use HTTP between Cloudflare and NPM if Cloudflare terminates TLS at the edge. If you enable origin TLS, make sure certificates and trust are configured properly.

Scaling and operations

- Add more hostnames in the Tunnel as you publish new services; just point them to http://npm:80 and configure the upstream in NPM.
- Use multiple cloudflared instances (on different servers) in the same Tunnel for high availability; Cloudflare will load-balance them.
- Monitor with docker compose logs -f and Cloudflare analytics. Schedule updates with watchtower or perform manual rolling updates.

Wrap-up

You have exposed internal services securely on your own domain without opening any inbound ports. Cloudflare Tunnel handles the edge and connectivity, while Nginx Proxy Manager gives you a clean UI for reverse proxy rules, SSL, and performance tweaks. With Access policies, WAF, and good Nginx hygiene, you can safely run public-facing apps from a single Docker host.

How to Deploy a Zero‑Trust WireGuard VPN with Tailscale on Ubuntu Server (2025 Guide)

Overview

This step-by-step guide shows how to deploy a zero-trust VPN using Tailscale (built on WireGuard) on Ubuntu Server. You will install the Tailscale client, log in with SSO, enable secure SSH, configure access control lists (ACLs), expose a private subnet, and optionally offer an exit node. The result is a modern, fast, and secure VPN with minimal maintenance and strong identity-based access controls—perfect for homelabs and production servers in 2025.

Why Tailscale (WireGuard) for Zero Trust

Tailscale uses the WireGuard protocol for speed and strong cryptography while removing the operational pain of traditional VPNs. Devices authenticate using your identity provider (Google, Microsoft, Okta, GitHub, and others) and connect peer-to-peer where possible. You gain per-device keys, automatic NAT traversal, policy-based access (ACLs), MagicDNS, and optional Tailscale SSH that replaces inbound firewall holes.

Prerequisites

You need an Ubuntu Server 22.04 or 24.04 host with sudo access. Ensure outbound HTTPS (TCP 443) and UDP 41641 are allowed. No inbound ports are strictly required. Have a browser handy to authenticate to your identity provider or prepare a reusable auth key from the Tailscale admin console for headless systems.

Install Tailscale on Ubuntu

Run these commands to add the official repository and install Tailscale. The snippet auto-detects your Ubuntu codename:

curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(. /etc/os-release; echo $VERSION_CODENAME).noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null

curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(. /etc/os-release; echo $VERSION_CODENAME).tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list

sudo apt-get update && sudo apt-get install -y tailscale

Authenticate and bring the node online

For interactive login, start Tailscale and enable Tailscale SSH. This avoids exposing port 22 to the internet and lets you restrict SSH via ACLs:

sudo tailscale up --ssh --operator=$USER --accept-dns=true --accept-routes=true --advertise-tags=tag:server

If the server is headless, generate an auth key in the Tailscale admin console (preferably tagged and reusable or ephemeral) and run:

sudo tailscale up --ssh --authkey=tskey-******** --advertise-tags=tag:server

Verify connectivity with tailscale status, view the device IPs via tailscale ip -4, and test pings to another node using tailscale ping <device-or-name>.

Enable zero-trust access controls (ACLs)

Open the Tailscale admin console and switch to the ACLs page. Keep policies simple and human-readable. Example: allow your admin group to SSH to servers and let developers access staging web ports:

{
  "groups": { "group:admins": ["[email protected]"], "group:devs": ["[email protected]"] },
  "tagOwners": { "tag:server": ["group:admins"] },
  "acls": [
    { "action": "accept", "users": ["group:admins"], "ports": ["tag:server:22,2222"] },
    { "action": "accept", "users": ["group:devs"], "ports": ["tag:server:80,443,8080"] }
  ],
  "ssh": [
    { "action": "check", "src": ["group:admins"], "dst": ["tag:server"], "users": ["root", "ubuntu"] }
  ]
}

This policy makes tag:server devices manageable by admins, grants controlled port access, and restricts Tailscale SSH to authorized identities. Commit and save to enforce instantly.

Expose your LAN with a Subnet Router (optional)

If the Ubuntu host can reach a private LAN (e.g., 192.168.10.0/24), you can advertise that subnet to Tailscale peers without opening your firewall. First, enable IP forwarding:

echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf

echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf

sudo sysctl --system

Now advertise the subnet routes:

sudo tailscale up --advertise-routes=192.168.10.0/24 --accept-routes=true

Approve the routes in the admin console. For UFW, permit forwarding within the LAN and from the Tailscale interface (typically tailscale0):

sudo ufw route allow in on tailscale0 out on eth0 to 192.168.10.0/24

sudo ufw reload

Set up an Exit Node (optional)

An exit node lets approved devices send all internet traffic through your Ubuntu server. This is useful for securing devices on public Wi‑Fi or egressing from a fixed IP. Enable it on the server:

sudo tailscale up --advertise-exit-node=true

In the admin console, allow the exit node and then, from a client, choose “Use exit node”. Optionally permit local LAN access while using the exit node by enabling “Allow LAN access” on the client.

Security hardening and best practices

Restrict who can reach what by tags and groups; avoid broad *:* policies. Prefer Tailscale SSH over public SSH. Disable password auth in OpenSSH (sudoedit /etc/ssh/sshd_config, set PasswordAuthentication no) and restart SSH. Keep the system current and enable unattended upgrades:

sudo apt-get install -y unattended-upgrades

sudo dpkg-reconfigure --priority=low unattended-upgrades

If you use UFW, you generally do not need to open inbound ports for Tailscale. Traffic arrives over the encrypted tunnel and is handled by tailscaled. For large fleets, use ephemeral auth keys for CI/CD runners (--ephemeral) and shorter key lifetimes. Consider using device posture checks and auto-approvers in ACLs where appropriate.

Troubleshooting quick checks

Run sudo tailscale bugreport to gather diagnostics if needed. Use tailscale netcheck to verify NAT traversal, tailscale status to view peers, and tailscale ping to test reachability. If subnets are not reachable, confirm routes are approved and that IP forwarding and UFW rules allow routed traffic. If speeds seem low, ensure direct connections are established (not relayed) and verify that CPU scaling or virtualization offloads are not limiting WireGuard throughput on the server.

What you achieved

You installed a production-ready, zero-trust WireGuard VPN with Tailscale on Ubuntu, authenticated it with SSO, enabled Tailscale SSH, enforced fine-grained ACLs, and optionally provided subnet routing and exit-node functionality. This approach is simpler, faster, and safer than legacy site-to-site or username/password VPNs, and it scales from a single VPS to a multi-site enterprise network with minimal toil.

Securely Expose Your Home Lab with Cloudflare Tunnel (Zero Trust) on Ubuntu and Docker

Overview

This step-by-step guide shows you how to publish a private web service to the internet securely using Cloudflare Tunnel (cloudflared) and Cloudflare Zero Trust—without opening inbound ports on your router. You will run the tunnel in Docker on Ubuntu, route your domain through Cloudflare, protect the app with Single Sign-On (SSO), and optionally verify JSON Web Tokens (JWT) at the application layer. This approach adds strong security, free TLS, DDoS protection, and granular access control to your self-hosted apps.

Prerequisites

You need an Ubuntu 22.04 or 24.04 server (bare metal or VM), Docker and Docker Compose installed, a domain you control, and a Cloudflare account. If your domain is not already on Cloudflare, change your registrar’s nameservers to Cloudflare’s to manage DNS and Zero Trust features.

Step 1 — Prepare your domain on Cloudflare

1. Log in to Cloudflare and add your domain if you have not already. Follow the prompts to update nameservers at your registrar. Propagation may take a few minutes.

2. In the Cloudflare dashboard, verify that DNS is active and the orange cloud is enabled for your root or subdomains.

Step 2 — Enable Zero Trust

1. Open the Zero Trust dashboard (also labeled “Zero Trust” or “Access” in Cloudflare). Create your Zero Trust account if prompted.

2. Under Settings > Authentication, connect an identity provider (e.g., Google, Microsoft Entra ID, GitHub) or enable the One-Time Pin option for quick protection.

Step 3 — Install Docker and Compose (if needed)

If Docker is not installed, use the official convenience script. Then enable and test it. Example:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
docker --version

Install Docker Compose v2 if it is not present (many modern Docker packages include it as docker compose):

docker compose version

Step 4 — Create a Tunnel in the Cloudflare Dashboard

1. In Zero Trust > Networks > Tunnels, click “Create a tunnel,” choose “Cloudflared,” name it (e.g., homelab-tunnel), and create it.

2. Copy the provided token. You will use this token in Docker to authenticate the tunnel without interactive login.

Step 5 — Run cloudflared in Docker

Create a working directory, then a docker-compose.yml file. Replace the placeholder token with yours.

mkdir -p ~/cloudflared && cd ~/cloudflared
nano docker-compose.yml

version: "3.8"
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel run
    restart: unless-stopped
    environment:
      - TUNNEL_TOKEN=<PASTE_YOUR_TUNNEL_TOKEN>
    volumes:
      - ./config:/etc/cloudflared

Bring it up:

docker compose up -d

The container will authenticate with Cloudflare automatically using the token. You should see the tunnel listed as “Healthy” in the Zero Trust dashboard within a minute.

Step 6 — Configure ingress rules to publish your service

Create a config file to route a public hostname to your internal service. For example, map app.yourdomain.com to a local web app on port 8080 and ha.yourdomain.com to Home Assistant on port 8123.

mkdir -p config
nano config/config.yml

tunnel: <YOUR_TUNNEL_ID>
credentials-file: /etc/cloudflared/<YOUR_TUNNEL_ID>.json
ingress:
  - hostname: app.yourdomain.com
     service: http://host.docker.internal:8080
  - hostname: ha.yourdomain.com
     service: http://host.docker.internal:8123
  - service: http_status:404

On Linux, host.docker.internal may not resolve by default. Use the host IP (e.g., http://192.168.1.50:8080) or attach the tunnel to the same Docker network as your app containers and reference them by service name (e.g., http://web:8080).

Restart the tunnel to apply changes:

docker compose restart

Step 7 — Route DNS to the tunnel

Back in Zero Trust > Networks > Tunnels, open your tunnel and add Public Hostnames. Enter app.yourdomain.com and select the corresponding service. Cloudflare will automatically create proxied DNS records and issue valid TLS certificates.

Step 8 — Protect the app with Cloudflare Access (SSO/MFA)

1. Go to Zero Trust > Access > Applications > Add an application > Self‑hosted.

2. Set the application domain to app.yourdomain.com, pick a name, and click Next.

3. Create a policy: allow emails from your domain (e.g., [email protected]) or a group from your IdP. Optionally require MFA with Cloudflare’s TOTP or your IdP’s enforced MFA.

4. Save. Now your app is behind an identity-aware proxy. Only authenticated users you choose can reach it.

Optional — Verify JWT in your application or reverse proxy

Cloudflare Access sends a signed JWT in the CF-Access-Jwt-Assertion header. Your app or Nginx can verify it for an extra layer of defense-in-depth. In Nginx, pass the header to upstream or validate with an auth_request service. If you use languages like Node.js, Python, or Go, verify using the public JWKs at https://<your-team>.cloudflareaccess.com/cdn-cgi/access/certs and check audience and issuer claims.

Monitoring and troubleshooting

Check health: In the Cloudflare dashboard, your tunnel should be Healthy. On the host, view logs with docker logs -f <container_name>.

403 after login: Ensure your Access policy allows your email or group, and that the application domain matches the hostname you visit.

Bad gateway: Verify the internal service address and port in config.yml. Confirm the service is reachable from the tunnel container’s network.

DNS mismatch: Confirm the Public Hostname in the tunnel matches the DNS entry and the Access application domain.

IPv6 or CGNAT: Cloudflare Tunnel works without inbound ports, even behind CGNAT. No router changes are needed.

Security best practices

Use strong identity controls with MFA and short Access session durations. Limit policies to specific users or groups, not anyone with the link. Avoid exposing administrative apps publicly unless protected by Access. Keep cloudflared updated by periodically pulling the latest Docker image, and restrict who can manage your Cloudflare account with role-based access.

Conclusion

With Cloudflare Tunnel and Zero Trust, you can publish internal apps safely without port forwarding or complicated firewall rules. You get automatic TLS, DDoS protection, identity-based access, and optional JWT validation. This modern pattern is ideal for homelabs, small businesses, and remote teams that need secure, simple access to self‑hosted services.

How to Publish Your Home Lab Apps with Cloudflare Tunnel and Zero Trust Access (No Port Forwarding)

Overview

Exposing self-hosted services to the internet usually means opening ports, managing dynamic DNS, and hardening firewalls. Cloudflare Tunnel (cloudflared) flips that model. Your server dials out to Cloudflare, creates an encrypted tunnel, and serves traffic from a Cloudflare edge without any inbound ports. Add Cloudflare Zero Trust Access on top, and you get identity‑aware filtering, one‑time PIN, and granular policies. This guide shows how to publish a web app from a Linux server using Cloudflare Tunnel and secure it with Zero Trust.

Prerequisites

- A Cloudflare account with your domain added and nameservers pointing to Cloudflare.

- A Linux host (Ubuntu/Debian examples below) running the service you want to expose (e.g., a dashboard on port 8080).

- Shell access with sudo privileges.

Step 1 — Install cloudflared

On Ubuntu/Debian, install the official package. The commands below add the repository, verify signatures, and install cloudflared.

# Add Cloudflare GPG key and repository
sudo mkdir -p /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/cloudflared.list

# Install cloudflared
sudo apt update
sudo apt install -y cloudflared

# Verify
cloudflared --version

On other distributions, you can use a static binary or Docker. The rest of the steps are identical.

Step 2 — Authenticate and create a named tunnel

Run an interactive login to authorize cloudflared with your Cloudflare account. Your browser will open and ask you to pick the domain.

cloudflared tunnel login

Create a tunnel with a friendly name. This command outputs a UUID and writes a credentials JSON file into ~/.cloudflared.

cloudflared tunnel create homelab-tunnel

Keep the UUID; you will need it in the configuration file.

Step 3 — Map a public hostname to your local service

Choose a subdomain such as app.example.com and route it to the tunnel. Cloudflared will create the necessary DNS record in Cloudflare automatically.

cloudflared tunnel route dns homelab-tunnel app.example.com

Now define the ingress rules that connect the hostname to your local service (for example, a web UI on http://localhost:8080). Create a config at ~/.cloudflared/config.yml:

tunnel: <your-tunnel-uuid>
credentials-file: /home/<your-user>/.cloudflared/<your-tunnel-uuid>.json

ingress:
  - hostname: app.example.com
    service: http://localhost:8080
  - service: http_status:404

The final catch‑all rule returns a 404 for unmatched hostnames, which is safer than accidentally proxying everything.

Step 4 — Run cloudflared as a service

Install cloudflared as a system service so it survives reboots and restarts automatically.

# From within the directory containing ~/.cloudflared/config.yml
sudo cloudflared service install

# Start and enable on boot
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

If you prefer foreground mode for testing, you can run: cloudflared tunnel run homelab-tunnel, then switch to the service after you confirm it works.

Step 5 — Secure the app with Cloudflare Zero Trust Access

With the tunnel working, the app is reachable at your hostname. Next, lock it behind identity-aware access.

1) In the Cloudflare dashboard, go to Zero Trust → Access → Authentication and add an identity provider (Google, Microsoft, GitHub, or email OTP). Keep One-time PIN enabled for easy onboarding.

2) Go to Zero Trust → Access → Applications → Add an application → Self-hosted. Enter:

- Application name: Homelab App

- Domain: app.example.com

- Session duration: e.g., 12 hours

3) Create a policy: Include only your email(s) or a Google Workspace group. Optionally add country restrictions, device posture checks, or require MFA. Save the app.

From now on, anyone visiting app.example.com must authenticate. Cloudflare enforces the policy before traffic touches your origin.

Optional hardening

- Run your local service on 127.0.0.1 so it is not exposed on the LAN. Update your app config to bind to localhost.

- Use mTLS origin authentication (Cloudflare Origin Cert) to ensure only your tunnel can reach the service.

- Split production and testing into separate tunnels with distinct hostnames and policies.

- Enable HTTP/2 or WebSockets if your app needs them; cloudflared supports both.

Troubleshooting tips

- Check logs: journalctl -u cloudflared -f shows real-time output from the service.

journalctl -u cloudflared -n 200 --no-pager
cloudflared tunnel list
cloudflared tunnel info homelab-tunnel
cloudflared tunnel ingress validate

- DNS not resolving? Confirm the CNAME record for app.example.com exists and points to your tunnel. Clear local DNS cache or wait a few minutes for propagation.

- 502/504 errors? Ensure the local app is listening and reachable at the service URL in config.yml. Try curl http://localhost:8080 from the server.

- Multiple apps? Add more hostname blocks under ingress and create matching Access apps.

Maintenance and updates

Keep cloudflared current to receive security patches and new features.

sudo apt update
sudo apt install --only-upgrade cloudflared

Back up ~/.cloudflared/ (credentials and config) and your systemd unit files if customized. If you rotate the tunnel credentials, update the credentials-file path accordingly and restart the service.

Wrap-up

Cloudflare Tunnel gives you a secure, low-friction way to publish internal services without opening ports or managing a reverse proxy on the perimeter. Pairing it with Zero Trust Access means your apps live behind identity, not just IP addresses, and you can layer device posture, MFA, and short sessions. In a few commands, you can make your home lab or small business apps safer and easier to reach from anywhere.

WireGuard on Ubuntu 24.04: A Zero‑Trust VPN Setup with Windows and Mobile Clients

Overview

WireGuard is a modern VPN protocol that is fast, secure, and simple to deploy. In this tutorial, you will build a production-ready WireGuard server on Ubuntu 24.04 and connect Windows and mobile clients. You will configure routing, firewall rules, auto-start, and testing. The guide uses clear steps and SEO-friendly terms to help you go from zero to a working zero-trust VPN in minutes.

Prerequisites

You need an Ubuntu 24.04 server (cloud VPS or on-prem) with a public IP, sudo access, and UDP port 51820 open on any external firewall. If the server is behind a home router, forward UDP 51820 to the server’s LAN address. For clients, you need a Windows 10/11 PC and an Android or iOS device.

Step 1: Install WireGuard on Ubuntu 24.04

Update packages and install WireGuard tools:
sudo apt update && sudo apt install -y wireguard qrencode

Create a configuration directory and restrict permissions:
sudo mkdir -p /etc/wireguard && sudo chmod 700 /etc/wireguard

Step 2: Generate keys and base server config

Generate the server keypair:
cd /etc/wireguard
sudo wg genkey | sudo tee server_private.key | sudo wg pubkey | sudo tee server_public.key
sudo chmod 600 server_private.key

Set your VPN subnet and interface variables (eth0 is common on cloud VMs; adjust if yours differs):
export WG_IFACE=wg0
export WG_SUBNET=10.7.0.0/24
export SERVER_ADDR=10.7.0.1/24
export WAN_IFACE=eth0

Create the server configuration file:
sudo bash -c 'cat >/etc/wireguard/wg0.conf' <<EOF
[Interface]
Address = 10.7.0.1/24
ListenPort = 51820
PrivateKey = $(cat /etc/wireguard/server_private.key)
# Accept forwarding and NAT to the Internet
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ${WAN_IFACE} -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ${WAN_IFACE} -j MASQUERADE
EOF'

Step 3: Enable IP forwarding and open the port

Enable IPv4 forwarding persistently:
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system

If you use UFW, allow the WireGuard port:
sudo ufw allow 51820/udp

Start and enable the VPN interface:
sudo systemctl enable --now wg-quick@wg0

Check status and listen port:
sudo wg show

Step 4: Add a Windows client

On the server, generate a keypair for your Windows PC (you can also generate on the PC inside the app):
sudo wg genkey | sudo tee win_private.key | sudo wg pubkey | sudo tee win_public.key

Add the Windows peer to the server:
sudo bash -c 'cat >>/etc/wireguard/wg0.conf' <<EOF
[Peer]
PublicKey = $(cat /etc/wireguard/win_public.key)
AllowedIPs = 10.7.0.2/32
EOF'

Then restart the interface:
sudo systemctl restart wg-quick@wg0

On Windows, install the WireGuard app from the official site or Microsoft Store. Create a new tunnel with this configuration (replace placeholders):
[Interface]
PrivateKey = <win_private_key>
Address = 10.7.0.2/32
DNS = 1.1.1.1

[Peer]
PublicKey = <server_public_key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <server_public_ip>:51820
PersistentKeepalive = 25

Copy values:
<server_public_key> is the content of /etc/wireguard/server_public.key.
<win_private_key> is the content of win_private.key if you generated it on the server, otherwise use the private key generated by the Windows app.
AllowedIPs set to 0.0.0.0/0, ::/0 routes all traffic through the VPN (full tunnel). For split tunnel, use 10.7.0.0/24 only.

Step 5: Add a mobile client (Android/iOS)

On the phone, install the WireGuard app. Creating keys on the device is the most secure method: add a new tunnel, let the app generate keys, and copy the public key.

Add the mobile peer on the server (replace with the phone’s public key and desired IP):
sudo bash -c 'cat >>/etc/wireguard/wg0.conf' <<EOF
[Peer]
PublicKey = <mobile_public_key>
AllowedIPs = 10.7.0.3/32
EOF'
sudo systemctl restart wg-quick@wg0

On the mobile app, create or import a config like this (adjust placeholders):
[Interface]
PrivateKey = <mobile_private_key>
Address = 10.7.0.3/32
DNS = 1.1.1.1

[Peer]
PublicKey = <server_public_key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <server_public_ip>:51820
PersistentKeepalive = 25

Optional: if you prefer generating the mobile config on the server and scanning a QR code, create a file (for example /etc/wireguard/mobile1.conf) with the contents above and show a QR in the terminal:
sudo qrencode -t ansiutf8 < /etc/wireguard/mobile1.conf

Step 6: Auto-start, verify, and test

Ensure the interface starts on boot:
sudo systemctl enable wg-quick@wg0

Verify that peers handshaked and received IPs:
sudo wg show

Test connectivity from a client: open a browser and check your public IP (it should show the server’s IP if using a full tunnel). Also, ping the server’s VPN IP:
ping 10.7.0.1

Troubleshooting quick wins

No handshake? Confirm UDP 51820 is open and reachable. Use:
sudo ss -ulpn | grep 51820 on the server to see if it is listening, and sudo tcpdump -ni any udp port 51820 to check if packets arrive.

Wrong interface name? Replace eth0 with your actual outbound interface (check with ip route get 1.1.1.1). Update PostUp/PostDown accordingly and restart the service.

Double NAT issues? Set PersistentKeepalive = 25 on clients and ensure router port forwarding is correct.

Can’t access the Internet from the VPN? Confirm IPv4 forwarding is enabled and that NAT rules exist (see iptables -t nat -S). Also verify AllowedIPs values on both sides.

Security and best practices

Rotate keys periodically and remove stale peers from wg0.conf. Keep Ubuntu and WireGuard updated. Use strong SSH hygiene on the server and restrict management access by IP if possible. For compliance-driven environments, log changes to /etc/wireguard with version control (without committing private keys).

You now have a fast, modern WireGuard VPN on Ubuntu 24.04 with Windows and mobile clients. This layout is minimal yet production-ready and can scale by adding more peers with unique /32 addresses inside the same VPN subnet.

3.

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.

How to Build a Zero-Config Mesh VPN with Tailscale for Secure Remote Access

Overview

Tired of port forwarding, dynamic DNS, and brittle VPN configs? Tailscale gives you a zero-config mesh VPN built on WireGuard, letting your devices talk to each other securely from anywhere. In this step-by-step guide, you will install Tailscale on Linux, Windows, and macOS, enable MagicDNS, set up an exit node, publish LAN subnets, and lock everything down with access controls. By the end, you will have a private, encrypted network for your home lab or remote team that takes minutes to deploy and scales without hassle.

Prerequisites

You need a Tailscale account (Google, Microsoft, GitHub, or email sign-in), admin access to your devices, and a stable internet connection. For subnet routing and exit nodes, a Linux or always-on device is recommended. Enable multi-factor authentication in your identity provider for best security.

Step 1: Create Your Tailnet

Go to the Tailscale website and sign in to create your tailnet. This is your private network. Open the Admin Console and confirm your tailnet name. Under Settings, enable device approvals if you want manual approval before new devices join. This is useful for production and shared environments.

Step 2: Install Tailscale

Linux (Debian/Ubuntu)
Run:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
When prompted, sign in to link the device. If your distro uses a service, ensure tailscaled is running.

Linux (Fedora/RHEL derivatives)
Install and start:
sudo dnf install tailscale -y
sudo systemctl enable --now tailscaled
sudo tailscale up

Windows
Download the Windows client from Tailscale, install it, and sign in. The client will assign a 100.x Tailscale IP and show your hostname in the Admin Console.

macOS
Install the app from the Mac App Store or from Tailscale’s website. Sign in to connect the Mac to your tailnet.

iOS/Android
Install the mobile app and sign in. You can toggle the VPN on/off and optionally route all traffic through an exit node.

Step 3: Turn On MagicDNS (Human-Friendly Names)

In the Admin Console, open Settings → DNS and enable MagicDNS. This lets you reach devices by name, for example builder.tailnet-name.ts.net, instead of by 100.x IPs. Keep “Override local DNS” enabled on clients so name resolution just works across platforms.

Step 4: Enable Tailscale SSH (Passwordless, Keyless)

In Settings → Tailscale SSH, enable it for your tailnet. On servers, run:
sudo tailscale up --ssh
You can now SSH between devices using identity-based auth, e.g.:
ssh ubuntu@server-name
Access is controlled by the tailnet policy (ACLs) rather than managing per-host SSH keys.

Step 5: Use an Exit Node (Full-Tunnel Internet)

An exit node routes all internet traffic from a device through a trusted peer (great for coffee shops and travel). On the machine that will be the exit node, run:
sudo tailscale up --advertise-exit-node
In the Admin Console, approve the exit node. On the client, open the Tailscale app and choose “Use exit node” → select your device. Optionally enable “Allow LAN access” to still reach your local network while tunneling the internet.

Step 6: Publish Your LAN with a Subnet Router

A subnet router lets remote devices reach a private LAN (e.g., 192.168.1.0/24) through Tailscale. On a Linux host connected to that LAN, enable IP forwarding:
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
Persist these settings in /etc/sysctl.d/99-tailscale.conf. Then advertise routes:
sudo tailscale up --advertise-routes=192.168.1.0/24
In the Admin Console → Machines, approve the advertised routes. Clients can now access printers, NAS devices, and servers on that LAN using IP or hostnames (with your DNS). If needed, add --snat=false to preserve client IPs for upstream firewall logs.

Step 7: Lock It Down with ACLs and Tags

Open the Admin Console → Access Controls and edit the policy. Use groups and tags to define who can reach what. Example: allow helpdesk to RDP to Windows servers, and engineers to SSH into Linux hosts. A minimal snippet could look like:
{ "groups": { "group:helpdesk": ["[email protected]"] }, "tagOwners": { "tag:server": ["group:helpdesk", "group:eng"] }, "acls": [ { "action": "accept", "src": ["group:helpdesk"], "dst": ["tag:server:3389"] }, { "action": "accept", "src": ["group:eng"], "dst": ["tag:server:22"] } ] }
Apply tags on devices by running:
sudo tailscale up --advertise-tags=tag:server
Only tagged and authorized devices will accept those connections.

Step 8: Headless and Auto-Join with Auth Keys

For servers and containers, create a reusable or short-lived auth key in the Admin Console → Keys. On the device, run:
sudo tailscale up --authkey=tskey-abcdef --hostname=ci-runner-01 --advertise-tags=tag:server
Use ephemeral keys for throwaway CI agents, and rotate long-lived keys on a schedule. You can also inject TS_AUTHKEY as an environment variable in Docker or systemd units.

Step 9: Troubleshooting Essentials

If a device looks offline, first check the local service:
sudo systemctl status tailscaled (Linux). Then test reachability:
tailscale status
tailscale ping device-name
tailscale netcheck
Ensure outbound UDP 41641 is open; Tailscale falls back to relays (DERP) if direct NAT traversal fails. On Linux firewalls, allow UDP/41641 and established/related traffic. If routes are not working, confirm “Accept routes” is enabled and IP forwarding is on. For deep diagnostics, run tailscale bugreport and review logs in the Admin Console.

Security Best Practices

Require SSO and MFA for all users. Enable device approval and machine key expiry. Use groups and tags to enforce least privilege in ACLs. Restrict exit node usage to trusted admins. Regularly prune unused devices, rotate auth keys, and audit connections in the logs. Avoid exposing services publicly; instead, use MagicDNS, Tailscale SSH, or consider Tailscale Funnel selectively with HTTPS for public endpoints.

What You Can Do Next

With your mesh VPN live, map drives to a NAS over the tailnet, RDP into Windows servers from anywhere, tunnel VS Code SSH to a remote lab, or back up endpoints securely to a central repository. Tailscale scales from a weekend project to a production-ready fabric without the usual VPN pain. Most changes are policy-driven, so you can iterate quickly and keep operations clean.

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

Debian Adoption at CERN Signals Strong Momentum for Enterprise Linux

By the end of this article readers will understand the implications of CERN’s migration of 2,200 control systems to Debian 13, the performance enhancements in Firefox 155, and recent developments across several Linux distributions that affect system administration and user experience. Debian 13 Deployment at CERN: Scale and Significance The European Organization for Nuclear Research (CERN) has announced the migration of 2,200 of its control systems to Debian 13. This move represents one of the largest coordinated deployments of a Debian release in a scientific research environment. Control systems at CERN are responsible for monitoring and managing critical hardware, from accelerator components to detector subsystems. Their reliability hinges on a stable operating system with long‑term support, predictable update cycles, and a robust package ecosystem. Debian’s reputation for stability and its extensive testing process make it a natural fit for such mission‑critical workloads. Debia...