Esse parece ser um tópico um tanto quanto... distópico? Talvez nem tanto. Mas todas as referências sobre o assunto na Internet estão incompletos e geralmente são pra versões mais antigas do traefik. Traefik é um roteador de conexões que funciona tanto como programa chamado por systemd (ou outro sistema de init se for um BSD), ou por container com algo como docker-compose, ou ainda diretamente em kubernetes. E não só funciona como proxy-reverse: pode também atuar como um servidor http pra suas conexões. E não somente tcp como udp. E escrito em Go!

Eu precisei colocar um servidor ssh atrás de um traefik. E deu um certo trabalho. Pra testar e mostrar aqui também, usei docker-compose pro serviço. Isso facilitou a configuração. Pra backend com ssh, subi uma instância do gitea com o ssh na porta 2222. Usei também certificados letsencrypt nas portas https do serviço.

Como um plus, ainda adicionei um allowlist pra acessar o serviço somente de certos IPs.

Vamos pra configuração então. Primeiramente do traefik. Pra subir, eu configurei pra ouvir nas portas 80 (http), 443 (https), 8080 (traefik dashboard) e 2222 (ssh). O conteúdo de traefik/docker-compose.yml:

  
services:
  traefik:
    image: "traefik:v3.3"
    container_name: "traefik"
    restart: unless-stopped
    environment:
      - TZ=Europe/Stockholm
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
      - "2222:2222"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./traefik.yml:/traefik.yml:ro"
    networks:
      - traefik

networks:
  traefik:
    name: traefik    
  

Junto com essa configuração de container, ainda inclui a configuração do serviço em traefik/traefik.yml:

  
api:
  dashboard: true
  insecure: true
  debug: true
entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"
  ssh:
    address: ":2222"
serversTransport:
  insecureSkipVerify: true
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik
certificatesResolvers:
  letsencrypt:
    acme:
      email: This email address is being protected from spambots. You need JavaScript enabled to view it.
      storage: /letsencrypt/acme.json
      httpChallenge:
        # used during the challenge
        entryPoint: web

log:
  level: WARN
  

Aqui é possível ver que defino novamente os entrypoints pra 80, 443 e 2222. E no 80 (http) eu redireciono pro 443 (https). E certificado do letsencrypt.

Tudo pronto no lado do traefik. Agora é o ponto de subir o serviço e preparar algumas coisas pra testar.

  
# docker compose -p traefik -f traefik/docker-compose.yml up -d
[+] Running 1/1
 ✔ Container traefik  Started                                0.4s
  

Dentro do diretório traefik será criado um diretório letsencrypt e dentro dele terá o arquivo acme.json com seus certificados.

Primeiramente vamos subir um container de teste pra ver se tudo funciona. O próprio projeto traefik fornece um container chamado whoami pra isso. O conteúdo de whois/docker-compose.yml:

  
services:
  whoami:
    container_name: simple-service
    image: traefik/whoami
    labels:
        - "traefik.enable=true"
        - "traefik.http.routers.whoami.rule=Host(`whoami.tests.loureiro.eng.br`)"
        - "traefik.http.routers.whoami.entrypoints=websecure"
        - "traefik.http.routers.whoami.tls=true"
        - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
        - "traefik.http.services.whoami.loadbalancer.server.port=80"
    networks:
        - traefik
networks:
  traefik:
    name: traefik    
  

E habilitando o serviço com docker-compose:

  
# docker compose -p whoami -f whoami/docker-compose.yml up -d
WARN[0000] a network with name traefik exists but was not created for project "whoami".
Set `external: true` to use an existing network 
[+] Running 1/1
 ✔ Container simple-service  Started                                              0.3s 
  

E um simples teste do serviço:

  
❯ curl -I http://whoami.tests.loureiro.eng.br
HTTP/1.1 308 Permanent Redirect
Location: https://whoami.tests.loureiro.eng.br/
Date: Thu, 24 Apr 2025 12:43:55 GMT
Content-Length: 18

❯ curl  https://whoami.tests.loureiro.eng.br
Hostname: 0da540e1039c
IP: 127.0.0.1
IP: ::1
IP: 172.18.0.3
RemoteAddr: 172.18.0.2:52686
GET / HTTP/1.1
Host: whoami.tests.loureiro.eng.br
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 1.2.3.4
X-Forwarded-Host: whoami.tests.loureiro.eng.br
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: 4cacedb0129a
X-Real-Ip: 1.2.3.4
  

O primeiro curl pro endpoint na porta 80 (http) recebe um redirect pra https. Perfeito! O segundo, os dados do container de forma transparente.

O serviço funciona! Mas pra http e https. Falta ssh na porta 2222, que está nessa porta pra não atrapalhar o uso do ssh normal do sistema.

Pra isso eu configurei um gitea, como descrevi no início do artigo. Seu docker-compose.yml é o seguinte:

  
    services:
  server:
    image: gitea/gitea:1.23-rootless #latest-rootless
    container_name: gitea
    environment:
     - GITEA__database__DB_TYPE=postgres
     - GITEA__database__HOST=db:5432
     - GITEA__database__NAME=gitea
     - GITEA__database__USER=gitea
     - GITEA__database__PASSWD=A4COU5a6JF5ZvWoSufi0L1aomSkzqww7s1wv039qy6o=
     - LOCAL_ROOT_URL=https://gitea.tests.loureiro.eng.br
     - GITEA__openid__ENABLE_OPENID_SIGNIN=false
     - GITEA__openid__ENABLE_OPENID_SIGNUP=false
     - GITEA__service__DISABLE_REGISTRATION=true
     - GITEA__service__SHOW_REGISTRATION_BUTTON=false
     - GITEA__server__SSH_DOMAIN=gitea.tests.loureiro.eng.br
     - GITEA__server__START_SSH_SERVER=true
     - GITEA__server__DISABLE_SSH=false    
    restart: always
    volumes:
      - gitea-data:/var/lib/gitea
      - gitea-config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    #ports:
    #  - "127.0.0.1:3000:3000"
    #  - "127.0.0.1:2222:2222"
    depends_on:
      - db
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.giteaweb.rule=Host(`gitea.tests.loureiro.eng.br`)"
      - "traefik.http.routers.giteaweb.entrypoints=websecure"
      - "traefik.http.routers.giteaweb.tls=true"
      - "traefik.http.routers.giteaweb.tls.certresolver=letsencrypt"
      - "traefik.http.services.giteaweb.loadbalancer.server.port=3000"
      - "traefik.http.middlewares.giteaweb-ipwhitelist.ipallowlist.sourcerange=1.2.3.4, 4.5.6.7"
      - "traefik.http.routers.giteaweb.middlewares=giteaweb-ipwhitelist"
      - "traefik.tcp.routers.gitea-ssh.rule=HostSNI(`*`)"
      - "traefik.tcp.routers.gitea-ssh.entrypoints=ssh"
      - "traefik.tcp.routers.gitea-ssh.service=gitea-ssh-svc"
      - "traefik.tcp.services.gitea-ssh-svc.loadbalancer.server.port=2222"
      - "traefik.tcp.middlewares.giteassh-ipwhitelist.ipallowlist.sourcerange=1.2.3.4, 4.5.6.7"
      - "traefik.tcp.routers.gitea-ssh.middlewares=giteassh-ipwhitelist"

  db:
    image: postgres:17
    restart: always
    container_name: gitea_db
    environment:
      - POSTGRES_DB=gitea
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=A4COU5a6JF5ZvWoSufi0L1aomSkzqww7s1wv039qy6o=

    networks:
      - traefik
    volumes:
      - gitea-db:/var/lib/postgresql/data

networks:
  traefik:
    name: traefik

volumes:
  gitea-data:
  gitea-config:
  gitea-db:
  

Pode ser visto que existe uma regra pra parte web, que roda na porta 3000 do container, e pra parte de ssh, que fica na porta 22222. Eu deixei comentado a parte que exporta as portas pra mostrar claramente que isso não é necessário. Aliás não funciona se especificar as portas.

E finalmente rodando o serviço:

  
# docker compose -p gitea -f gitea/docker-compose.yml up -d
WARN[0000] a network with name traefik exists but was not created for project "gitea".
Set `external: true` to use an existing network 
[+] Running 2/2
 ✔ Container gitea_db  Started                   0.3s 
 ✔ Container gitea     Started                   1.0s     
  

E vamos ao testes.

  
❯ curl -s  https://gitea.tests.loureiro.eng.br | head -n 10
<!DOCTYPE html>
<html lang="en-US" data-theme="gitea-auto">
<head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Gitea: Git with a cup of tea</title>
        <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRoIGEgY3VwIG9mIHRlYSIsInNob3J0X25hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3RhcnRfdXJsIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwLyIsImljb25zIjpbeyJzcmMiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvYXNzZXRzL2ltZy9sb2dvLnBuZyIsInR5cGUiOiJpbWFnZS9wbmciLCJzaXplcyI6IjUxMng1MTIifSx7InNyYyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9hc3NldHMvaW1nL2xvZ28uc3ZnIiwidHlwZSI6ImltYWdlL3N2Zyt4bWwiLCJzaXplcyI6IjUxMng1MTIifV19">
        <meta name="author" content="Gitea - Git with a cup of tea">
        <meta name="description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">
        <meta name="keywords" content="go,git,self-hosted,gitea">
        <meta name="referrer" content="no-referrer">
❯ telnet gitea.tests.loureiro.eng.br 2222
Trying 1.2.3.4...
Connected to gitea.tests.loureiro.eng.br.
Escape character is '^]'.
SSH-2.0-Go

^]
telnet> q
Connection closed.    
  

E pronto! Temos o serviço funcionando e roteado pelo traefik. E podendo fazer ACL de ip do serviço sem precisar do firewall pra isso.

Boa diversão!

Nota: eu coloquei um básico da configuração do gitea pra exemplo. E não funciona se copiar e colar. Pra ter um serviço rodando, terá de adicionar mais algumas partes em environment.