Em produção2026-05 – atualDesenvolvedor full-stack, único

Code Sphere

Sessenta alunos de Chromebook precisavam de um VS Code real cada, em hardware com memória para uns trinta.

  • docker
  • traefik
  • flask
  • python
  • pygame

O problema e as restrições

Uma sala de aula de até 60 alunos precisava da mesma coisa que cada um deles um dia vai querer num emprego de verdade: um VS Code de verdade, rodando Python de verdade, com um terminal de verdade, em vez de uma sandbox de ensino simplificada que precisaria ser desaprendida depois. O hardware disponível para rodar tudo isso era um único servidor dimensionado para mais ou menos metade dessa quantidade de pessoas ao mesmo tempo, porque na prática uma escola roda duas turmas de uns 30 alunos por vez, bem longe dos sessenta martelando a mesma máquina no mesmo instante.

Esse único fato (trinta de verdade, trinta em teoria) virou a restrição que moldou quase tudo o resto. Um container de code-server não é de graça nem parado: processo de extensão em background, observador de arquivo, servidor de linguagem custam de 400 a 700 MB de memória só existindo. Multiplique isso por sessenta e um servidor de 16 GB estoura o orçamento em três a quatro vezes antes de qualquer aluno escrever uma linha de código. A plataforma não podia assumir que os sessenta estavam ativos; precisava arranjar ativamente para que a maioria não estivesse, na maior parte do tempo, sem exigir que um professor gerenciasse isso na mão para cada aula.

A segunda restrição era a rede atrás da qual os alunos de fato estão. Wi-Fi escolar e Chromebook fornecido pela escola são notórios por bloquear qualquer coisa que não seja tráfego web padrão, porta não convencional principalmente. Fosse o que saísse do navegador de um aluno, tinha que parecer tráfego HTTPS comum na porta 443, indistinguível de qualquer outro site, sem número de porta para lembrar ou digitar.

A terceira restrição era quem opera isso no dia a dia: um professor só, sem departamento de TI por trás. A configuração inteira (gerar configuração, construir imagem, subir a stack) precisava colapsar num punhado pequeno de scripts que um não especialista conseguisse rodar uma vez, mais um botão de “pré-aquecer uma turma” para a única esquisitice operacional (30 a 45 segundos de partida fria) que containers sob demanda introduzem. Não existe plantão por trás dessa plataforma; o que quebrar, quebra na frente de uma sala de aula, em tempo real.

A quarta restrição chegou depois que a plataforma já existia e já funcionava para programação em texto simples: o pedido de também ensinar Pygame Zero, jogo visual e interativo de verdade. Isso é um formato de recurso completamente diferente: um jogo rodando precisa de uma tela para desenhar e, eventualmente, som para tocar, dentro do mesmo teto de memória e CPU por aluno que já era apertado antes de qualquer coisa desenhar um quadro ou misturar um buffer de áudio. Todo byte gasto tornando um jogo visível ou audível era um byte que deixava de estar disponível para a própria coisa que a plataforma existe para ensinar.

A última restrição era uma realidade específica e nada glamorosa de onde isso de fato roda: alguns dos ambientes rodando ou desenvolvendo contra essa plataforma são máquinas Windows usando WSL2, que tem esquisitices reais e documentadas em como compartilha arquivo e diretório com container Linux, esquisitices que não aparecem num ambiente de CI Linux limpo, mas aparecem sem dúvida na frente de uma turma de verdade.

Arquitetura

O navegador de um aluno fala com exatamente uma coisa na rede: o Traefik, escutando numa porta única. Toda rota que o Traefik conhece (o portal de login, e uma vaga por aluno, de /code/aluno01/ a /code/aluno60/) é escrita num único arquivo de configuração antes de qualquer container sequer iniciar, gerado uma vez na configuração em vez de descoberto em tempo de execução. Essa abordagem estática existe porque a alternativa mais “moderna” (deixar cada container anunciar a própria rota ao Traefik conforme inicia) parou de funcionar em silêncio: a integração do Traefik com o Docker fala com a API numa versão fixa e desatualizada, e o Docker Engine atual recusa conexões abaixo da versão mínima suportada. Pré-declarar toda rota contorna a incompatibilidade por completo e nem exige que o Traefik toque no socket do Docker.

Atrás desse proxy fica um portal pequeno em Flask, que é a única coisa com autoridade para iniciar ou parar o container de um aluno. Quando um aluno faz login, o portal confere a senha contra um hash Argon2id, inicia o container específico dele se ainda não estiver rodando, e espera (checando repetidamente a própria porta HTTP do container, em vez de simplesmente dormir um número fixo de segundos) até o code-server lá dentro estar de fato pronto para responder. Uma thread em background no mesmo portal percorre a lista de containers rodando de tempos em tempos e para qualquer um que tenha ficado quieto, devolvendo a memória para o próximo aluno que fizer login. Todo container que o portal cria começa completamente do zero em vez de retomar um parado: reiniciar do zero toda vez é o que garante que um token de acesso novo nunca colida com um token de uma sessão anterior.

Depois que o ambiente de um aluno está rodando, ele oferece mais que um terminal. Uma tela de monitor, construída sobre noVNC, mostra o que quer que o jogo em Pygame Zero do aluno esteja de fato desenhando: necessário no momento em que “programar” passa a significar “programar algo que se pode ver mexer na tela”. O áudio veio depois e teve que brigar pelo mesmo orçamento de recurso minúsculo que o caminho de vídeo já estava gastando: o PulseAudio dentro do container publica um sink virtual, um segundo processo websockify (a mesma ferramenta que já fazia a ponte do vídeo, então nenhuma dependência nova) carrega esse áudio bruto até o navegador pelo próprio WebSocket, e um script pequeno decodifica com a Web Audio API. Os dois canais, vídeo e áudio, rodam lado a lado pelo mesmo proxy, no mesmo limite de cgroup por aluno, dividindo um orçamento que já estava contabilizado antes de qualquer um dos dois precisar fazer barulho.

Três capturas de tela simples lado a lado: o menu do portal, oferecendo a escolha entre abrir o VS Code ou o monitor; o VS Code no navegador com um script em Pygame Zero aberto; e o monitor via noVNC, com o painel de opções aberto, renderizando um jogo de demonstração ao vivo.
As três telas de um aluno, da esquerda para a direita e de cima para baixo: o menu do portal, o VS Code, e o monitor via noVNC (painel de opções aberto) renderizando um jogo de demonstração escrito para este print. As três rodam como páginas de navegador comuns, sem nenhum aplicativo nativo envolvido.

Arquitetura

Code Sphere: architecture A student's browser talks to Traefik on a single public port, which routes by URL path to either the Flask portal or a specific student's container. The portal starts a student's container only on login and stops it after idle time. Each running container exposes VS Code, a screen monitor over noVNC, and audio over a separate websockify channel, all through the same Traefik routes. Student browser Chromebook, port 80 only Traefik static routes, one per slot Flask portal auth, start/stop, GC idle Student container created, stopped by default code-server VS Code in the browser noVNC: visual monitor websockify: audio (PCM) starts / stops 60 slots declared, ~30 running at once

Decisões

DECISION 01/04 · Ciclo de vida do container

EscolhidoDeclarar os 60 containers de aluno de antemão, mas nunca subir automaticamente: eles ficam num estado criado e parado, e o portal só inicia um quando aquele aluno faz login, parando de novo depois de um período de inatividade

DescartadoRodar os 60 containers o tempo todo

Cada container de code-server custa de 400 a 700 MB de RAM só parado; 60 rodando ao mesmo tempo estoura um servidor de 16 GB em três a quatro vezes, mas na prática uma turma de 30 não usa o pico de recurso todo mundo ao mesmo tempo. O teto real é o tamanho da turma, em vez do total de alunos

Custo aceitoO primeiro login do dia de qualquer aluno leva de 30 a 45 segundos enquanto o container sobe e o code-server inicializa. Um script de pré-aquecimento deixa o professor iniciar uma turma com antecedência, mas um professor que esquece de rodar isso tem alunos olhando para uma tela de carregamento bem na hora que a aula começa

DECISION 02/04 · Roteamento do proxy reverso

EscolhidoRotear todo tráfego pelo Traefik usando um arquivo de rotas gerado estaticamente, com o caminho de URL de cada aluno pré-declarado antes do proxy sequer iniciar

DescartadoRoteamento por label do Docker, em que cada container anuncia a própria rota ao iniciar

Roteamento por label parecia mais simples até quebrar: a integração do Traefik com o Docker fala com a API numa versão fixa e desatualizada, e o Docker Engine moderno recusa conexões abaixo da versão mínima suportada. Nenhuma variável de ambiente resolve, porque o Traefik negocia a versão internamente. Rotas estáticas contornam a incompatibilidade por completo e nem precisam que o Traefik toque no socket do Docker

Custo aceitoUma vaga de aluno nova exige um script de regeneração e reiniciar o proxy, em vez de simplesmente aparecer sozinha, custo aceitável, já que o número de alunos é fixado uma vez na configuração e não muda no meio do semestre

DECISION 03/04 · Entrega de áudio do jogo

EscolhidoTransmitir áudio bruto, sem compressão (PCM), do container de cada aluno para o navegador por um WebSocket simples, decodificado no cliente com a Web Audio API

DescartadoComprimir o áudio com Opus via ffmpeg e tocar por um elemento de áudio HTML padrão

As duas formas foram construídas e medidas na imagem real. Opus usava uma fração da banda, mas o elemento de áudio do navegador bufferiza de 1 a 3 segundos antes de tocar qualquer coisa: inútil para um jogo reagindo a uma tecla apertada. PCM bruto não tem buffer de codec nenhum, então a única latência é um jitter buffer deliberadamente pequeno

Custo aceito3,8% de um núcleo de CPU e cerca de 350 kbps de banda por aluno, tirados exatamente do mesmo orçamento de recurso que um jogo em Pygame Zero já quase esgota sozinho, um custo pago exatamente onde a plataforma menos pode pagar, aceito porque a alternativa simplesmente não funcionava para o caso de uso

DECISION 04/04 · Armazenamento de senha

EscolhidoHashear toda senha de aluno com Argon2id, usando os parâmetros de custo padrão da biblioteca, com re-hash automático no login se os padrões um dia mudarem

Descartadobcrypt

Alunos só precisam de senha com mínimo de 6 caracteres, o que torna quebra acelerada por GPU uma ameaça real se o arquivo de senhas algum dia vazar; Argon2 é memory-hard de um jeito que bcrypt não é, exatamente a propriedade que resiste a aceleração por GPU

Custo aceitoCada login leva de 50 a 100 ms só para a verificação do hash, deliberadamente lento, e tranquilo para umas 60 logins no início de uma aula, ainda que não escalasse para milhares de logins por segundo

Invariantes

  • Um container de aluno nunca sobe sozinho: fica num estado criado e parado até aquele aluno fazer login

    Garantido pora configuração `profiles: ["manual"]` em todo serviço de aluno no docker-compose.yml, que os exclui de um `docker compose up` simples

  • Um container de aluno ocioso eventualmente é parado, devolvendo a memória para o conjunto disponível

    Garantido poruma thread de coleta de lixo em background no portal que rastreia a última atividade por container e para o que passar do limite de ociosidade

  • Um container reiniciado nunca reaproveita o token de acesso de uma sessão anterior

    Garantido poro portal remove e recria o container por completo em cada início, em vez de retomar um parado, então um token novo é gerado toda vez

  • A senha de um aluno nunca é guardada nem exibida de uma forma que possa ser lida de volta

    Garantido porhash Argon2id em formato PHC; o caminho de login do portal só chama verify, nunca decrypt

  • Um daemon de áudio quebrado custa silêncio para a turma, nunca um jogo travado

    Garantido porSDL_AUDIODRIVER é configurado com a lista de fallback `pulse,dummy` em vez de só `pulse`, uma garantia medida na imagem real em vez de presumida na teoria: só com `pulse`, uma falha do PulseAudio levanta uma exceção que mata o jogo do aluno na hora

O que quebrou

Sintoma
Em máquinas Windows rodando a plataforma via WSL2, o Traefik subia e não roteava nada: o arquivo de rotas que ele precisava aparecia vazio para o container, mesmo existindo no host e tendo acabado de ser gerado
Causa raiz
O arquivo de rotas era montado como parte de um bind-mount de diretório que o Traefik também estava configurado para observar em busca de mudanças ao vivo. Bind-mounts de diretório no WSL2 são pouco confiáveis para arquivos criados imediatamente antes do container iniciar, e além disso, a observação de arquivo do Traefik depende de inotify, que não se propaga pela camada de sistema de arquivos 9P que o WSL2 usa para compartilhar arquivos com containers Linux, então mesmo um mount bem-sucedido nunca pegaria uma mudança posterior de qualquer forma
Correção
Passou a montar o arquivo específico em vez do diretório que o contém, e desligou a observação de arquivo por completo. O script de inicialização já recria o container do Traefik à força a cada execução, então o arquivo é lido do zero de qualquer jeito
Prevenção
Documentado como problema conhecido permanente no próprio registro de decisão de arquitetura do proxy reverso, especificamente para que uma futura limpeza que reintroduza observação de diretório por conveniência não quebre a plataforma em silêncio de novo, justamente no ambiente mais provável de estar rodando ela: o notebook Windows de um professor

Resultados

60contas de aluno em hardware dimensionado para ~30 simultâneoscontagem de diretórios alunos/alunoNN no deploy real, 2026-08-29
27commitscontagem de git log --oneline na main, 2026-08-29
11registros de decisão de arquiteturacontagem de arquivos em docs/decisions/, 2026-08-29
3,8%de um núcleo de CPU gasto com áudio do jogo, medidoADR-0011, medido ponta a ponta na imagem de aluno real
13arquivos de código do portal e da orquestraçãocontagem de arquivos .py/.sh/.js fora dos diretórios de aluno, 2026-08-29
desde maio de 2026em produção contínuadata do primeiro commit

Stack completa

Backend

  • Flask (Python)
  • Argon2id (argon2-cffi)
  • Docker SDK for Python

Frontend

  • code-server (VS Code no navegador)
  • noVNC
  • Web Audio API

Dados

  • Arquivo JSON (portal-data/alunos.json)

Infra

  • Traefik v3.2
  • Docker Compose
  • PulseAudio + websockify
  • Cloudflare Tunnel

Tem interesse em um projeto como esse? Entre em contato.

Entrar em contato