> ## Content Index
> Fetch the complete content index at: https://blog.ifirestone.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Exponer entornos de desarrollo con Cloudflare Tunnel y WARP sin abrir puertos
- URL: https://blog.ifirestone.dev/exponer-entornos-de-desarrollo-con-cloudflare-tunnel-y-warp-sin-abrir-puertos/
- Published: 2026-10-23T17:15:00.000Z
- Updated: 2026-10-23T17:15:00.000Z
- Description: Cómo publicar apps de desarrollo con TLS y dominios propios sin exponer el servidor: Cloudflare Tunnel, Traefik, Portainer, Laravel detrás de un proxy y acceso privado con WARP. Con sus costos.
- Author: Ivan Firestone
- Tags: devops, docker, security, #spanish, cloudflare

Necesitaba un entorno de desarrollo con **despliegues frecuentes**, URLs públicas con HTTPS y acceso cómodo a las bases de datos para depurar. Lo que no quería era un servidor en Internet con los puertos 80 y 443 abiertos, esperando a que alguien lo escanee.

Esos requisitos tiran en direcciones opuestas: mientras más fácil el acceso, mayor la exposición. Esta es la historia de la combinación que me funcionó:

- **Cloudflare Tunnel** para publicar las apps sin abrir los puertos 80 y 443.
- **Traefik** como enrutador interno, con labels por aplicación.
- **Portainer** para administrar los contenedores.
- **Cloudflare One WARP** para llegar a la red privada de Docker solo desde mi máquina.

Voy a explicar el diseño, el detalle que rompe Laravel detrás del proxy y, sobre todo, los riesgos.

## La idea: que el servidor llame hacia afuera

La forma habitual de publicar una app es abrir un puerto y dejar que el mundo se conecte. El túnel invierte eso:

```
Internet → Cloudflare (TLS) → túnel (conexión saliente) → Traefik → contenedores

```

La clave: el conector `cloudflared` **abre la conexión hacia Cloudflare**, y no al revés. El servidor no necesita puertos de entrada abiertos. Cloudflare termina el TLS, y dentro del servidor el tráfico va por HTTP hacia Traefik.

Eso resuelve la exposición y los certificados de un solo golpe. Pero un túnel solo lleva el tráfico hasta la puerta; algo tiene que decidir qué contenedor responde.

## Elegir el enrutador

Despliego con mucha frecuencia. Con Traefik, cada aplicación declara su enrutamiento con labels en su propio `docker-compose`, y no hay configuración central que tocar por cada proyecto nuevo.

Antes evalué Dokploy para desarrollo. Es cómodo, pero me pareció pesado en recursos para un servidor pequeño. El camino que elegí (Portainer + Traefik + cloudflared) es más liviano, al precio de **escribir a mano los labels de Traefik**. Es un intercambio entre comodidad y consumo.

## Armar las piezas

La base es una red compartida, Traefik sin HTTPS (de eso se encarga Cloudflare) y `cloudflared` con el token del túnel:

```yaml
services:
  traefik:
    image: traefik:v3
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks: [edge]

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN}
    networks: [edge]

networks:
  edge:
    name: edge

```

Fíjate en que **ningún servicio publica puertos** (`ports:`). Es intencional y es la mejor defensa: si no se publica ningún puerto, no hay nada que puedas exponer por accidente.

En el panel de Cloudflare configuras el *public hostname* (por ejemplo, un comodín bajo tu dominio) apuntando al servicio `http://traefik:80`.

A partir de ahí, cada app nueva solo se une a la misma red y declara su enrutamiento:

```yaml
services:
  myapp:
    image: myapp:dev
    networks: [edge]
    labels:
      - traefik.enable=true
      - traefik.http.routers.myapp.rule=Host(`myapp.dev.example.com`)
      - traefik.http.routers.myapp.entrypoints=web
      - traefik.http.services.myapp.loadbalancer.server.port=80

networks:
  edge:
    external: true

```

Con eso, una app queda en vivo por HTTPS. Pero si es una app Laravel, hay un detalle.

## El detalle que rompe Laravel

Como el TLS termina en Cloudflare y Traefik habla HTTP con la app, Laravel ve peticiones HTTP. Los síntomas típicos: URLs generadas con `http://`, redirecciones rotas y *mixed content* (contenido mixto).

Se arregla confiando en los headers `X-Forwarded-*` del proxy y forzando HTTPS. En Laravel 11 o superior, en `bootstrap/app.php`:

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(at: '*');
})

```

Y en el service provider, forzar el esquema:

```php
URL::forceScheme('https');

```

Funciona. Pero mira bien ese `'*'`, porque trae un riesgo.

### La advertencia importante

`trustProxies(at: '*')` significa "confío en los headers de **cualquiera**". Solo es seguro **si el origen es inalcanzable salvo a través del túnel**. Si alguien puede llegar directo al contenedor, puede falsificar `X-Forwarded-For` o `X-Forwarded-Proto`.

Por eso conviene combinar tres cosas:

1. No publicar los puertos de las apps.
2. Cerrar 80/443 en el firewall.
3. Limitar en Traefik qué IPs son de confianza para los headers reenviados (`forwardedHeaders.trustedIPs` en el *entrypoint*), usando la red interna del túnel.

Lo que lleva a la pregunta de la que depende todo el diseño: ¿está realmente cerrado el origen?

## Cerrar los puertos de verdad

Hay capas, de la más a la menos confiable:

1. **No publiques puertos en Docker.** Sin `ports:`, no hay nada que filtrar.
2. **Firewall del proveedor de nube** (reglas de seguridad de red): permite solo lo estrictamente necesario, por ejemplo SSH desde tu IP.
3. **Reglas en el host** como defensa adicional. Ojo: Docker manipula `iptables` por su cuenta y puede saltarse reglas pensadas para firewalls de nivel superior; para filtrar el tráfico hacia contenedores publicados, la cadena `DOCKER-USER` es el lugar previsto.

Verifica siempre **desde afuera** con un escaneo de puertos, no solo leyendo la configuración.

## La última pieza: llegar a las bases de datos

Las apps eran públicas y los puertos estaban cerrados. Pero para depurar yo seguía queriendo conectarme a las bases de datos del entorno, sin exponerlas a nadie.

Ahí entra **Cloudflare One WARP**. Con una ruta de red privada a través del mismo túnel, mi máquina llega a la red de Docker como si estuviera dentro. Esto permite:

- Darles a las bases de datos una **IP fija** en la red interna y conectarme con mi cliente de siempre.
- Que solo los **dispositivos autorizados** en la política de Zero Trust puedan acceder.
- No abrir ningún puerto de base de datos hacia afuera.

## Producción es otra decisión

¿Se traslada la misma configuración a producción? No del todo. En producción uso Portainer **sin Traefik**: el túnel apunta directamente al contenedor de cada aplicación. Menos piezas, menos superficie y menos cosas que pueden fallar. Traefik me resuelve un problema de **velocidad de despliegue** en desarrollo; en producción, donde los cambios son menos frecuentes, ese problema no existe.

## Lo que cedes

Nada de esto es gratis:

- **Dependencia de Cloudflare.** Si el servicio tiene una caída, tus entornos quedan inalcanzables. Para desarrollo es tolerable; para producción hay que evaluarlo.
- **El token del túnel es un secreto.** Quien lo tenga puede ejecutar un conector. Guárdalo como secreto, no en el repositorio.
- **El socket de Docker.** Traefik lo monta en solo lectura, pero sigue siendo un acceso sensible. Hay alternativas con un proxy de socket que limita las operaciones permitidas.
- **Labels escritos a mano.** Es más trabajo por proyecto que un panel integrado.
- **Mantenimiento de WARP.** Hay que gestionar el enrolamiento de dispositivos y las políticas de acceso.
- **Versiones fijadas.** Evita `latest` para `cloudflared` y Traefik si quieres despliegues reproducibles.

## Lo que me llevo

El servidor que quería mantener oculto está oculto: nada escucha en Internet público, y aun así despliego con frecuencia con HTTPS y llego a las bases de datos desde mi propia máquina.

Si te quedas con pocas cosas:

- Un túnel saliente elimina la necesidad de abrir puertos y simplifica el TLS.
- No publicar puertos en Docker es la defensa más fuerte y más barata.
- `trustProxies(at: '*')` solo es seguro si el origen es inalcanzable por cualquier otra ruta.
- Traefik acelera el desarrollo; en producción, menos piezas suelen ser mejor.
- Verifica la exposición desde afuera, no solo en la configuración.