Um aplicativo Linux legado de 15-y anos que ainda executa a lógica central do negócio não é um problema a ser resolvido. É um ativo que ninguém quer tocar porque ninguém entende completamente tudo o que ele faz.
O aplicativo em si raramente é o risco. O ambiente ao redor dele é. Um sistema operacional envelhecido, pacotes sem suporte, etapas de configuração manuais e dependências não documentadas podem transformar cada implantação em uma pequena aposta.
Algumas organizações contratam um parceiro de software para modernizar um sistema legado sem substituí-lo para gerenciar esse risco diretamente. Para equipes que desejam conteinerizar um aplicativo Linux legado internamente, a conteinerização pode fornecer um ponto de partida controlado, empacotando o aplicativo e suas dependências de runtime necessárias em um ambiente isolado e reproduzível.
As etapas abaixo percorrem o processo em ordem: avalie se o aplicativo é um bom candidato, mapeie suas dependências reais, construa e teste um contêiner funcional e implante-o em produção gradualmente.
Docker é usado como exemplo aqui, mas você também pode usar Podman.
Confirme se o aplicativo é um bom candidato
Antes de escrever qualquer coisa, verifique o aplicativo em relação a duas listas.
Bons candidatos geralmente têm:
- Limites claros de aplicativo
- Dependências reproduzíveis (você pode listar o que ele precisa, mesmo que a lista seja longa)
- Interfaces de rede padrão (TCP, HTTP ou similares)
- Requisitos de armazenamento gerenciáveis
- Sem dependências incomuns de hardware
- Um runtime que executa sem privilégios especiais do host
Candidatos difíceis geralmente envolvem comportamento específico do kernel, drivers de hardware especializados, sensibilidade extrema a latência, licenciamento vinculado a máquinas físicas ou lógica fundida diretamente ao sistema operacional host.
Se o seu aplicativo se enquadra principalmente na segunda lista, a conteinerização custará mais esforço do que retornará. Confirme qual lista descreve seu sistema antes de continuar.
Inventarie cada dependência
Comece com uma lista de verificação: versão do SO, runtime, bibliotecas compartilhadas, pacotes instalados, variáveis de ambiente, arquivos de configuração, caminhos de sistema de arquivos, bancos de dados, APIs externas, portas de rede, certificados, segredos, cron jobs, processos em segundo plano e locais de armazenamento.
Em seguida, confirme a lista de verificação contra o que o aplicativo realmente faz, usando ferramentas que observam o comportamento real em vez de confiar na memória ou em documentação antiga.
Encontre cada arquivo que o processo toca enquanto executa:
strace -f -e trace=open,openat -p <PID> 2>&1 | grep -v ENOENT
Isso imprime cada arquivo que o aplicativo abre, incluindo aqueles que ninguém documentou. Execute-o durante a operação normal, não apenas na inicialização, pois cron jobs e tarefas em segundo plano geralmente tocam caminhos diferentes do processo principal.
Encontre cada conexão de rede aberta e identificador de arquivo:
lsof -p <PID>
Isso mostra soquetes abertos, arquivos abertos e seus caminhos em uma única passada. Cruze isso com sua lista de verificação. Qualquer coisa nesta lista que não esteja documentada é uma dependência oculta.
Descubra contra o que o binário está vinculado:
ldd /path/to/binary
Isso lista as bibliotecas compartilhadas que o aplicativo requer em tempo de execução. Qualquer biblioteca listada como "não encontrada" aqui é uma que você precisará instalar na imagem do contêiner.
strace adiciona sobrecarga notável e pode desacelerar o processo enquanto está anexado, então limite seu uso a janelas curtas em produção. Se você precisar observar um sistema de produção ao vivo por mais tempo, uma ferramenta baseada em eBPF como bpftrace captura os mesmos dados de syscall e acesso a arquivos com muito menos sobrecarga, pois executa no kernel em vez de interceptar cada chamada.
Quatro categorias são responsáveis pela maioria das surpresas que esta etapa revela:
- Suposições de sistema de arquivos. Leituras ou gravações em
/etc/,/opt/,/var/lib/ou/tmp/que assumem que esses caminhos sempre existem. - Suposições de permissão. Executar como root, ou esperar um UID ou GID específico que o usuário padrão de um contêiner não terá.
- Suposições de rede. Chamadas para
localhostpara um serviço que costumava executar na mesma máquina física, ou referências a um hostname fixo ou IP estático. - Suposições de SO. Dependência de um comportamento específico do kernel, fuso horário, locale ou um utilitário de sistema não instalado por padrão em uma imagem mínima.
Quando a saída de strace, lsof e ldd parar de revelar algo novo, o inventário está completo o suficiente para começar a construir.
Escolha uma imagem base
FROM ubuntu:latest não é automaticamente o ponto de partida certo. Uma imagem base é o sistema de arquivos e conjunto de ferramentas iniciais sobre o qual um contêiner é construído.
Combine a imagem base com o que você encontrou na Etapa 2, não com o que é mais novo:
# Check the glibc version the old server uses
ldd --version
# Check the OpenSSL version
openssl version
# Compare against what's available in a candidate base image
docker run --rm ubuntu:24.04 bash -c "ldd --version && openssl version"
Se as versões estiverem próximas, essa imagem base é um ponto de partida razoável. Se estiverem várias versões principais distantes, espere que as bibliotecas que sua verificação ldd encontrou se comportem de maneira diferente e planeje testar isso especificamente quando o contêiner estiver em execução.
Ubuntu 26.04 LTS é o lançamento atual, mas não é automaticamente a escolha certa para uma migração legada. Para um aplicativo de 15-y anos, ubuntu:24.04 geralmente é o ponto de partida mais seguro. Ainda é um lançamento LTS totalmente suportado. Você pode migrar para 26.04 mais tarde, quando o contêiner estiver estável, se quiser o toolchain mais novo.
Escreva o primeiro Dockerfile e espere que ele falhe
Comece com o mínimo necessário para executar o aplicativo, com base diretamente no inventário da Etapa 2:
FROM ubuntu:24.04
# Install the runtime and OS packages identified during dependency analysis
RUN apt-get update && apt-get install -y \
libssl3 \
libpq5 \
<other-packages-from-your-inventory> \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY ./legacy-app /app/
EXPOSE 8080
CMD ["/app/legacy-app"]
Construa e execute:
docker build -t legacy-app:test .
docker run --rm -p 8080:8080 legacy-app:test
Neste ponto, espere falhas específicas e diagnosticáveis em vez de um início limpo. Um status de contêiner de sucesso não significa um aplicativo funcionando, pois uma verificação de saúde apenas confirma que o processo foi iniciado.
Falhas comuns de primeira -rexecução e o que geralmente significam:
Corrija uma falha de cada vez, reconstrua e execute novamente. Este loop, não o Dockerfile inicial, é onde a maior parte do trabalho real acontece.
Mova o estado para fora do contêiner
O sistema de arquivos de um contêiner é temporário por padrão. Qualquer coisa gravada dentro dele desaparece quando o contêiner é removido.
Identifique o que o aplicativo realmente precisa persistir (arquivos enviados, relatórios gerados, logs, arquivos de banco de dados se não estiver usando um banco de dados externo) e monte isso especificamente:
docker run -d \
-v /host/data/uploads:/app/uploads \
-v /host/data/reports:/app/reports \
-p 8080:8080 \
legacy-app:test
Não monte todo o sistema de arquivos do servidor antigo como um atalho. Monte apenas os caminhos específicos identificados na Etapa 2 como necessitando de persistência. Cada montagem adicional é um caminho do qual o contêiner depende para existir corretamente no host, e cada uma deve ser uma escolha deliberada.
Separe processos em segundo plano
Um sistema de 15-y anos é frequentemente, na prática, um servidor executando o aplicativo mais um worker, cron jobs, scripts de limpeza e rotação de logs ao mesmo tempo, porque ninguém nunca os separou.
Liste cada processo em segundo plano que o inventário da Etapa 2 encontrou e atribua a ele um destes:
# Example docker-compose.yml separating responsibilities
services:
app:
image: legacy-app:latest
ports:
- "8080:8080"
worker:
image: legacy-app:latest
command: ["/app/run-worker.sh"]
cron:
image: legacy-app:latest
command: ["/app/run-cron.sh"]
Um padrão comum de contêiner é dar a cada responsabilidade gerenciada independentemente seu próprio contêiner. Isso torna o ciclo de vida, o escalonamento, a falha e o monitoramento mais fáceis de controlar. Agrupar responsabilidades gerenciadas independentemente em um único contêiner pode recriar parte do acoplamento operacional que você estava tentando remover.
Endureça o contêiner antes da produção
Um contêiner não corrige problemas de segurança por padrão. Ele isola um processo, mas o processo ainda carrega os riscos que tinha antes.
Execute como um usuário não--rroot:
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
Defina limites explícitos de recursos. Sem eles, um contêiner pode consumir mais CPU ou memória do host do que o pretendido e afetar outras cargas de trabalho.
docker run -d \
--memory="512m" \
--cpus="1.0" \
legacy-app:production
Escanee a imagem em busca de vulnerabilidades conhecidas antes de implantá-la:
docker scout cves legacy-app:production
Isso requer uma conta Docker. Execute docker login primeiro se você ainda não autenticou a CLI. Você também pode usar sua ferramenta de escaneamento de imagem preferida para identificar vulnerabilidades conhecidas antes da implantação.
Mova segredos para fora da imagem e das variáveis de ambiente e para um gerenciador de segredos apropriado para sua plataforma. Publique apenas as portas que o aplicativo realmente precisa e aplique o acesso à rede por meio do runtime do contêiner, proxy reverso, firewall ou plataforma de orquestração.
Valide contra o sistema legado
O aplicativo legado é a linha de base para o comportamento correto. O contêiner deve corresponder a ele, não apenas iniciar com sucesso.
Teste os mesmos fluxos de trabalho em ambos os sistemas e compare:
Execute uma comparação básica de carga para detectar regressões de desempenho cedo:
# Example using Apache Bench against both systems
ab -n 1000 -c 10 http://legacy-server:8080/
ab -n 1000 -c 10 http://localhost:8080/
Não avance para a implantação até que cada fluxo de trabalho aplicável tenha sido validado e quaisquer diferenças da linha de base legada sejam compreendidas.
Você também pode usar uma ferramenta de teste de carga -t apropriada para a carga de trabalho real do aplicativo e comparar tempos de resposta, throughput, CPU, memória e taxas de erro entre as versões legada e conteinerizada.
Implante gradualmente
Mudar todo o tráfego de produção do sistema antigo para o novo em um único momento é a maneira mais arriscada de implantar essa mudança.
Execute ambos os sistemas em paralelo atrás de um proxy reverso e mude o tráfego gradualmente:
upstream backend {
server legacy-server:8080 weight=9;
server container-app:8080 weight=1;
}
Aumente o peso do contêiner conforme a confiança aumenta e monitore taxas de erro e tempos de resposta em cada etapa.
Se o aplicativo está sendo dividido em partes menores ao longo do tempo em vez de movido como uma unidade, o padrão strangler fig se aplica: um proxy na frente de ambos os sistemas roteia cada requisição para o aplicativo legado ou para o novo componente específico que substituiu aquela parte da funcionalidade. Isso permite migrar uma capacidade de cada vez. É útil quando você está decompondo a funcionalidade gradualmente, não quando está movendo um aplicativo intacto para um contêiner como uma única etapa.
Antes de qualquer corte, escreva como serão o sucesso e a falha, o que você monitorará, como você mudará o tráfego de volta se necessário e quem tem autoridade para tomar essa decisão.
Uma migração segura não é aquela em que a falha é impossível. É aquela em que a falha tem um caminho controlado e pré--ddefinido de volta a um estado funcional.
O que a conteinerização corrige e o que ela não corrige
A conteinerização normalmente melhora o isolamento de dependências, a reprodutibilidade do ambiente, a consistência da implantação, a portabilidade, a velocidade de rollback e a integração CI/CD.
Ela não corrige arquitetura ruim, dívida técnica existente, lógica de negócios desatualizada, cobertura de testes pobre, design monolítico, problemas subjacentes de banco de dados ou regras de negócios que nunca foram documentadas.
Conhecer essa distinção previne um erro comum: tratar a conteinerização como um substituto para o trabalho arquitetural que ela nunca foi destinada a fazer.
Quando conteinerizar primeiro e quando não
Conteinerize primeiro quando o aplicativo funciona e o ambiente é o problema real, quando as dependências podem ser reproduzidas de forma confiável, quando a carga de trabalho não requer acesso incomum ao host e quando uma reescrita completa introduziria mais risco do que remove.
Considere um caminho diferente quando a arquitetura em si é o gargalo, quando o aplicativo depende de comportamento do SO que não é mais suportado, quando o acesso a hardware é essencial para seu funcionamento, quando substituí-lo diretamente seria simplesmente mais barato ou quando os requisitos de negócios para os quais foi construído mudaram fundamentalmente.
Não há uma resposta universalmente correta. A escolha certa depende de quais dessas condições realmente descrevem seu sistema.
A conteinerização é um passo, não uma linha de chegada
Um contêiner funcional é uma fundação, não um destino. Ele normalmente se torna a base para automação CI/CD, melhores testes, observabilidade aprimorada e trabalho adicional de modernização, se e quando isso se tornar necessário.
Um aplicativo Linux legado não precisa ser reescrito antes de ser modernizado. A conteinerização pode ser um primeiro passo controlado em direção a um ambiente mais reproduzível, mais portátil e mais fácil de manter, sem tocar na lógica de negócios da qual a produção já depende.



