Pular para o conteúdo
LuzTech Blog
Voltar

Stargazed: container2wasm

9 minPor

Olá! Hoje quero compartilhar com você uma nova série de artigos no Blog da LuzTech, que vai se chamar Stargazed.

E do quê se trata esta série? Excelente pergunta! A ideia é compartilhar repositórios interessantes, criativos e/ou úteis que encontrei nas minhas andanças pelo GitHub, e que podem ser úteis para você também.

O nome é uma alusão à dar uma Estrela (Star) em um repositório no GitHub, e também à ideia de olhar para o céu estrelado (Stargazing) e se inspirar com o que encontramos por lá.

A ideia da série não é mostrar repositórios ultra-mega produtivos, mas sim trazer algo interessante e que, se for possível, te dê uma pontinha a mais de conhecimento.

Bom, introduções de lado, vamos para o que interessa!

Sumário

Sumário

Contêineres

Vamos começar com os contêineres. Essa tecnologia revolucionou a forma de desenvolver e implantar aplicações, e hoje é um requisito fundamental para um desenvolvedor moderno.

Contêineres permitem empacotar uma solução e tudo o que ela precisa em uma imagem imutável, portátil e (se bem feita) leve.

Como assim?

Bom, suponha que você tenha uma API em Node.js que recebe um vídeo por um endpoint e precisa convertê-lo para um formato compacto para uma análise de IA posterior (afinal ninguém merece gastar um monte de tokens com um vídeo em AVI de 5 segundos e 10GB).

E, para chamar o ffmpeg para fazer a conversão, você use o famoso exec do Node.js:

import { exec } from 'child_process';

// Suponha que inputPath e outputPath estejam devidamente sanitizados e validados
export default async function convertToSmallVideoFormat(inputPath, outputPath) {
  const process = exec(
    `ffmpeg -i "${inputPath}" -c:v libx264 -preset fast -crf 28 "${outputPath}"`
  );

  await new Promise((resolve, reject) => {
    process.on("exit", code => {
      if (!code) {
        resolve();
      } else {
        reject(new Error(`ffmpeg exited with code ${code}`));
      }
    });
  })
}

Na tua máquina vai funcionar, já que o FFMpeg vai estar instalado e disponível no PATH. Mas… e se tu precisar rodar em uma plataforma diferente, como Azure Functions, um Web App ou em uma VM Windows?

Daí tu vai precisar instalar o FFMpeg na máquina, o que não é possível e muitas plataformas serverless, como Azure Functions.

Então, qual a solução? Contêineres! Tu pode criar uma imagem com o Node.js e o FFMpeg instalados, e rodar a tua API em qualquer lugar que aceite contêineres, que é o caso da maioria das plataformas modernas, incluindo as que citei acima.

Um container para esta solução seria algo assim:

FROM node:24.15.0

WORKDIR /app

RUN apt-get update && apt-get install -y ffmpeg

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

CMD ["npm", "start"]

Depois é só rodar um docker build . e a imagem estará prontinha para rodar em praticamente qualquer lugar.

Dica

O código acima foi simplificado para fins didáticos. Para uso em produção, sempre procure as boas práticas da linguagem e/ou framework que você estiver usando.

O repositório

Com este breve resumo de contêineres, vem o ponto principal do artigo: o container2wasm. Quando descobri ele fiquei pasmo, pois, teoricamente, ele permite rodar qualquer coisa diretamente do navegador, sem instalar nada.

Mas claro, nem tudo são flores. Mesmo com um computador bem potente, o contêiner vai rodar com uma performance péssima, então não dá pra dizer que a experiência do usuário vai ser a melhor do mundo (se tiver como o usuário ter uma experiência, pra começo de conversa).

Além disso, a não ser que tenha um servidor de proxy de rede, as chamadas HTTP de contêiner vão usar a função fetch do navegador, que traz o infame CORS pra jogada, bloqueando qualquer requisição cross-domain sem o cabeçalho Access-Control-Allow-Origin adequado.

Mas, chega de enrolar. Como fazer um contêiner rodar no navegador? Primeiramente, precisamos instalar a ferramenta a partir das releases.

Dica

Para usuários Windows, é necessário usar o WSL ou outra forma de virtualização Linux, já que o container2wasm não tem suporte nativo para Windows. E para usuários do MacOS, existe uma fórmula no Homebrew que deixa a instalação bem mais simples.

Depois de instalado e configurado no PATH, os comandos c2w e c2w-net devem estar disponíveis no terminal.

Agora, pegue uma imagem salva localmente, pode ser uma imagem criada com docker build ou uma imagem baixada com docker pull, e rode o seguinte comando:

c2w --to-js "<imagem>:<tag>" "<pasta de saída>/"
Importante

Ao rodar a versão instalada via Homebrew, tive um erro de git clone ao fazer o build da imagem WASM. Pra resolver, adicionei esta flag: --build-arg "SOURCE_REPO=https://github.com/container2wasm/container2wasm". Não tive o mesmo problema ao clonar o repositório e fazer o build manualmente em Go.

Com isso, uma imagem WASM será gerada, e será possível adicionar o contêiner a um site. Mas… como fazer isso?

Rodando o contêiner WASM no navegador

Com os arquivos em mãos, é necessário criar um arquivo HTML que vá carregar o contêiner WASM. A pasta de exemplos do projeto contém uma boa referência. Para o caso de imagens usando a flag --to-js os exemplos de emscripten e emscripten-simple são o melhor ponto de partida.

E qual deles escolher?

A diferença entre eles é o gerenciamento de rede.

O emscripten permite que o contêiner se comunique com a internet por meio da API fetch do navegador, ou usando um WebSocket como proxy de rede (com o c2w-net, por exemplo).

Já o emscripten-simple não tem suporte a rede, e é mais simples de configurar.

Cada exemplo possui um README.md que mostra como criar uma imagem compatível e como configurar a rede, se necessário.

Importante

Todas as instruções a seguir foram retiradas do repositório e podem ficar desatualizadas. Se for seguir estes passos, confira se não houve atualizações no repositório antes de prosseguir.

Na prática: Neovim com o repositório de tipografia da LuzTech

Vamos testar o mais simples (emscripten-simple). Este é o Dockerfile que criei para abrir o Neovim diretamente na raíz do repositório de tipografia da LuzTech:

Importante

Se você olher no link do repositório, vai ver que usei a tag v2026.08.03-02.22. Isto porque, no momento em que eu estava trabalhando neste artigo, o repositório ainda não era um site.

FROM alpine:latest AS builder

RUN apk add --no-cache git

WORKDIR /workspace

RUN git clone "https://github.com/LuzTech-Development/Typography" .

RUN rm -rf .git

FROM alpine:latest AS runtime

RUN apk add --no-cache neovim

COPY --from=builder /workspace /workspace

WORKDIR /workspace

ENTRYPOINT ["nvim", "."]

Construí a imagem com o comando:

docker build --platform linux/amd64 -t nvim-demo:latest .
Dica

Eu adicionei a flag --platform linux/amd64 para forçar a construção da imagem para a arquitetura x86_64, já que o README do container2wasm recomenda esta arquitetura para diminuir as camadas de emulação.

Depois disso rodei o comando que descrevi na seção acima para gerar os arquivos em WebAssembly.

Então copiei o index.html do exemplo e adicionei o Fit Addon para o XTerm.js para o terminal ficar em tela cheia.

Ver index.html modificado
<html>
  <head>
    <title>container2wasm: Neovim + LuzTech Typography</title>
    <link rel="stylesheet" href="./vendor/xterm.css" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <style>
      html,
      body {
        width: 100%;
        height: 100%;
        margin: 0;
        overflow: hidden;
        background: #000;
      }

      #terminal {
        width: 100%;
        height: 100dvh;
      }
    </style>
  </head>
  <body>
    <div id="terminal"></div>
    <script src="./load.js"></script>
    <script type="module">
      import './vendor/xterm.js';
      import './vendor/xterm-pty.js';
      import { FitAddon } from './vendor/addon-fit.mjs';
      import './arg-module.js';
      import initEmscriptenModule from './out.js';

      const xterm = new Terminal();
      const fitAddon = new FitAddon();
      xterm.loadAddon(fitAddon);
      xterm.open(document.getElementById('terminal'));
      fitAddon.fit();

      const { master, slave } = openpty();

      xterm.loadAddon(master);

      Module.pty = slave;
      Module['mainScriptUrlOrBlob'] = new URL('./out.js', import.meta.url).href;

      // "/pack/info" file needs to be provided to the filesystem to start
      // the container. Refer to /cmd/init/main.go for the detail of info file.
      let info = 't:' + Math.round(new Date() / 1000) + '\n';
      Module['preRun'].push(mod => {
        try {
          FS.mkdir('/pack');
        } catch (e) {}
        mod.FS.writeFile('/pack/info', info);
      });
      var readableCallbacks = [];
      Module.pty.onReadable(() => {
        readableCallbacks.forEach(cb => cb());
        readableCallbacks = [];
      });
      Module['preRun'].push(Module => {
        Module['TTY'].stream_ops.poll = (stream, timeout, notifyCallback) => {
          if (Module.pty.readable) {
            return 1;
          }
          if (notifyCallback != null) {
            notifyCallback.registerCleanupFunc(() => {
              const i = readableCallbacks.indexOf(notifyCallback);
              if (i != -1) readableCallbacks.splice(i, 1);
            });
            readableCallbacks.push(notifyCallback);
          }
          return 0;
        };
      });

      const fitTerminal = () => fitAddon.fit();
      addEventListener('resize', fitTerminal);
      new ResizeObserver(fitTerminal).observe(
        document.getElementById('terminal')
      );

      (async () => {
        await initEmscriptenModule(Module);
        fitTerminal();
      })();
    </script>
  </body>
</html>
Nota

As dependências são carregadas via CDN no index.html original, mas os cabeçalhos Cross-Origin-Embedder-Policy e Cross-Origin-Opener-Policy (obrigatórios para o container2wasm funcionar) podem causar inconsistência no download delas. Para evitar isso, baixei tudo para uma pasta vendor/ ao lado do index.html.

Ver script para baixar dependências localmente
#! /bin/bash

mkdir -p vendor
curl -fsSL "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css" -o vendor/xterm.css
curl -fsSL "https://unpkg.com/xterm@5.3.0/lib/xterm.js" -o vendor/xterm.js
curl -fsSL "https://unpkg.com/xterm-pty/index.js" -o vendor/xterm-pty.js
curl -fsSL "https://esm.sh/@xterm/addon-fit@0.10.0/es2022/addon-fit.mjs" -o vendor/addon-fit.mjs

Por fim, para ver como ficou, rodei o comando serve de Vercel com o seguinte arquivo serve.json na raíz:

{
    "headers": [{
        "source": "**/*",
        "headers": [
            {
                "key": "Cross-Origin-Embedder-Policy",
                "value": "require-corp"
            }, {
                "key": "Cross-Origin-Opener-Policy",
                "value": "same-origin"
            }
        ]
    }]
}
Importante

Estes cabeçalhos são obrigatórios para o sistema funcionar, porém eles causam outros problemas, como, no caso do blog, impedir que o Gravatar funcione ou causar inconsistências com o download de dependências de CDNs. Se for usar este repositório em algum projeto tenha isto em mente.

E assim funcionou! Olha como ficou:

Neovim rodando no navegador no repositório de tipografia da LuzTech na pasta raíz
Neovim rodando no navegador no repositório de tipografia da LuzTech com o arquivo clean.svg aberto

Se quiser testar: Clique aqui

Nota

Demora BASTANTE para o contêiner iniciar, não é erro na implementação. Em um Macbook Air M5 demorou cerca 3m30s para aparecer a primeira tela do Neovim.

Conclusão

Tá, mas e o exemplo com rede? Estamos já na conclusão?

Sim! Eu me propus a explorar bem o repositório, mas o exemplo com rede consumiu bem mais tempo e energia do que eu tinha me proposto a usar. Então decidi focar no exemplo mais simples para esta postagem.

Dito isso, o que podemos concluir deste repositório?

Bom, é uma ideia bem interessante. Deixar contêineres mais portáteis do que já são. No entanto, sendo bem honesto, ainda há muitos desafios a serem superados para que isso seja realmente prático no dia a dia.

Primeiro eu diria que a experiência de uso não é muito boa, já que há várias variações e flags relevantes para configurar. No Docker, por exemplo, é docker build e docker run.

Em segundo lugar, apesar de ser compreensível, o tempo de compilação é bem longo, mesmo para imagens pequenas, o que torna iterações lentas e frustrantes.

E por fim eu diria que o uso ainda não faz sentido no cenário atual. Contêineres já são portáteis, tempos de carregamento são vitais no navegador e muitas ferramentas (Vite+, por exemplo) estão otimizando para experiência de desenvolvimento.

De qualquer forma, é um projeto legal para explorar e aprender sobre a arquitetura de contêineres e para ver até onde o navegador aguenta.


Compartilhe este artigo:

Próximo artigo
Bem-vindo ao Blog da LuzTech