Rodar docker run num app funciona para uma pessoa. Mas e quando cinquenta usuários acessam ao mesmo tempo, cada um precisando de sua própria sessão isolada, com login e sem que um trave o outro? É aqui que entra o ShinyProxy: uma plataforma open-source que dá, a cada usuário, seu próprio contêiner Docker do app, com autenticação, isolamento e escalabilidade — de graça. Este módulo fecha a trilha de deploy, transformando a imagem de implantação com Docker num serviço multiusuário de produção.

Implantar apps Shiny em produção com ShinyProxy: entender seu modelo de um-contêiner-por-sessão, configurar o application.yml (apps, imagens Docker, autenticação), integrar com a imagem golem+Docker dos módulos anteriores, e comparar com as alternativas de deploy (Shiny Server, Posit Connect).

O problema de escala do Shiny

Um processo Shiny é, essencialmente, single-threaded por sessão. Num servidor simples (Shiny Server open-source), todos os usuários compartilham o mesmo processo R por app, então:

  • Uma tarefa pesada de um usuário pode degradar a experiência dos outros.
  • Não há isolamento: um erro grave ou um vazamento de memória afeta todos.
  • Autenticação e gestão de recursos são limitadas.

O promises/future de programação em Shiny ameniza o bloqueio dentro de um processo, mas não dá isolamento real entre usuários. Para isso, a solução arquitetural é dar a cada usuário seu próprio processo isolado — e a forma mais limpa de isolar processos é… um contêiner por usuário. É exatamente o que o ShinyProxy faz.

O modelo do ShinyProxy: um contêiner por sessão

Quando um usuário abre o app pelo ShinyProxy, ele inicia um novo contêiner Docker daquele app só para aquela sessão. Quando o usuário sai, o contêiner é destruído.

Usuário A ──▶ ShinyProxy ──▶ Contêiner Docker A (app isolado)
Usuário B ──▶ ShinyProxy ──▶ Contêiner Docker B (app isolado)
Usuário C ──▶ ShinyProxy ──▶ Contêiner Docker C (app isolado)

As consequências, todas positivas para produção:

  • Isolamento total: o crash ou o consumo de memória de um usuário não afeta os demais — cada um está no seu contêiner.
  • Escalabilidade: dá para distribuir os contêineres por vários servidores (com Docker Swarm ou Kubernetes).
  • Autenticação integrada: LDAP, OpenID Connect, SAML, Keycloak — o login acontece no ShinyProxy, antes do app (a abordagem segura de notificações, validação e segurança).
  • Reprodutibilidade: usa a mesma imagem Docker de implantação com Docker, então "funciona no proxy" é garantido por construção.

Pré-requisitos e arquitetura

ShinyProxy é uma aplicação Java que conversa com o Docker (ou Kubernetes) para orquestrar os contêineres. A pilha mínima:

  • Docker instalado no servidor (o ShinyProxy chama a API dele).
  • Java (a JVM que roda o ShinyProxy) — geralmente via o próprio contêiner do ShinyProxy.
  • A imagem Docker do seu app (a que construímos em implantação com Docker), disponível localmente ou num registry.
  • Um arquivo application.yml que descreve tudo.

O coração da configuração: application.yml

Todo o comportamento do ShinyProxy vive num único YAML. Aqui está um exemplo comentado que serve dois apps:

proxy:
  title: Portal de Dashboards
  port: 8080                       # porta em que o ShinyProxy escuta

  # --- AUTENTICAÇÃO (aqui: usuários simples; em produção: LDAP/OIDC) ---
  authentication: simple
  admin-groups: administradores
  users:
    - name: ana
      password: senha_hash
      groups: analistas
    - name: chefe
      password: senha_hash
      groups: administradores

  # --- DOCKER: como o proxy fala com o Docker ---
  docker:
    internal-networking: true

  # --- OS APPS: cada 'specs' é um app, ligado a uma imagem Docker ---
  specs:
    - id: dashboard-vendas
      display-name: Dashboard de Vendas
      description: Vendas por região e período
      container-image: meu-dashboard:1.0        # a imagem construída com Docker!
      container-env:                             # variáveis de ambiente (Aula 5/9)
        GOLEM_CONFIG_ACTIVE: production
      access-groups: [analistas, administradores] # quem pode ver este app

    - id: dashboard-rh
      display-name: Dashboard de RH
      container-image: dashboard-rh:2.1
      access-groups: [administradores]            # só admins

# porta interna que o app expõe DENTRO do contêiner (o EXPOSE do Dockerfile)
spring:
  application:
    name: ShinyProxy

Os campos que você mais ajusta:

  • container-image — liga o app à imagem Docker construída em implantação com Docker. Esse é o elo entre os dois módulos.
  • access-groups — a autorização, tema de notificações, validação e segurança: quais grupos de usuários enxergam cada app. Autenticação diz quem é; access-groups diz o que pode ver.
  • container-env — injeta as variáveis de ambiente (config e segredos) no contêiner de cada sessão, exatamente como o -e do docker run.
  • authentication — simple para testes; em produção, ldap, openid, saml ou keycloak.

A porta interna do app precisa bater

Um ponto que causa muita confusão: o ShinyProxy precisa saber em que porta dentro do contêiner o app escuta. Em implantação com Docker, fixamos shiny.port = 3838 e EXPOSE 3838. O ShinyProxy assume 3838 por padrão para apps Shiny; se você usou outra porta, precisa declará-la:

    - id: dashboard-vendas
      container-image: meu-dashboard:1.0
      port: 3838                 # a porta que o app escuta DENTRO do contêiner

A cadeia completa de portas: o usuário acessa a porta do ShinyProxy (8080) → o ShinyProxy encaminha para a porta interna do contêiner da sessão (3838) → dentro do contêiner, o app escuta em 0.0.0.0:3838 (por isso o shiny.host='0.0.0.0' do Dockerfile era essencial). Se qualquer elo dessa cadeia não bater, o app "não carrega".

Rodando o ShinyProxy

O jeito mais limpo é rodar o próprio ShinyProxy como um contêiner Docker, dando a ele acesso ao Docker do host (para que possa criar os contêineres das sessões):

docker run -d \
  -p 8080:8080 \
  -v /var/run/docker.sock:/var/run/docker.sock \   # acesso ao Docker do host
  -v $(pwd)/application.yml:/opt/shinyproxy/application.yml \  # a config
  openanalytics/shinyproxy:latest
# Acesse o portal em http://localhost:8080

O volume /var/run/docker.sock é o que permite ao ShinyProxy pedir ao Docker do host que crie e destrua os contêineres de cada sessão. Sem ele, o ShinyProxy não consegue orquestrar nada.

Escalando com Kubernetes

Para produção de larga escala — centenas de usuários, alta disponibilidade — o ShinyProxy roda sobre Kubernetes em vez do Docker de uma única máquina. A troca é praticamente só de configuração: em vez do backend docker, usa-se o backend kubernetes, e o cluster distribui os contêineres de sessão por vários nós, com balanceamento e recuperação automática de falhas.

proxy:
  container-backend: kubernetes    # em vez de 'docker'
  kubernetes:
    namespace: shinyproxy
    # ... configs do cluster ...

A grande sacada arquitetural: nada muda no seu app. A mesma imagem golem+Docker que roda no seu laptop roda no Kubernetes de produção. Toda a complexidade de escala fica na plataforma (ShinyProxy + K8s), não no código R — que é justamente o objetivo de toda a trilha de engenharia deste curso.

Panorama das opções de deploy

Para fechar, um mapa das alternativas e quando cada uma faz sentido:

Opção Custo Isolamento Autenticação Quando usar
runApp localgrátisnenhumnenhumadesenvolvimento
Shiny Server (open source)grátisprocesso compartilhadonenhuma nativauso interno leve
ShinyProxygrátiscontêiner por sessãoLDAP/OIDC/SAMLmultiusuário, isolamento, controle total
Posit Connectpagogerenciadointegrada, robustaempresas que querem solução gerenciada
shinyapps.iofreemiumgerenciadobásica/pagaprotótipos e apps pequenos na nuvem

A escolha equilibra custo, esforço de operação e requisitos. ShinyProxy é o ponto ótimo quando você precisa de isolamento e autenticação sérios sem custo de licença, e tem disposição para operar a infraestrutura. Posit Connect troca dinheiro por conveniência (deploy com um clique, sem gerenciar Docker). Este curso te deixou apto ao caminho mais poderoso e portável — Docker + ShinyProxy — do qual os demais são simplificações.

O Shiny é single-threaded por sessão, e num servidor compartilhado isso significa que um usuário pesado afeta os outros, sem isolamento real. O ShinyProxy resolve dando a cada sessão seu próprio contêiner Docker: isolamento total, escalabilidade, autenticação integrada e reprodutibilidade, já que usa a mesma imagem que o app de produção. É uma aplicação Java que orquestra Docker ou Kubernetes, e precisa de duas coisas: a imagem do app e um application.yml. Nesse arquivo, specs define os apps, container-image liga cada um à sua imagem, access-groups cuida da autorização, container-env injeta configuração e segredos, e authentication integra LDAP, OIDC ou SAML. Um detalhe operacional derruba muita gente: a cadeia de portas precisa bater, do usuário à porta do proxy (8080) e dela à porta interna do contêiner (3838, com shiny.host='0.0.0.0'). O próprio ShinyProxy roda como contêiner, montando /var/run/docker.sock para poder criar e destruir os contêineres de sessão. Para larga escala, troca-se o backend docker por kubernetes sem mudar o app. E o panorama de opções, do mais simples ao mais gerenciado: runApp em desenvolvimento, Shiny Server para uso interno leve, ShinyProxy quando se quer isolamento e autenticação sem custo de licença, Posit Connect como solução gerenciada paga, e shinyapps.io para nuvem simples.

Exercícios

  1. Qual a diferença fundamental entre servir um app com Shiny Server (open source) e com ShinyProxy?

    ✓ Resposta:

    No Shiny Server open source, todos os usuários de um app compartilham o mesmo processo R (por app), sem isolamento: uma tarefa pesada ou um erro de um usuário pode degradar ou derrubar a experiência dos demais, e não há autenticação nativa robusta. No ShinyProxy, cada sessão de usuário roda em seu próprio contêiner Docker, isolado dos outros — crash, memória e CPU de um usuário não afetam os demais —, com autenticação integrada (LDAP/OIDC/SAML) e escalabilidade horizontal. Em resumo: processo compartilhado vs. contêiner por sessão.

  2. No application.yml, o que faz o campo container-image e por que ele conecta este módulo ao anterior?

    ✓ Resposta:

    container-image especifica qual imagem Docker o ShinyProxy deve instanciar quando um usuário abre aquele app (ex.: meu-dashboard:1.0). Ele conecta diretamente a implantação com Docker porque essa é exatamente a imagem construída lá — o app golem conteinerizado. O ShinyProxy não "conhece" R nem Shiny: ele apenas orquestra contêineres da imagem que você indicou. Por isso a reprodutibilidade do Docker se propaga ao deploy: se a imagem roda com docker run, roda no ShinyProxy do mesmo jeito.

  3. Explique a diferença entre authentication e access-groups no ShinyProxy, relacionando com os conceitos de notificações, validação e segurança.

    ✓ Resposta:

    authentication cuida da autenticação — "quem é você?" — validando as credenciais do usuário (via simple, LDAP, OpenID, SAML etc.) antes de qualquer app carregar. access-groups cuida da autorização — "o que você pode ver/fazer?" — restringindo quais grupos de usuários têm acesso a cada app (specs). É exatamente a distinção de notificações, validação e segurança: autenticação identifica; autorização controla permissões. No ShinyProxy, um usuário pode se autenticar com sucesso mas não ver um app cujo access-groups não inclui seu grupo.

  4. Por que o shiny.host = '0.0.0.0' configurado no Dockerfile é essencial para o ShinyProxy funcionar?

    ✓ Resposta:

    Porque o ShinyProxy encaminha as requisições do usuário para o app dentro do contêiner da sessão, através da rede Docker. Se o app escutasse apenas em 127.0.0.1 (localhost do contêiner), ele recusaria conexões vindas de fora do próprio processo — inclusive as do ShinyProxy — e o app "não carregaria". Com shiny.host = '0.0.0.0', o app escuta em todas as interfaces do contêiner, aceitando as conexões encaminhadas pelo proxy. É o mesmo motivo pelo qual o -p do docker run precisava disso; na cadeia usuário→proxy→contêiner, esse é o elo final.

  5. Você precisa migrar de um único servidor para um cluster que suporte centenas de usuários com alta disponibilidade. O que muda no ShinyProxy e — crucialmente — o que muda no seu app R?

    ✓ Resposta:

    No ShinyProxy, troca-se o backend de contêineres de docker para kubernetes (container-backend: kubernetes + configs do cluster), e o Kubernetes passa a distribuir os contêineres de sessão por vários nós, com balanceamento de carga e recuperação automática de falhas. No app R, não muda nada: a mesma imagem golem+Docker de implantação com Docker é usada. Essa é a grande vantagem arquitetural da trilha — a complexidade de escala vive na plataforma (ShinyProxy + K8s), não no código R, que permanece idêntico do laptop ao cluster de produção.

Fontes e leituras recomendadas