Boas práticas para conteinerizar aplicações Python com Docker
Daniel Campos Olivares
11 de novembro de 2021
0 minutos de leituraAo ler vários artigos sobre contêineres Docker para Python, percebemos que a maioria apresenta exemplos de como conteinerizar uma aplicação Python sem considerar o framework usado (Django, Flask, Falcon etc.). Por exemplo, você pode encontrar algo assim:
Com este Dockerfile, podemos criar e executar uma aplicação Python com Flask:
São apenas duas etapas e funciona direitinho, certo?
Embora este exemplo seja simples e útil para demonstrações e tutoriais para iniciantes, ele deixa de lado várias questões importantes. Pensando nisso, neste post vamos abordar essas questões e conferir 6 boas práticas para conteinerizar aplicações Python com Docker. Vamos explorar por que você deve:
Usar tags explícitas e determinísticas para as imagens-base do Docker em aplicações Python conteinerizadas.
Separar as dependências do código-fonte.
Usar Python WSGI em produção.
Executar contêineres com o mínimo de privilégios possível (e nunca como root).
Lidar com estados não saudáveis da aplicação.
Encontrar e corrigir vulnerabilidades de segurança na imagem Docker da sua aplicação Python.
1. Use tags explícitas e determinísticas para as imagens-base do Docker em aplicações Python conteinerizadas
Embora pareça lógico usar python como imagem-base da nossa aplicação Python no Docker, isso deixa em aberto qual versão do Python está sendo usada.
Na época em que este artigo foi escrito, a imagem-base mencionada no Dockerfile correspondia a uma imagem com Python 3.10. Por quê? Como não adicionamos uma tag específica, o padrão foi a versão :latest dessa imagem-base, que, como podemos ver na página oficial da imagem no Docker Hub, é a 3.10
Como queremos controlar as versões do Python que vamos conteinerizar, devemos sempre especificar essa informação no Dockerfile.
Então, como quero usar a versão 3.10 do Python, basta adicionar a tag
:3.10ao meu Dockerfile, certo?
Bem... não exatamente.
A tag da imagem-base do Docker :3.10 corresponde a um sistema operacional completo com Python 3.10 instalado e inclui muitas bibliotecas que provavelmente você nunca vai usar. Além disso, essa grande quantidade de software tem um efeito colateral: aumenta a superfície de ataque devido às vulnerabilidades de segurança presentes nessas bibliotecas.
Se você usar uma imagem Docker grande para Python, será mais difícil fazer a manutenção e manter todas essas versões de bibliotecas atualizadas.
Se usarmos a ferramenta Snyk Advisor para analisar uma imagem-base do Python, veremos que a imagem-base do Docker python:3.10 tem 12 problemas de gravidade alta, 27 de gravidade média e 132 de gravidade baixa. Ou seja, por padrão, a imagem Docker do Python já começa com pelo menos 171 vulnerabilidades de segurança — e ainda não adicionamos nada!
Além disso, como estamos incluindo um sistema operacional completo, a imagem-base será bem grande para nosso servidor de aplicações Python. Isso resultará em builds mais lentos e exigirá mais espaço. Em geral, há algumas regras para escolher a imagem-base, mas duas são fundamentais.
Boas práticas para escolher uma imagem Docker para Python
Escolha a menor imagem-base que atenda a todos os seus requisitos e construa a partir dela. Imagens menores contêm menos vulnerabilidades, consomem menos recursos e incluem menos pacotes desnecessários.
Usar uma tag nomeada não basta para garantir que você sempre usará a mesma imagem-base. A única forma de garantir isso é usar o digest da imagem.
Com isso em mente, vamos voltar ao Snyk Advisor e conferir quais tags alternativas ele recomenda. A ferramenta oferece uma ótima visão geral das vulnerabilidades e do tamanho das imagens-base, o que ajuda muito na hora de decidir.
Como queremos usar o Python 3.10 na nossa aplicação, vamos procurar essa tag específica.

Além das vulnerabilidades mencionadas, podemos ver que esta imagem tem cerca de 350 MB, é baseada no Debian 11 e tem 427 pacotes instalados. Consideramos que ela é grande demais para a nossa pequena aplicação Python.
Por outro lado, a imagem-base do Docker para Python :3.10-slim tem 1 problema de gravidade alta, 1 de gravidade média e 35 de gravidade baixa. Seu tamanho-base é de 46,2 MB, ela também é baseada no Debian 11 e tem 106 pacotes instalados. Ao escolher essa imagem-base do Docker para nosso servidor de aplicações Python em vez da padrão, reduzimos o número de vulnerabilidades de segurança, o espaço em disco e a quantidade de bibliotecas instaladas — além de atender ao nosso requisito de usar a versão :3.10 do Python.
Pronto! É só adicionar a tag :3.10-slim e seguir em frente!
Quase! Cumprimos a primeira regra — temos uma imagem-base pequena do Docker que atende aos nossos requisitos —, mas ainda precisamos resolver a segunda questão. Precisamos garantir que usaremos exatamente essa imagem-base do Docker sempre que criarmos nosso servidor de aplicações Python.
Para fazer isso, temos algumas opções:
Obter o digest da imagem-base do Docker no Docker Hub.
Baixar a imagem Docker para o nosso computador com
docker pull python:3.10-slim, o que revela o digest da imagem:
Se já tivermos a imagem Docker do Python no computador, basta obter o digest da imagem existente em disco com o comando docker images --digests | grep python:
Depois de obter o digest da imagem-base, basta adicioná-lo ao Dockerfile mencionado acima:
Dockerfile
Essa prática garante que, sempre que recriarmos a imagem Docker desta aplicação Python, usaremos as mesmas versões do sistema operacional e das bibliotecas subjacentes. Isso torna o build determinístico.
2. Separe as dependências do código-fonte
Esta segunda boa prática evita um dos erros mais comuns em qualquer tipo de imagem Docker que envolva projetos com dependências. Primeiro, veja o que não fazer:
Copiar tudo da pasta do projeto para o contexto da imagem.
Instalar as dependências.
Executar a aplicação.
Funciona, mas há muito espaço para melhorar. Para começar, quando você desenvolve um projeto localmente, só instala as dependências quando elas mudam, certo? Então, não faça a imagem Docker baixar e instalar tudo de novo sempre que você alterar uma linha de código sequer.
Esta boa prática trata de otimizar as camadas de uma imagem Docker. Para aproveitar o sistema de cache durante um build do Docker, devemos ter sempre uma coisa em mente ao escrever um Dockerfile: ordenar as camadas de acordo com a frequência com que podem mudar.
Vamos conferir o Dockerfile que usamos até agora. Sempre que criamos a imagem da aplicação Python, o Docker examina as diferentes camadas e responde à pergunta: Algo mudou ou posso simplesmente reutilizar o que já fiz?
Na versão atual do Dockerfile, qualquer alteração na pasta do projeto aciona novamente a instrução COPY — e, em seguida, todas as camadas restantes do build. Isso não faz muito sentido e deixa bastante espaço para otimizar e acelerar o processo.
Vamos melhorar isso com o seguinte Dockerfile:
Com este novo Dockerfile, na próxima vez que o Docker verificar se pode reutilizar as camadas, se não houver alterações no arquivo requirements.txt, ele irá direto para a instrução COPY, que será processada em questão de segundos. Essa pequena mudança acelera bastante o processo de build — chega de esperar minutos entre builds toda vez que alteramos algo no código.
Vale lembrar que nem todas as dependências são necessariamente distribuídas como wheels. Nesse caso, seria preciso instalar um compilador na imagem.
Mas você não disse para usar a menor imagem possível para executar a aplicação?
Você tem razão. Por isso, vamos mostrar outro recurso incrível do Docker: os builds em vários estágios.
Builds em vários estágios
Em um build de vários estágios, usamos uma imagem Docker com mais ferramentas para compilar as dependências necessárias e, depois, copiamos apenas os artefatos necessários para a imagem Docker do Python usada na aplicação.
Veja um exemplo com aplicações baseadas em Node.js:
Observação: Este é um exemplo breve, apenas para demonstrar os recursos dos builds em vários estágios. Se você quiser aprender as boas práticas para conteinerizar aplicações Node.js corretamente, confira este artigo de Liran Tal (esse nome parece familiar...) e Yoni Goldberg.
Fazer builds em vários estágios para Node.js é bem simples, porque a pasta node_modules fica no mesmo diretório do projeto. Mas esse não é o caso das aplicações Python.
Se simplesmente executarmos pip install, muitos itens serão instalados em vários locais, o que impossibilita fazer um build em vários estágios. Para resolver isso, temos duas opções:
Usar
pip install --userUsar um
virtualenv
Usar pip install --user pode parecer uma boa opção, já que todos os pacotes serão instalados no diretório ~/.local e é fácil copiá-los de um estágio para outro. Mas isso cria outro problema: você adicionaria à imagem-base final do Docker todas as dependências de sistema da imagem usada para compilar as dependências — e não queremos isso (lembre-se da boa prática de usar a menor imagem-base possível).
Descartada a primeira opção, vamos explorar a segunda: usar um virtualenv. Nesse caso, o Dockerfile ficaria assim.
Agora temos todas as dependências necessárias, sem os pacotes extras exigidos para compilá-las.
Problemas conhecidos dos builds em vários estágios para aplicações Python conteinerizadas
Atualmente, há um problema conhecido: os estágios anteriores não são armazenados em cache. A maneira mais fácil de resolver isso é usar o BuildKit e adicionar o argumento BUILDKIT_INLINE_CACHE=1 ao processo de build.
Na primeira vez, faremos o build normalmente. Nas seguintes, usaremos o comando abaixo:
Você precisará da variável de ambiente DOCKER_BUILDKIT=1 para que o Docker saiba que deve usar o BuildKit no processo de build. Também é possível ativá-lo adicionando as configurações abaixo ao arquivo de configuração do Docker em /etc/docker/daemon.json (assim, não será necessário definir a variável de ambiente):
3. Use Python WSGI em produção
Manter o modo de depuração ativado em aplicações Python preparadas para produção é uma péssima ideia — e uma brecha de segurança esperando para ser explorada. Infelizmente, é um erro comum que encontramos em muitos artigos sobre aplicações Python Flask conteinerizadas e outros frameworks de aplicações Python WSGI.
O depurador permite executar código Python arbitrário pelo navegador, o que representa um enorme risco de segurança. É possível reduzir esse risco até certo ponto, mas ele sempre será uma vulnerabilidade. Mais uma vez, por segurança: não execute servidores de desenvolvimento nem depuradores em um ambiente de produção! Isso também vale para suas aplicações Python conteinerizadas.
Sabemos que é mais fácil simplesmente implantar suas aplicações Python Flask no modo de depuração do que configurar um servidor WSGI e o servidor web/proxy, mas isso só é mais fácil até você ter que explicar por que sua aplicação foi invadida.
Como resolver isso? Primeiro, você deve decidir qual implementação de servidor WSGI quer usar. Estas são as quatro opções mais comuns para aplicações Python:
Green Unicorn (Gunicorn) — Modelo de workers pré-fork, portado do projeto Unicorn, do Ruby.
uWSGI — Implementação versátil e eficiente de servidor WSGI, com baixo consumo de recursos.
mod_wsgi — Um módulo do Apache que pode hospedar qualquer aplicação web em Python compatível com a especificação WSGI do Python.
CherryPy — Um framework HTTP orientado a objetos, com estilo Python, que também funciona como servidor WSGI.
Neste artigo, usaremos o Gunicorn como exemplo, mas fique à vontade para consultar a documentação e as informações de cada opção e escolher a que melhor atende às suas necessidades. Nesta publicação, não abordaremos a configuração, pois ela depende totalmente do caso de uso.
Para a parte de containerização, precisamos apenas:
Incluir a dependência
gunicornemrequirements.txtAlterar o ponto de entrada do container da aplicação Python (consulte a instrução
CMDpara fazer essa alteração):
Depois de recriar a aplicação Python com base nesse novo Dockerfile, podemos executá-la e testar se a aplicação Flask está pronta para processar solicitações:
Observação: para implantações realmente prontas para produção, recomendamos não vincular diretamente ao host a porta exposta pelo Gunicorn. Em vez disso, recomendamos implantar um servidor proxy reverso na mesma rede, responsável por gerenciar todas as solicitações HTTP e servir arquivos estáticos.
4. Execute aplicações Python em containers com o menor privilégio possível (e nunca como root)
O princípio do menor privilégio é um controle de segurança usado há muito tempo, desde os primórdios do Unix — e devemos sempre segui-lo ao executar aplicações Python em containers.
A imagem oficial do Docker python não inclui um usuário privilegiado por padrão. Portanto, precisamos criá-lo para executar o processo com um usuário com o menor privilégio possível.
Para isso, adicionaremos os seguintes comandos groupadd ao Dockerfile da imagem final (a segunda etapa da compilação em várias etapas), que executa o processo gunicorn:
O problema é que, com as modificações anteriores, o usuário python é o proprietário do processo do sistema executado pelo Docker... E quanto à propriedade dos arquivos copiados ou do diretório WORKDIR? Por padrão, o compilador do Docker cria o diretório WORKDIR se ele não existir, mas o cria com o usuário do sistema root como proprietário. Assim, qualquer operação que envolva gravar nesse diretório pode causar uma falha fatal na aplicação. Além disso, por padrão, os arquivos copiados serão de propriedade de root se não alterarmos esse comportamento, mesmo que já tenhamos trocado de usuário.
Vamos corrigir isso:
Com as instruções COPY atualizadas acima, evitamos comportamentos inesperados relacionados à propriedade dos arquivos no diretório WORKDIR e garantimos que todos os arquivos pertençam ao mesmo usuário que executará o processo.
5. Gerencie estados não saudáveis da sua aplicação Python em container
Ao implantar uma aplicação, você precisa considerar possíveis eventos ou problemas não tratados que podem deixá-la em um estado não saudável, no qual: 1) ela para de funcionar, mas 2) o processo não é encerrado. Se isso acontecer, o container não será notificado, e você ficará com um servidor de aplicação Python em execução que não responde mais às solicitações HTTP.
Para evitar isso, queremos implementar um endpoint de verificação de integridade. Para verificar a integridade da sua aplicação Python em container, recomendamos sempre incluir um endpoint HTTP de monitoring ou de health, para confirmar que a aplicação continua processando as solicitações dos usuários corretamente. Em alguns frameworks web para Python, como o Flask, essa tarefa é simples (veja o exemplo abaixo). Em conjunto com a instrução HEALTHCHECK do Docker, ela garante que o estado da aplicação seja monitorado adequadamente.
Este trecho de uma aplicação Flask em Python adiciona um endpoint HTTP /health:
"Depois de adicionar esse endpoint à aplicação, basta incluir a instrução HEALTHCHECK no Dockerfile:
Se você estiver implantando no Kubernetes, saiba que as diretivas HEALTHCHECK do Dockerfile são ignoradas. É necessário incluir no YAML uma sonda de atividade, prontidão e inicialização do Kubernetes equivalente.
Então, para adaptar a instrução HEALTHCHECK acima às implantações no Kubernetes:
Essa configuração deve ficar aninhada na parte container spec do arquivo YAML do pod ou da implantação. Agora, com uma política de reinicialização definida como always ou unless_stopped, o container da nossa aplicação Python será sempre reiniciado se entrar em um estado não saudável.
6. Encontre e corrija vulnerabilidades de segurança na imagem Docker da sua aplicação Python
Já vimos que imagens-base maiores trazem vários desafios, como uma pilha de software extensa que precisamos manter atualizada com as correções de segurança, entre outras coisas.
Também vimos como usar o Snyk Advisor para consultar métricas de tamanho e vulnerabilidades de diferentes imagens-base. Mas o Advisor é só a ponta do iceberg. A Snyk é uma plataforma gratuita de segurança para desenvolvedores que você pode usar para testar tudo: do seu próprio código Python às dependências Python (como as listadas em requirements.txt), à imagem de container Python que executa sua aplicação e até às configurações de Terraform ou Kubernetes que orquestram tudo.
O melhor da Snyk é que ela recomenda correções para as vulnerabilidades encontradas. Em vez de apenas mostrar quais vulnerabilidades existem, ela cria automaticamente um pull request com a correção ou fornece orientações para remediá-las. E tudo isso acontece nas ferramentas (IDE, CLI, Docker etc.) e nos fluxos de trabalho (Git, CI/CD etc.) que você já usa.
Exemplo: como analisar com a Snyk nosso aplicativo Python em container
Vamos ver como isso funciona com a CLI da Snyk, usando a imagem Docker python:3.8 para compilar este exemplo de aplicação Flask em Python.
Se você instalar a CLI da Snyk, poderá usá-la para analisar as dependências do seu projeto Python, seu código Python e muito mais. Primeiro, vamos instalar a CLI da Snyk. Se você tiver um ambiente Node.js, poderá fazer isso com o gerenciador de pacotes npm, da seguinte forma:
Se você usa macOS ou Linux e o Homebrew, também pode instalá-la assim:
Para conhecer outros métodos de instalação, consulte nosso guia sobre como instalar a CLI da Snyk.
Em seguida, precisamos nos autenticar pela CLI para obter um token de API válido e consultar o banco de dados de vulnerabilidades:
Com isso concluído, podemos criar uma imagem Docker local da aplicação Python usando a imagem-base python:3.8:
Agora, vamos analisá-la com a Snyk executando:
O resultado apresenta os dados a seguir (reduzidos intencionalmente, pois a saída é extensa):
Temos 427 dependências na forma de bibliotecas de código aberto incluídas no conteúdo do sistema operacional Python 3.8. Elas introduzem um total de 171 vulnerabilidades de segurança nesta aplicação Flask em Python, devido à escolha da imagem-base python:3.8.
Neste momento, talvez você esteja se perguntando: "Como faço para corrigir isso?" Felizmente, a Snyk recomenda outras imagens-base para as quais você pode atualizar ou migrar, reduzindo assim a superfície de ataque.
Veja uma captura de tela com as recomendações para essa imagem-base:

Agora podemos tomar uma decisão embasada em dados para proteger nossa aplicação Python. Ao escolher qualquer uma das imagens Docker alternativas recomendadas pela Snyk, podemos reduzir significativamente a superfície de ataque do software incluído na aplicação.
Para ter ainda mais controle sobre a segurança da sua aplicação, conecte seus repositórios à interface da Snyk para importar seu código-fonte e o Dockerfile. Assim, além de encontrar essas vulnerabilidades, você poderá monitorá-las continuamente e identificar novos problemas de segurança. Veja o mesmo relatório da imagem-base do Docker na interface da Snyk:

O que é melhor do que monitorar e encontrar vulnerabilidades de segurança? Corrigi-las! :-)
Se você conectar seus repositórios Git à Snyk, também poderemos criar pull requests automaticamente no seu repositório para sugerir atualizações da imagem-base do Docker, como neste exemplo:

Se você gostou, confira esta publicação complementar sobre como automatizar a segurança de containers com pull requests para Dockerfiles.
Como aplicações Python são conteinerizadas?
Docker é uma tecnologia de virtualização de software que permite criar aplicações Python conteinerizadas reutilizáveis, multiplataforma e rápidas de implantar, com base em imagens Docker do Python. Essas aplicações são definidas como infraestrutura como código em um arquivo chamado Dockerfile.
Para criar e usar um aplicativo Python em contêiner, execute o seguinte:
docker build -t flask-application . docker run -p 8080:5000 flask-application
Recomendações de segurança em Python para desenvolvedores
Essas práticas recomendadas ajudam você a criar, gerenciar e proteger melhor suas aplicações Python em containers. Se você gostou de conhecer essas práticas e valoriza a segurança de aplicações e a promoção da segurança de modo geral, recomendamos estes conteúdos para continuar aprendendo:
O excelente guia de Daniel Berman sobre como começar a usar a Snyk para desenvolver em Python com segurança
Os segredos da CLI da Snyk, de Brian Vermeer, com uma ótima folha de consulta rápida
Estas práticas recomendadas abrangentes para aplicações Node.js com imagens-base Docker, de Liran Tal e Yoni Golberg
E, por fim, para quem trabalha com Java, este artigo de Brian Vermeer: Docker para desenvolvedores Java: 5 coisas que você precisa saber para não comprometer sua segurança.
A folha de consulta rápida de Frank Fischer sobre práticas recomendadas de segurança em Python
Um relatório abrangente e repleto de insights sobre problemas de segurança comuns em projetos Python
Segurança de contêineres que prioriza os desenvolvedores
O Snyk encontra e corrige automaticamente vulnerabilidades em imagens de contêiner e workloads do Kubernetes.



