---
title: "Cloudflare Tunnel + Docker: Expose Containers Without Nginx or Open Ports"
slug: "cloudflared-tunnel-expose-docker-no-nginx-open-ports"
published: "2025-10-08"
updated: "2025-12-26"
categories:
  - "Docker"
tags:
  - "cloudflare tunnel docker"
  - "cloudflared tunnel"
  - "expose docker containers"
  - "nginx alternative"
  - "zero trust network"
  - "automatic ssl"
  - "reverse proxy docker"
  - "cloudflare tunnel setup"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "ubuntu@22.04+"
  - "docker@latest"
  - "cloudflared@latest"
  - "systemd@latest"
status: "stable"
llm-purpose: "Expose any Docker container publicly using Cloudflare Tunnel without Nginx or open ports. Simple setup, automatic SSL, DNS routing, and systemd."
llm-prereqs:
  - "Access to Cloudflare Tunnel"
  - "Access to Docker"
  - "Access to Ubuntu"
  - "Access to systemd"
llm-outputs:
  - "Completed outcome: Expose any Docker container publicly using Cloudflare Tunnel without Nginx or open ports. Simple setup, automatic SSL, DNS routing, and systemd."
---

**Summary Triples**
- (cloudflared, establishes, a persistent, encrypted outbound tunnel from your server to the Cloudflare edge)
- (Cloudflare Tunnel, eliminates, the need to open inbound ports 80/443 or install Nginx on the origin)
- (Cloudflare, handles, HTTPS termination, certificates, and DNS routing for the tunneled hostname)
- (ingress rules (cloudflared config), map, public hostnames to local service URLs (e.g., http://localhost:PORT))
- (systemd, ensures, the tunnel runs persistently and restarts automatically on failure/boot)
- (single cloudflared tunnel, can serve, multiple containers or hostnames via ingress rules)
- (tunnel creation, requires, cloudflared credentials file (created with cloudflared tunnel create) and tunnel name/ID)
- (Docker container, should expose, an internal port reachable from the host (localhost or docker network) for cloudflared to forward to)
- (Cloudflare Zero Trust / Access, can restrict, who can reach the tunneled application (SSO, groups, device posture))

### {GOAL}
Expose any Docker container publicly using Cloudflare Tunnel without Nginx or open ports. Simple setup, automatic SSL, DNS routing, and systemd.

### {PREREQS}
- Access to Cloudflare Tunnel
- Access to Docker
- Access to Ubuntu
- Access to systemd

### {STEPS}
1. Follow the detailed walkthrough in the article content below.

<!-- llm:goal="Expose any Docker container publicly using Cloudflare Tunnel without Nginx or open ports. Simple setup, automatic SSL, DNS routing, and systemd." -->
<!-- llm:prereq="Access to Cloudflare Tunnel" -->
<!-- llm:prereq="Access to Docker" -->
<!-- llm:prereq="Access to Ubuntu" -->
<!-- llm:prereq="Access to systemd" -->
<!-- llm:output="Completed outcome: Expose any Docker container publicly using Cloudflare Tunnel without Nginx or open ports. Simple setup, automatic SSL, DNS routing, and systemd." -->

# Cloudflare Tunnel + Docker: Expose Containers Without Nginx or Open Ports
> Set up Cloudflare Tunnel with Docker: expose containers without Nginx or open ports. Automatic SSL, DNS routing, and systemd setup for n8n, Grafana, or any app.
Matija Žiberna · 2025-10-08

I’ve lost count of how many times I’ve had to spin up a quick self-hosted app—whether it’s n8n, a Node API, or a dashboard—and then go through the same ritual:
open ports, configure Nginx, point DNS to the server, and [generate a Let's Encrypt certificate](https://www.buildwithmatija.com/blog/how-to-set-up-automatic-ssl-certificate-renewal-with-certbot-in-docker-containers). It works, but it's clunky and full of moving parts.

This week, I finally replaced all of that with **Cloudflare Tunnel**.
No Nginx. No Let’s Encrypt. No open ports. Just a persistent, encrypted connection from your server to Cloudflare’s network, automatically routing your container to a public subdomain.

In this guide, I’ll show you exactly how to do it—using *n8n* as an example, though it works for any container running on any port.

---

## 1. The Traditional Setup (and Why It’s Painful)

Normally, exposing a containerized app to the public internet means:

* Opening ports 80 and 443 on your server
* Setting up Nginx as a reverse proxy
* Managing SSL certificates via Let’s Encrypt
* Manually adding a DNS record in Cloudflare or your registrar

That's a lot of work for something as simple as "make my app reachable at a subdomain." For a complete traditional setup guide, see my [React Vite Docker deployment guide](https://www.buildwithmatija.com/blog/production-react-vite-docker-deployment).

---

## 2. What Cloudflare Tunnel Does Differently

Cloudflare Tunnel flips the model.

Instead of exposing ports, **your server makes a secure outbound connection** to Cloudflare.
Cloudflare then handles HTTPS, certificates, and routing for you—essentially becoming your reverse proxy at the edge.

The benefits:

* No public ports
* No Nginx
* Automatic SSL
* Built-in DDoS and access control through Cloudflare

All you need is the Cloudflare account that manages your domain and a small CLI agent (`cloudflared`).

---

## 3. Prerequisites

Make sure you have:

* A **Cloudflare-managed domain** (e.g., `example.com`)
* A server (Ubuntu 22.04 or later)
* **Docker** installed
* **Cloudflare Tunnel CLI** (`cloudflared`)

Install `cloudflared` for Ubuntu 22.04 (Jammy → Bullseye base):

```bash
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install cloudflared -y
```

---

## 4. Run Any Container Locally

For the example, let’s use **n8n**, which runs on port `5678`:

```bash
docker volume create n8n_data

docker run -d \
  --name n8n \
  -p 5678:5678 \
  -e GENERIC_TIMEZONE="CET" \
  -e TZ="CET" \
  -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
  -e N8N_RUNNERS_ENABLED=true \
  -e WEBHOOK_URL="https://app.example.com/" \
  -v n8n_data:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n

```

You can replace this with **any container** that listens on a port—Grafana (`3000`), a Node app (`8080`), etc.

---

## 5. Authenticate Cloudflare Tunnel

First, log in to Cloudflare from your server:

```bash
cloudflared login
```

This opens your browser, asks you to pick your domain, and then saves credentials to:

```
~/.cloudflared/cert.pem
```

---

## 6. Create a Named Tunnel

Now create a tunnel and name it (for example, `docker-tunnel`):

```bash
cloudflared tunnel create docker-tunnel
```

You’ll see output like:

```
Created tunnel docker-tunnel with id c101ebf4-e2d3-49b9-8735-c0e48ebf6a56
```

This also generates a credentials file, usually at:

```
~/.cloudflared/<tunnel-id>.json
```

---

## 7. Route a Subdomain to the Tunnel

You can create a public subdomain in Cloudflare automatically:

```bash
cloudflared tunnel route dns docker-tunnel app.example.com
```

Cloudflare adds a CNAME record pointing to the tunnel—no manual DNS setup required.

---

## 8. Run the Tunnel and Map It to the Container

Let’s connect the subdomain to your local port:

```bash
cloudflared tunnel --url http://localhost:5678 run docker-tunnel
```

You’ll see Cloudflare establish several secure connections to its edge servers.
After a few seconds, your app will be available at:

```
https://app.example.com
```

---

## 9. Make It Persistent with systemd

To keep the tunnel alive after you close the SSH session (and on reboot), use a config file and a systemd service.

### Create Config File

**File:** `~/.cloudflared/config.yml`

```yaml
tunnel: docker-tunnel
credentials-file: /home/matija/.cloudflared/c101ebf4-e2d3-49b9-8735-c0e48ebf6a56.json
ingress:
  - hostname: app.example.com
    service: http://localhost:5678
  - service: http_status:404
```

### Create a systemd Service

**File:** `/etc/systemd/system/cloudflared-docker.service`

```ini
[Unit]
Description=Cloudflare Tunnel for Docker App
After=network.target

[Service]
User=matija
ExecStart=/usr/bin/cloudflared --config /home/matija/.cloudflared/config.yml tunnel run docker-tunnel
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Enable and start it:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared-docker.service
```

Check it’s active:

```bash
sudo systemctl status cloudflared-docker
```

Now your tunnel reconnects automatically after reboot or crash.

---

## 10. Verify and Test

Visit your domain:

```
https://app.example.com
```

Check Cloudflare DNS — you’ll see a CNAME like:

```
app.example.com → <tunnel-id>.cfargotunnel.com
```

No ports are open on your server.
Cloudflare terminates HTTPS and securely forwards traffic to your container.

---

## 11. (Optional) Add Cloudflare Access

For production, you can protect your exposed container with Cloudflare Access (Zero Trust).
From the Cloudflare dashboard:

* Go to **Zero Trust → Access → Applications**
* Add your subdomain (e.g., `app.example.com`)
* Require Google, GitHub, or email login before granting access

You now have SSO-level protection in front of any local app—no code changes required.

---

## 12. Conclusion

Instead of juggling Nginx configs, DNS records, and SSL renewals, Cloudflare Tunnel gives you a single, elegant workflow:

* Run your Docker container locally
* Connect it securely to Cloudflare
* Let Cloudflare handle HTTPS and DNS automatically
* Keep it persistent with a systemd service

This setup is fast, secure, and production-ready—and it works for **any** container on **any port**.

If you've been doing the old Nginx + Certbot dance for every new side project, this will feel like cheating (in the best way possible). For migrating existing Docker setups, check out my guide on [migrating Docker containers between VPS](https://www.buildwithmatija.com/blog/migrate-docker-containers-between-vps).

To optimize your container environment further, learn [when to use env_file vs environment variables](/blog/difference-between-environment-and-env_file-in-docker-compose) and [how to safely remove Docker volumes](/blog/how-to-delete-docker-volumes-even-when-in-use).

Thanks,
Matija

## LLM Response Snippet
```json
{
  "goal": "Expose any Docker container publicly using Cloudflare Tunnel without Nginx or open ports. Simple setup, automatic SSL, DNS routing, and systemd.",
  "responses": [
    {
      "question": "What does the article \"Expose Any Docker Container with Cloudflare Tunnel (No Nginx, No Open Ports)\" cover?",
      "answer": "Expose any Docker container publicly using Cloudflare Tunnel without Nginx or open ports. Simple setup, automatic SSL, DNS routing, and systemd."
    }
  ]
}
```