SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-24

Traefik en Docker Compose: 5 apps con una IP

Configura Traefik v3 para publicar cinco apps con una sola IP: reglas Host, TLS automático de Let's Encrypt y el error por permisos de acme.json que impide iniciar.

Una IP, cinco aplicaciones, un puerto 443

Tu VPS tiene una única dirección IPv4 pública y un único puerto TCP 443. Quieres alojar Gitea, una copia de staging de tu aplicación, un panel interno, una página de estado y un receptor de webhooks: cinco nombres de host en un solo servidor. Un reverse proxy es el proceso que controla :80 y :443, lee la cabecera Host de cada petición y la entrega al contenedor correcto. Traefik hace eso y obtiene y renueva un certificado para cada nombre de host sin que tengas que ejecutar certbot manualmente. Nginx y Caddy también terminarían TLS para esos cinco nombres de host sin problemas, así que, si aún no has elegido, conviene comparar los tres proxies según la gestión de certificados y el coste de configuración por aplicación antes de conectar todo a uno de ellos.

Lo que diferencia Traefik de un bloque server {} de nginx es el origen de su configuración. Con nginx editas un archivo y recargas el servicio, mientras que el ciclo de vida de los certificados sigue siendo una tarea independiente. Ese es el flujo que sigues cuando emites certificados de Let's Encrypt con certbot en nginx y el temporizador de renovación queda completamente fuera del servidor web. El proveedor Docker de Traefik supervisa el flujo de eventos de Docker y lee las labels de los contenedores: inicia un contenedor con una label de regla Host() y se puede enrutar hacia él en menos de un segundo; deténlo y la ruta desaparece. Ese también es el riesgo. La configuración almacenada en labels queda repartida en cinco lugares, y una label incorrecta no genera ningún aviso: el contenedor simplemente no recibe tráfico y Traefik no informa de nada.

Los cuatro conceptos

  • Entrypoints son sockets de escucha. Definirá dos: web en :80 y websecure en :443.
  • Routers comparan una solicitud (Host(...)) y la asocian con un servicio. Los certificados se solicitan por router mediante tls.certresolver.
  • Services son el backend: un contenedor y el puerto en el que escucha dentro de la red Docker.
  • Middlewares se sitúan entre el router y el servicio: autenticación básica, listas de IP permitidas, reescrituras de cabeceras y redirecciones.

Esos cuatro conceptos son los nombres que usa Traefik para tareas que, de otro modo, tendría que escribir manualmente: el router es un server_name, el servicio es un destino proxy_pass y los middlewares son las directivas de cabeceras y autenticación que configuraría al crear línea por línea un bloque server de proxy inverso de nginx.

La configuración estática (entrypoints, providers y ACME) se pasa en la línea de comandos de Traefik o en traefik.yml. Para cambiarla, debe reiniciar Traefik. La configuración dinámica (routers, services y middlewares) llega mediante etiquetas de los contenedores y se recarga automáticamente. Confundir ambas configuraciones es la causa habitual de que «mi flag no haga nada».

El archivo de Compose

Una red de Docker compartida llamada proxy es la base. Traefik llega a un contenedor sólo si ambos están conectados a ella.

name: edge

networks:
  proxy:
    name: proxy

services:
  traefik:
    image: traefik:v3.5
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=proxy
      - --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=you@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      # while you iterate, point at staging so a mistake costs nothing:
      # - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      - --api.dashboard=true
      - --log.level=INFO
      - --accesslog=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS

  gitea:
    image: gitea/gitea:1  # major-only pin keeps this demo copy-pasteable; pin an exact release in production
    restart: unless-stopped
    volumes:
      - ./gitea:/data
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.gitea.rule=Host(`git.example.com`)
      - traefik.http.routers.gitea.entrypoints=websecure
      - traefik.http.routers.gitea.tls.certresolver=le
      - traefik.http.services.gitea.loadbalancer.server.port=3000

docker compose up -d y después docker compose logs -f traefik. Cada aplicación adicional es una copia del bloque gitea, con su propio nombre de router, su propio Host() y su propio puerto interno. Una instalación de Nextcloud ejecutada en Docker con TLS y copias de seguridad se integra del mismo modo: elimine sus puertos publicados, conéctela a proxy y deje que las etiquetas del router gestionen el nombre de host y el certificado.

Hay cinco detalles importantes.

exposedByDefault=false hace que un contenedor sea invisible para Traefik hasta que tenga traefik.enable=true. Si se omite, cada contenedor que inicie, incluido el contenedor temporal postgres que ejecutó para comprobar algo, obtiene una ruta.

providers.docker.network=proxy indica a Traefik qué red debe usar cuando un contenedor está conectado a varias. Si se omite, Traefik puede elegir la IP de contenedor incorrecta. El resultado es un error 502 que parece un fallo de la aplicación.

loadbalancer.server.port=3000 es el puerto dentro del contenedor. Gitea escucha allí en el puerto 3000. Observe que ningún contenedor de aplicación publica un puerto. Sólo Traefik lo hace.

La redirección del punto de entrada web convierte las solicitudes sin cifrar en una respuesta 308 hacia HTTPS. El puerto 80 permanece abierto de todos modos: el desafío HTTP de ACME lo necesita, y también los usuarios que escriben un nombre de host sin especificar el esquema.

El $$ duplicado en el hash de basic-auth es un escape de Compose, no un error tipográfico. Genérelo con htpasswd -nbB admin 'your-password' (paquete apache2-utils) y duplique después cada $.

El certificado y la trampa de acme.json

tlschallenge=true selecciona TLS-ALPN-01: Let's Encrypt se conecta a tu servidor por 443 y Traefik responde al desafío durante el handshake TLS. La alternativa es HTTP-01, en el puerto 80. Sustituye la línea tlschallenge de la lista command: de Traefik por estas dos:

      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

Ambas opciones funcionan. Las dos requieren que el DNS público del nombre de host ya apunte a tu VPS y que la autoridad certificadora pueda resolver el nombre y conectarse desde el exterior. Crea primero el registro A (y AAAA), confírmalo con dig +short git.example.com y después inicia Traefik.

Ahora, la trampa que puede hacerte perder una tarde. Traefik guarda la clave de cuenta de ACME y todos los certificados emitidos en un solo acme.json. Si ese archivo puede leerlo el grupo o cualquier usuario, Traefik muestra una línea muy similar a esta y se detiene:

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

La solución correcta es la indicada arriba: monta el directorio mediante un bind mount y deja que Traefik cree el archivo con los permisos adecuados. Si creaste acme.json con touch, tu umask hizo que tuviera permisos 644. Corrígelo en el host:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

Incluye ese directorio en las copias de seguridad junto con los volúmenes de tu aplicación. Perderlo se puede resolver y los certificados se vuelven a emitir, pero volver a emitir cinco nombres de host a la vez puede alcanzar los límites de tasa.

Usa la CA de staging mientras haces pruebas. Quita el comentario de la línea caserver, comprueba que todas las rutas funcionen, vuelve a comentarla y elimina acme.json para solicitar certificados de producción nuevos. La instancia de producción de Let's Encrypt permite cinco certificados duplicados por semana para un conjunto idéntico de nombres de host y limita las validaciones fallidas repetidas para el mismo nombre. Staging emite certificados no confiables, el navegador muestra una advertencia y esa advertencia confirma que funciona, con límites mucho más amplios.

El dashboard es una interfaz de control, no una demostración

La mayoría de las guías de inicio rápido establecen --api.insecure=true, que sirve el dashboard en el puerto 8080 sin autenticación. En un servidor con una IP pública, esto expone la topología de enrutamiento, los nombres de host, los nombres del middleware y los puertos de los backends a cualquiera que lo explore.

Las etiquetas del servicio traefik anterior son la alternativa: el dashboard se enruta como cualquier otra aplicación, con un nombre de host real, mediante TLS y detrás de basicauth. service=api@internal conecta el router con la API integrada de Traefik. Puede reforzarlo encadenando una lista de IP permitidas, aplicada de izquierda a derecha. Si la dirección de la oficina es dinámica, establezca el rango en la subred asignada por una VPN WireGuard que aloje usted mismo en el mismo VPS y acceda al dashboard únicamente mediante el túnel:

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

Una sola contraseña compartida en basicauth deja de ser una opción defendible cuando cinco aplicaciones necesitan sus propias cuentas. La misma posición de middleware acepta un forwardauth que delega la decisión en Authentik, un servidor de inicio de sesión único autoalojado. Así, el dashboard y todas las rutas asociadas quedan protegidos por un único inicio de sesión que puede revocar desde un solo lugar.

El socket de Docker tiene privilegios de root

/var/run/docker.sock es una API que puede crear un contenedor con un montaje de / desde el host. El acceso a ella equivale a tener privilegios de root en la máquina, y Traefik la necesita para leer las etiquetas.

Mantenga :ro en el montaje, pero explique claramente qué proporciona: hace que el archivo de socket sea de solo lectura. No impide que las solicitudes POST a la API de Docker pasen por él. La mitigación real consiste en no entregar nunca el socket a Traefik y colocar un proxy de filtrado entre ambos:

  dockerproxy:
    image: tecnativa/docker-socket-proxy   # pin the current tag
    restart: unless-stopped
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy

Elimine el volumen del socket de Traefik y apunte el proveedor al proxy:

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik conserva el acceso de lectura a los contenedores y las redes, pero pierde la capacidad de crear cualquier recurso.

Firewall, puertos y la regla que casi todos aplican mal

Dos puertos abiertos, además de SSH:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Los puertos publicados por Docker omiten ufw. Docker inserta sus propias reglas de iptables, que se evalúan antes que las cadenas de ufw. Por eso, un contenedor iniciado con ports: ["3000:3000"] es accesible desde Internet aunque exista una regla ufw deny en ese mismo punto. La defensa es estructural, no una cuestión de configuración del firewall: publique puertos sólo desde Traefik y asigne a todos los demás contenedores networks: [proxy], sin nada más. Si algo necesita acceder realmente al host, enlácelo a loopback, "127.0.0.1:3000:3000".

Solución de problemas: errores que verá realmente

404 page not found, servido por Traefik. Ningún router coincidió. En orden de probabilidad: al contenedor le falta traefik.enable=true (con exposedByDefault=false configurado); la regla Host() no coincide con el nombre que escribió; el nombre del router de una etiqueta no coincide con el de otra (routers.gitea.rule y routers.gitea.entrypoints deben ser la misma palabra); o incluyó el nombre de host entre comillas en lugar de usar backticks. Traefik v3 requiere backticks dentro de las expresiones de coincidencia.

502 Bad Gateway. Un router coincidió, pero no se pudo acceder al backend. Casi siempre, el contenedor no está conectado a la red proxy; compruebe docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. La otra posibilidad es un loadbalancer.server.port incorrecto: indicó un puerto publicado o la aplicación escucha en otro puerto. El registro muestra el intento: dial tcp 172.18.0.5:8080: connect: connection refused.

El navegador muestra una advertencia y el certificado se emitió para TRAEFIK DEFAULT CERT. No existe ningún certificado para ese nombre de host y Traefik sirvió su certificado autofirmado provisional. Lea las líneas de ACME:

unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.com

El DNS todavía no apunta al servidor. Corrija el registro, espere a que transcurra el TTL y reinicie Traefik.

Invalid response from http://git.example.com/.well-known/acme-challenge/... en el desafío HTTP: el puerto 80 no llega a Traefik desde el exterior. Normalmente, el problema está en un firewall del proveedor situado delante del VPS, no en ufw.

Los certificados nunca se emiten y su DNS está en Cloudflare con la nube naranja activada. Cloudflare termina TLS en su red perimetral y TLS-ALPN-01 no puede completarse a través de ella. Configure el registro como DNS-only durante la emisión o cambie al desafío DNS-01 con un token de API. DNS-01 también es el único desafío que permite emitir certificados comodín.

Bucle de redirección. Algo situado delante de Traefik ya termina TLS y reenvía tráfico sin cifrar al puerto :80; la redirección del entrypoint lo devuelve a HTTPS. Elimine una de las dos redirecciones.

Mantenerlo en funcionamiento

La unidad de Docker debe estar habilitada para arrancar con el sistema (systemctl is-enabled docker), y restart: unless-stopped vuelve a iniciar la pila después de un reinicio. Para obtener un control explícito, una unidad pequeña de systemd que ejecute docker compose -f /srv/edge/compose.yml up -d con RemainAfterExit=yes proporciona systemctl status edge y control del orden de inicio.

Fije el tag de Traefik (traefik:v3.5), nunca latest. La actualización de v2 a v3 cambió la sintaxis de las reglas y los nombres de los proveedores. Un latest no supervisado volverá a cargar sin problemas una configuración que ya no puede interpretar. Actualice de forma deliberada: lea las notas de migración, cambie el tag, docker compose up -d traefik y monitorice el registro. Si todavía usa un tag de v2, la guía de migración de Traefik de v2 a v3 explica cada cambio de nombre, el modo de compatibilidad y un rollback que conserva sus certificados.

Haga una copia de seguridad de ./letsencrypt y del volumen de datos de cada aplicación. Traefik no conserva ningún otro estado que no pueda reconstruirse a partir del archivo compose.

Qué falla al escalar

El primer límite no es el rendimiento, sino el servidor único: un Traefik en un solo VPS es un único punto de fallo para cinco aplicaciones, y acme.json es almacenamiento en un archivo plano; dos instancias de Traefik que escriban en él lo dañarán. Escalar horizontalmente implica trasladar el almacenamiento de certificados fuera de un archivo o terminar TLS en otro lugar.

El segundo problema son las conexiones de larga duración. Los eventos enviados por el servidor, las cargas de archivos grandes y los clientes lentos alcanzan los tiempos de espera de respuesta del entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout y sus equivalentes writeTimeout y idleTimeout son los parámetros que se deben ajustar. WebSockets atraviesa el proxy sin configuración adicional.

El tercer problema es el disco. --accesslog=true escribe en stdout, y el controlador json-file de Docker conserva esos datos indefinidamente si no se establece un límite. Configure logging.options.max-size en el servicio de Traefik o escriba el registro de acceso en un archivo y rótelo.

Nada de esto requiere un orquestador. Sí requiere un servidor bajo su control, con una IP pública real y los puertos 80 y 443 abiertos a Internet. Un único VPS pequeño es toda la lista de dependencias.

FAQ

¿Sigo necesitando certbot si ejecuto Traefik?

No. El resolvedor ACME de Traefik solicita y renueva el certificado para cada nombre de host que encamina, y almacena todos los certificados en acme.json. Certbot sigue siendo la herramienta adecuada cuando nginx u otro servidor termina TLS directamente; ejecutar ambos para los mismos nombres de host sólo consume los límites de frecuencia de Let's Encrypt.

¿Por qué mi contenedor devuelve 404 a través de Traefik?

Un 404 servido por Traefik significa que ningún router coincidió con la petición. Compruebe que el contenedor incluya traefik.enable=true (es obligatorio una vez establecido exposedByDefault=false), que el valor de Host() coincida con el nombre que ha escrito y que el nombre del router sea idéntico en todas las etiquetas de esa aplicación. Traefik v3 también requiere usar comillas invertidas dentro del matcher, no comillas normales.

¿Cuál es aquí la diferencia entre un 404 y un 502?

Un 404 significa que el encaminamiento no llegó a producirse; un 502 significa que un router coincidió, pero el backend rechazó la conexión. Las causas habituales de un 502 son que un contenedor no esté conectado a la red proxy y que loadbalancer.server.port apunte a un puerto publicado en lugar de al puerto en el que la aplicación escucha dentro del contenedor. El registro de acceso indica la dirección exacta a la que Traefik intentó conectarse.

¿Es suficiente montar el socket de Docker en modo de sólo lectura?

La opción :ro hace que el archivo del socket sea de sólo lectura, pero no limita la API que hay detrás; las peticiones POST siguen pasando por él y el acceso a la API de Docker equivale a tener acceso root en el host. La configuración más segura es el contenedor docker-socket-proxy mostrado arriba, que expone a Traefik únicamente operaciones de lectura sobre contenedores y redes, y bloquea directamente las operaciones de escritura.

¿Puede Traefik emitir un certificado comodín?

Sólo mediante el desafío DNS-01, con un token de API de su proveedor de DNS. TLS-ALPN-01 y HTTP-01 validan un único nombre de host y no pueden generar un certificado comodín. DNS-01 también es la opción adecuada cuando una CDN como Cloudflare termina TLS delante de su VPS y los otros dos desafíos nunca se completan.