Conteúdo

Tutorial de Docker para aplicações Python: implante Flask, FastAPI e Django

Nesta seção, usamos Flask como exemplo principal e percorremos toda a estrutura do projeto, o Dockerfile, o build e a execução, além do modo de desenvolvimento. Em seguida, explicamos as diferenças de FastAPI, Django e uv. Todos os exemplos usam a imagem base python:3.14-slim.

Flask: estrutura do projeto

flask-app/
├── app.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
└── compose.yaml      ← usado apenas no modo de desenvolvimento

app.py:

from flask import Flask

app = Flask(__name__)


@app.get("/")
def index():
    return {"message": "Hello from Flask in Docker"}

requirements.txt:

flask==3.1.*
gunicorn==23.*

Escrever a versão como 3.1.* significa aceitar atualizações da série 3.1, sem passar automaticamente para 3.2. Em projetos de produção, recomendamos fixar a versão completa com pip freeze ou uv (veja adiante).

Flask: Dockerfile

FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

RUN useradd --system --no-create-home app
USER app

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app:app"]

Explicação linha por linha:

ConfiguraçãoFunção
python:3.14-slimVersão enxuta baseada em Debian, com glibc. A maioria dos pacotes tem wheel pré-compilado e pode ser instalada diretamente.
PYTHONUNBUFFERED=1A saída não passa por buffer, então você pode vê-la em tempo real com docker logs.
PYTHONDONTWRITEBYTECODE=1Não gera arquivos .pyc.
PIP_NO_CACHE_DIR=1pip não mantém o cache de downloads, o que reduz o tamanho da imagem.
Primeiro COPY requirements.txt, depois pip installQuando você altera apenas o código, a camada de instalação dos pacotes pode usar o cache. Consulte Cache de build.
useradd + USER appExecuta com um usuário que não é root. Consulte Segurança do Docker.
gunicorn ... app:appEm produção, use gunicorn em vez de flask run (servidor de desenvolvimento). app:app significa “a variável app no módulo app.py”.
--bind 0.0.0.0:8000É necessário vincular a 0.0.0.0. Se você vincular a 127.0.0.1, não será possível conectar de fora do contêiner.

CMD usa o formato exec (array JSON). gunicorn é o PID 1 e recebe diretamente o SIGTERM enviado por docker stop, encerrando de forma controlada. Assim, não espera 10 segundos até ser encerrado à força, como no exemplo de Instruções do Dockerfile em detalhes.

.dockerignore:

__pycache__
*.pyc
.venv
.env
.git

Você sempre deve excluir .venv: o ambiente virtual local é destinado ao sistema operacional da sua máquina. Copiá-lo para o contêiner não serve para nada e ainda aumenta o conteúdo do build.

Flask: construir e executar

$ ls -a
.
..
.dockerignore
Dockerfile
app.py
compose.yaml
requirements.txt

$ docker build -t flask-app .

[+] Building 6.0s (11/11) FINISHED                                                                                                     docker:default
 => [internal] load build definition from Dockerfile                                                                                             0.0s
 => => transferring dockerfile: 364B                                                                                                             0.0s
 => [internal] load metadata for docker.io/library/python:3.14-slim                                                                              0.0s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 74B                                                                                                                 0.0s
 => [1/6] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170                        0.8s
 => => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170                        0.0s
 => => extracting sha256:a5883d972f3b60387a858b212a0a24115f143c826e40fdef3270e8474a8f42d6                                                        0.1s
 => => extracting sha256:6aaf5b0aae75c38277901ef64362f50475b879e0ff3ed6610eb2a9f69bcf3295                                                        0.7s
 => => extracting sha256:bb8166eb34e6e8ea17f8c73a7eec8438693d91ebf29eda1d9d3c0dba36b579de                                                        0.0s
 => [internal] load build context                                                                                                                0.1s
 => => transferring context: 907B                                                                                                                0.0s
 => [2/6] WORKDIR /app                                                                                                                           0.1s
 => [3/6] COPY requirements.txt .                                                                                                                0.0s
 => [4/6] RUN pip install -r requirements.txt                                                                                                    3.9s
 => [5/6] COPY . .                                                                                                                               0.0s
 => [6/6] RUN useradd --system --no-create-home app                                                                                              0.3s
 => exporting to image                                                                                                                           0.7s
 => => exporting layers                                                                                                                          0.5s
 => => exporting manifest sha256:7189a55c270e8f9c73535044b6bd76d012e11017d9fb01e26f8da841870a0c0d                                                0.0s
 => => exporting config sha256:d34e60a2d4bdc7b72f16e2673fd83a3f058e8d73a3a12c5b368a250170caeb43                                                  0.0s
 => => exporting attestation manifest sha256:171f89e1a4fd243b116baac7aa4798fa03b80160a5dad67f360f825635782aa5                                    0.0s
 => => exporting manifest list sha256:e38ff17f0c938ac71cb8683d8b2527ece5278ac450b1b989173178502d20ee47                                           0.0s
 => => naming to docker.io/library/flask-app:latest                                                                                              0.0s
 => => unpacking to docker.io/library/flask-app:latest                                                                                           0.2s

A etapa [4/6] RUN pip install levou 3,9 segundos e foi a mais demorada do build. Se você construir a imagem novamente sem alterar requirements.txt, essa etapa aparecerá como CACHED.

Execute em segundo plano e teste:

$ docker run -d --name flask -p 8000:8000 flask-app
4d1e3914573d0ef92e9b11840475100237868cb8aaaddefc8f3765a2b5a7c8ce

$ curl -s http://localhost:8000
{"message":"Hello from Flask in Docker"}

$ docker logs flask
[2026-10-13 00:26:21 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2026-10-13 00:26:21 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2026-10-13 00:26:21 +0000] [1] [INFO] Using worker: sync
[2026-10-13 00:26:21 +0000] [6] [INFO] Booting worker with pid: 6
[2026-10-13 00:26:21 +0000] [7] [INFO] Booting worker with pid: 7

$ docker exec flask whoami
app
  • curl retornou JSON, o que indica que o serviço está funcionando. Se você abrir http://localhost:8000 no navegador, verá o mesmo conteúdo.
  • Em docker logs, você verá o processo principal do gunicorn (pid 1) e dois workers (--workers 2).
  • whoami exibe app, confirmando que o processo não está sendo executado como root.

Em geral, defina o número de workers como cerca de 2 a 4 vezes o número de núcleos da CPU. O gunicorn também pode ler o número de workers da variável de ambiente WEB_CONCURRENCY, mas isso só funciona quando --workers não está especificado no comando. O CMD deste Dockerfile já especifica --workers 2, portanto definir WEB_CONCURRENCY não terá efeito (nos testes, continuaram sendo usados 2 workers).

Modo de desenvolvimento: recarregamento automático ao modificar o código

Durante o desenvolvimento, reconstruir a imagem a cada alteração no código é muito lento. Use o Compose para montar a pasta do código no contêiner e use o servidor de desenvolvimento do Flask (--debug recarrega automaticamente quando os arquivos mudam):

compose.yaml:

services:
  web:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    command: ["flask", "--app", "app", "run", "--host", "0.0.0.0", "--port", "8000", "--debug"]

command substitui o CMD do Dockerfile. Assim, o mesmo Dockerfile executa o gunicorn em produção e o servidor de desenvolvimento do Flask durante o desenvolvimento.

$ docker compose up -d

[+] Building 0.6s (13/13) FINISHED
 => [internal] load local bake definitions                                                                                                       0.0s
 => => reading from stdin 468B                                                                                                                   0.0s
 => [internal] load build definition from Dockerfile                                                                                             0.0s
 => => transferring dockerfile: 364B                                                                                                             0.0s
 => [internal] load metadata for docker.io/library/python:3.14-slim                                                                              0.0s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 74B                                                                                                                 0.0s
 => [1/6] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170                        0.0s
 => => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170                        0.0s
 => [internal] load build context                                                                                                                0.0s
 => => transferring context: 297B                                                                                                                0.0s
 => CACHED [2/6] WORKDIR /app                                                                                                                    0.0s
 => CACHED [3/6] COPY requirements.txt .                                                                                                         0.0s
 => CACHED [4/6] RUN pip install -r requirements.txt                                                                                             0.0s
 => CACHED [5/6] COPY . .                                                                                                                        0.0s
 => CACHED [6/6] RUN useradd --system --no-create-home app                                                                                       0.0s
 => exporting to image                                                                                                                           0.3s
 => => exporting layers                                                                                                                          0.0s
 => => exporting manifest sha256:b30dc1eb5dd7e3ae2b911558671243f8e2aeb4eb916b88ba0f4a1b6f59612147                                                0.0s
 => => exporting config sha256:5f7347cc4514246282f5be1a4e1eb3c7cfe90b0ee36c9adeaed934c91bcb2311                                                  0.0s
 => => exporting attestation manifest sha256:2498fb10cf0b98b8be07e54464eaa656528a1fa9324d15544ab7beb474766027                                    0.0s
 => => exporting manifest list sha256:34a36fcb4d656d04ad0e8f86740d02d314d5c68b67b1ff4fb1b897f053bdb39e                                           0.0s
 => => naming to docker.io/library/flask-web:latest                                                                                              0.0s
 => => unpacking to docker.io/library/flask-web:latest                                                                                           0.2s
 => resolving provenance for metadata file                                                                                                       0.0s
[+] up 3/3
 ✔ Image flask-web       Built                                                                                                                    0.7s
 ✔ Network flask_default Created                                                                                                                  0.0s
 ✔ Container flask-web-1 Started                                                                                                                  0.3s

$ curl -s http://localhost:8000
{
  "message": "Hello from Flask in Docker"
}

$ docker compose logs web
web-1  |  * Serving Flask app 'app'
web-1  |  * Debug mode: on
web-1  | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
web-1  |  * Running on all addresses (0.0.0.0)
web-1  |  * Running on http://127.0.0.1:8000
web-1  |  * Running on http://172.19.0.2:8000
web-1  | Press CTRL+C to quit
web-1  |  * Restarting with stat
web-1  |  * Debugger is active!
web-1  |  * Debugger PIN: 127-885-458
web-1  | 172.19.0.1 - - [13/Oct/2026 00:27:50] "GET / HTTP/1.1" 200 -

O WARNING: This is a development server nos logs é normal; ele avisa que o servidor só deve ser usado durante o desenvolvimento. Quando Debug mode: on, o Flask formata automaticamente o JSON retornado em várias linhas.

Agora, use um editor para alterar a mensagem em app.py para Hello again, reloaded! e salve o arquivo. Não é necessário reconstruir a imagem nem reiniciar o contêiner. Faça outra solicitação:

$ curl -s http://localhost:8000
{
  "message": "Hello again, reloaded!"
}

$ docker compose logs --tail 6 web
web-1  | 172.19.0.1 - - [13/Oct/2026 00:26:34] "GET / HTTP/1.1" 200 -
web-1  |  * Detected change in '/app/app.py', reloading
web-1  |  * Restarting with stat
web-1  |  * Debugger is active!
web-1  |  * Debugger PIN: 202-909-220
web-1  | 172.19.0.1 - - [13/Oct/2026 00:26:37] "GET / HTTP/1.1" 200 -

Detected change in '/app/app.py', reloading nos logs indica que o Flask detectou uma alteração no arquivo e o recarregou. Isso acontece porque .:/app monta a pasta local diretamente em /app no contêiner, então o contêiner vê o mesmo arquivo que você está editando.

Quando terminar o desenvolvimento, execute docker compose down para parar e remover o contêiner.

Observação para Windows e macOS: em alguns casos, o compartilhamento de arquivos do Docker Desktop não propaga notificações de alterações nos arquivos. Se o recarregamento automático não funcionar, você pode colocar o projeto no sistema de arquivos do WSL 2 (Windows) ou usar o recurso develop.watch do Compose.

FastAPI

O FastAPI usa o servidor ASGI uvicorn. requirements.txt:

fastapi[standard]>=0.115

fastapi[standard] também instala pacotes comuns, como uvicorn. main.py:

from fastapi import FastAPI

app = FastAPI(title="Todo API")


@app.get("/")
def index():
    return {"message": "Hello from FastAPI"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

O Dockerfile é quase igual ao do Flask. Só o comando de inicialização da última linha muda:

FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Construa a imagem e execute (-q exibe apenas o ID da imagem):

$ docker build -q -t fastapi-app .
sha256:cd8e9d2a7b6db3a54ecab866c502794cc91f11af78efbcb862eeac962566d2ac

$ docker run -d --name fastapi -p 8000:8000 fastapi-app
b40cf8583a166c85dae4560f6356f201de8eb67c919f3fb709964cd635f0b934

$ curl -s "http://localhost:8000/items/42?q=docker"
{"item_id":42,"q":"docker"}

$ docker exec fastapi pip list 2>/dev/null | grep -iE "^(fastapi|uvicorn|pydantic) "
fastapi                                  0.143.0
pydantic                                 2.14.1
uvicorn                                  0.54.0

/items/42?q=docker retorna {"item_id":42,"q":"docker"}; o parâmetro de caminho 42 é convertido automaticamente em inteiro. pip list confirma as versões realmente instaladas no contêiner.

O FastAPI gera a documentação da API automaticamente. Abra http://localhost:8000/docs:

Página da documentação da API no Swagger UI, gerada automaticamente pelo FastAPI
Documentação da API gerada automaticamente pelo FastAPI (/docs); o título vem de FastAPI(title="Todo API")

Durante o desenvolvimento, adicione --reload ao comando uvicorn e monte a pasta do código, como no modo de desenvolvimento do Flask acima. Na produção, para usar vários workers, adicione --workers 4.

Django

Crie um projeto Django com um contêiner

Você não precisa instalar Python nem Django na máquina local; pode gerar o projeto com um contêiner. Primeiro, prepare requirements.txt:

django==6.*
gunicorn==23.*

E um Dockerfile para desenvolvimento:

FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Construa a imagem uma vez (ainda não há um projeto Django). Depois, use essa imagem para executar django-admin startproject e monte a pasta atual para gravar nela os arquivos gerados. Em seguida, construa a imagem novamente para incluir os arquivos do projeto:

$ docker build -q -t django-app .
sha256:1ed89110a11971cd5fdec6e565d389a88ffb106c4fe0093274a4923b8d2fc954

$ docker run --rm -v "$(pwd):/app" -w /app django-app django-admin startproject mysite .

$ ls
Dockerfile
manage.py
mysite
requirements.txt

$ docker build -q -t django-app .
sha256:4f6c035057a7507b0a1a6009c6a67506e58c8c90684e4ec7a5df9300e02ff628

$ docker run -d --name django -p 8000:8000 django-app
a4b4df39bd719fb6bee85a6c4f90cc912586b4c80efce9aa43fc1b76522a9c6f

$ docker exec django python -m django --version
6.1.2

$ docker run --rm django-app python manage.py migrate | tail -n 3
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

No Windows, use assim (clique nas abas para alternar):

docker run --rm -v "$(pwd):/app" -w /app django-app django-admin startproject mysite .
docker run --rm -v "${PWD}:/app" -w /app django-app django-admin startproject mysite .
docker run --rm -v "%cd%:/app" -w /app django-app django-admin startproject mysite .
  • ls mostra os novos manage.py e mysite/, criados pelo django-admin dentro do contêiner. No Linux, esses arquivos pertencerão ao root. Você pode adicionar --user "$(id -u):$(id -g)" para que pertençam a você.
  • python -m django --version exibe 6.1.2.
  • migrate é executado em um contêiner temporário com docker run --rm. Este exemplo usa o SQLite padrão do Django; o arquivo do banco de dados fica no contêiner temporário e desaparece quando ele é removido. Em um projeto real, use PostgreSQL, veja abaixo.

Abra http://localhost:8000 para ver a página de boas-vindas do Django:

Página de boas-vindas após a instalação bem-sucedida do Django 6.1
Página inicial padrão do Django 6.1 (exibida apenas quando DEBUG=True e nenhuma URL foi configurada)

Produção: collectstatic e gunicorn

Em produção, não use runserver: use gunicorn. Os arquivos estáticos (CSS, JS e imagens do admin) precisam ser coletados primeiro com collectstatic em uma mesma pasta e, depois, servidos pelo Nginx ou pelo WhiteNoise. Crie Dockerfile.prod:

FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "mysite.wsgi:application"]

O build direto falha:

$ docker build -f Dockerfile.prod -t django-prod .
... (Traceback omitted)
1.787 django.core.exceptions.ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path.
------
Dockerfile.prod:7
--------------------
   5 |     RUN pip install -r requirements.txt
   6 |     COPY . .
   7 | >>> RUN python manage.py collectstatic --noinput
   8 |     EXPOSE 8000
   9 |     CMD ["gunicorn", "--bind", "0.0.0.0:8000", "mysite.wsgi:application"]
--------------------
ERROR: failed to build: failed to solve: process "/bin/sh -c python manage.py collectstatic --noinput" did not complete successfully: exit code: 1

A mensagem de erro é bem clara: o settings.py gerado por startproject não define STATIC_ROOT, então o collectstatic não sabe para onde coletar os arquivos. Adicione ao final de mysite/settings.py:

STATIC_ROOT = BASE_DIR / "staticfiles"

Faça o build novamente. Desta vez, ele será concluído:

$ docker build -f Dockerfile.prod -t django-prod .

[+] Building 3.7s (11/11) FINISHED                                                                                                     docker:default
 => [internal] load build definition from Dockerfile.prod                                                                                        0.0s
 => => transferring dockerfile: 343B                                                                                                             0.0s
 => [internal] load metadata for docker.io/library/python:3.14-slim                                                                              0.0s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                  0.0s
 => [1/6] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170                        0.0s
 => => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170                        0.0s
 => [internal] load build context                                                                                                                0.0s
 => => transferring context: 3.58kB                                                                                                              0.0s
 => CACHED [2/6] WORKDIR /app                                                                                                                    0.0s
 => CACHED [3/6] COPY requirements.txt .                                                                                                         0.0s
 => CACHED [4/6] RUN pip install -r requirements.txt                                                                                             0.0s
 => [5/6] COPY . .                                                                                                                               0.0s
 => [6/6] RUN python manage.py collectstatic --noinput                                                                                           1.5s
 => exporting to image                                                                                                                           2.1s
 => => exporting layers                                                                                                                          0.1s
 => => exporting manifest sha256:961f8d60c4ec63f3e3e5c41acbdef0b9df6a967dbd806f9d4d178865a5722dd4                                                0.0s
 => => exporting config sha256:8a741faf7acfcad1032b73ec67edbe2717396c8db8fe80c969a3b87903cf0c11                                                  0.0s
 => => exporting attestation manifest sha256:651df3cadbdeb850b8ca4ec2c4cd5a30b77e67f1789ba5bb2ab05c42d17fc717                                    0.0s
 => => exporting manifest list sha256:0e28a34c1ea5ce9838ea3c7204938979ab6a43e7b16d121c059946d6e7a0df01                                           0.0s
 => => naming to docker.io/library/django-prod:latest                                                                                            0.0s
 => => unpacking to docker.io/library/django-prod:latest                                                                                         1.9s

$ docker run -d --name django -p 8000:8000 django-prod
e23efa1aa481b13393faf41f4fb4ff842dee0db8053908846442824dbed97625

$ docker logs django
[2026-10-13 00:28:24 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2026-10-13 00:28:24 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2026-10-13 00:28:24 +0000] [1] [INFO] Using worker: sync
[2026-10-13 00:28:24 +0000] [6] [INFO] Booting worker with pid: 6

$ curl -sI http://localhost:8000/ | head -n 1
HTTP/1.1 200 OK

$ docker exec django ls staticfiles
admin

Os logs mostram que o gunicorn iniciou e que a pasta staticfiles/ contém os arquivos estáticos coletados do admin.

Antes de colocar a aplicação em produção, ajuste também o settings.py: defina DEBUG = False, configure ALLOWED_HOSTS e leia SECRET_KEY de uma variável de ambiente (consulte Variáveis de ambiente).

Migrações do banco de dados

Não coloque migrate em CMD para executá-lo automaticamente a cada inicialização (vários contêineres poderiam executar as migrações ao mesmo tempo). Execute-o uma vez à parte durante a implantação:

docker compose run --rm web python manage.py migrate

Consulte Executar PostgreSQL com Docker para ver uma configuração do Compose com PostgreSQL.

Gerencie dependências com uv

uv é uma ferramenta rápida de gerenciamento de pacotes Python. Use pyproject.toml para declarar as dependências e uv.lock para fixar as versões exatas de todos os pacotes, incluindo as dependências indiretas. Vamos adaptar o exemplo anterior com Flask para usar uv:

pyproject.toml:

[project]
name = "flask-uv"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
    "flask>=3.1",
    "gunicorn>=23",
]

Dockerfile:

FROM python:3.14-slim
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /usr/local/bin/uv

WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-dev --no-install-project

COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-dev

ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
ConfiguraçãoFunção
COPY --from=ghcr.io/astral-sh/uv:0.11Copia o executável do uv diretamente da imagem oficial do uv, sem instalar com pip
--mount=type=cacheMonta um cache do BuildKit. Os pacotes baixados pelo uv são compartilhados entre builds, mas não ficam na imagem
--lockedFalha imediatamente se uv.lock e pyproject.toml forem diferentes, garantindo que as versões instaladas correspondam exatamente ao arquivo de lock
Primeiro sync --no-install-projectInstala primeiro apenas as dependências; essa camada pode ser armazenada em cache. O projeto em si é adicionado na segunda etapa
UV_COMPILE_BYTECODE=1Compila previamente os arquivos .pyc durante a instalação, acelerando a inicialização do contêiner
ENV PATH=...Faz com que comandos como gunicorn usem diretamente a versão do .venv

Gere uv.lock antes e faça commit dele no Git. Se você tiver uv instalado localmente, basta executar uv lock; caso contrário, pode gerá-lo com um contêiner. Atenção: a imagem ghcr.io/astral-sh/uv contém somente o executável uv, sem Python. Executar uv lock nela gera o erro Failed to discover managed Python installations, então use uma imagem Python:

$ docker run --rm -v "$PWD:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"
Using CPython 3.14.8 interpreter at: /usr/local/bin/python3
Resolved 9 packages in 96ms

$ ls -a
.
..
.dockerignore
Dockerfile
app.py
pyproject.toml
uv.lock

$ docker build -t flask-uv .

[+] Building 8.3s (14/14) FINISHED                                                                                                     docker:default
 => [internal] load build definition from Dockerfile                                                                                             0.0s
 => => transferring dockerfile: 497B                                                                                                             0.0s
 => [internal] load metadata for ghcr.io/astral-sh/uv:0.11                                                                                       0.0s
 => [internal] load metadata for docker.io/library/python:3.14-slim                                                                              0.0s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 63B                                                                                                                 0.0s
 => FROM ghcr.io/astral-sh/uv:0.11@sha256:77280f2f771df71f90786c314fe1bbc1e023feac652969bbf139c280babf2eb7                                       0.7s
 => => resolve ghcr.io/astral-sh/uv:0.11@sha256:77280f2f771df71f90786c314fe1bbc1e023feac652969bbf139c280babf2eb7                                 0.0s
 => => extracting sha256:42b502055136e736d5e1d3f5be28fa847276dafc7a55e3fc6bbee14532b41021                                                        0.6s
 => => extracting sha256:95d48f9d48ecf28dfaf61ee94d6692294fd1c4c0527f9d926d6906744b6e3a1d                                                        0.0s
 => [internal] load build context                                                                                                                0.0s
 => => transferring context: 28.88kB                                                                                                             0.0s
 => CACHED [stage-0 1/7] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170         0.0s
 => => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170                        0.0s
 => [stage-0 2/7] COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /usr/local/bin/uv                                                                    0.1s
 => [stage-0 3/7] WORKDIR /app                                                                                                                   0.0s
 => [stage-0 4/7] COPY pyproject.toml uv.lock ./                                                                                                 0.0s
 => [stage-0 5/7] RUN --mount=type=cache,target=/root/.cache/uv     uv sync --locked --no-dev --no-install-project                               2.3s
 => [stage-0 6/7] COPY . .                                                                                                                       0.1s
 => [stage-0 7/7] RUN --mount=type=cache,target=/root/.cache/uv     uv sync --locked --no-dev                                                    0.6s
 => exporting to image                                                                                                                           4.2s
 => => exporting layers                                                                                                                          3.3s
 => => exporting manifest sha256:08d48ef913a933c9b86dfde0768976f21c10d0a39e77e87f14221c2379fa7231                                                0.0s
 => => exporting config sha256:b7444ccd0f3b14cf614f33f8e0e50b47798a97685e3d8fc8afa400731bde86ae                                                  0.0s
 => => exporting attestation manifest sha256:752398dc502698e4bf24a28a5fbfb7fff9940084c5ae815f7687822c96d0cbc5                                    0.0s
 => => exporting manifest list sha256:08accafba065f4996215355e6b25a29c024354ea5853bd31cd1eadc0cb15efb7                                           0.0s
 => => naming to docker.io/library/flask-uv:latest                                                                                               0.0s
 => => unpacking to docker.io/library/flask-uv:latest                                                                                            0.8s

$ docker run -d --name flaskuv -p 8000:8000 flask-uv
6a6fcaf04c1005467f010e1016981c1309fc59f6ed363c29dda8c4f5bd657eed

$ curl -s http://localhost:8000
{"message":"Hello from Flask in Docker"}

$ docker exec flaskuv which gunicorn
/app/.venv/bin/gunicorn

$ docker images flask-uv
IMAGE             ID             DISK USAGE   CONTENT SIZE   EXTRA
flask-uv:latest   08accafba065        266MB         67.8MB   U

Comandos equivalentes no Windows (selecione a aba para alternar):

docker run --rm -v "$PWD:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"
docker run --rm -v "${PWD}:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"
docker run --rm -v "%cd%:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"

which gunicorn mostra /app/.venv/bin/gunicorn, indicando que está sendo usado o ambiente virtual criado pelo uv.

Comparação do tamanho das imagens

$ docker images --filter reference=flask-app --filter reference=fastapi-app --filter reference=django-app --filter reference=python:3.14-slim
IMAGE                ID             DISK USAGE   CONTENT SIZE   EXTRA
django-app:latest    4f6c035057a7        243MB         52.7MB
fastapi-app:latest   cd8e9d2a7b6d        300MB         68.9MB
flask-app:latest     e38ff17f0c93        189MB         45.8MB
python:3.14-slim     a2b82f3c4855        180MB         45.7MB
ImagemA mais que a imagem basePrincipal motivo
flask-appCerca de 9 MBFlask e gunicorn são leves
django-appCerca de 63 MBO Django em si é maior
fastapi-appCerca de 120 MBfastapi[standard] inclui vários pacotes, como uvicorn, pydantic-core, httptools e uvloop

O flask-uv, construído com uv, tem 266 MB, maior que o flask-app, porque inclui o próprio executável uv e os arquivos .pyc gerados por UV_COMPILE_BYTECODE. Se o tamanho for uma preocupação, use builds em vários estágios: instale o uv no primeiro estágio e copie apenas o .venv no segundo. Assim, a imagem final não inclui o uv.

Pacotes que precisam ser compilados

A imagem slim não tem compilador. Se pip install apresentar um erro como gcc: not found ou error: command 'gcc' failed, isso indica que algum pacote não tem um wheel pré-compilado compatível e precisa ser compilado a partir do código-fonte. Há duas formas de fazer isso:

  1. Instale as ferramentas de build (a imagem ficará maior):

    RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
        && rm -rf /var/lib/apt/lists/*
  2. Use builds em vários estágios: instale as ferramentas de compilação e gere os wheels no primeiro estágio; no segundo, instale apenas os wheels. A imagem final não incluirá o compilador.

A maioria dos pacotes populares, como numpy, pandas e psycopg, oferece wheels pré-compilados para Linux, que podem ser instalados diretamente em slim. O alpine usa musl libc e precisa de outro tipo de wheel (musllinux). Alguns pacotes não oferecem esse wheel, então é necessário compilá-los por conta própria, o que aumenta o tempo de build e a chance de falhas. Por isso, projetos Python geralmente recomendam slim em vez de alpine.

Resumo

  • Em produção, use gunicorn (WSGI) ou uvicorn (ASGI), não o servidor de desenvolvimento.
  • Copie primeiro o requirements.txt e depois instale as dependências para aproveitar o cache de build.
  • Durante o desenvolvimento, monte a pasta do código para que o framework o recarregue automaticamente.
  • O collectstatic do Django exige que STATIC_ROOT esteja configurado.

Exercícios

Exercício 1: Altere para 4 o número de workers gunicorn do exemplo Flask sem modificar o Dockerfile.

Resposta

Escreva o comando completo depois do nome da imagem para substituir o CMD:

docker run -d -p 8000:8000 flask-app gunicorn --bind 0.0.0.0:8000 --workers 4 app:app

Use docker logs para confirmar que aparecem 4 linhas Booting worker. Definir apenas -e WEB_CONCURRENCY=4 não funciona, pois o CMD já especifica --workers 2.

Exercício 2: Qual é o endereço da documentação da API do exemplo FastAPI?

Resposta

http://localhost:8000/docs, gerado automaticamente pelo FastAPI.

Perguntas frequentes

Para a imagem Python, devo escolher slim ou alpine?

Recomendamos slim. alpine usa musl libc, e alguns pacotes não têm wheel pré-compilado. Você precisa compilá-los, o que torna o build lento e sujeito a falhas.

A saída de print aparece com atraso em docker logs?

Defina PYTHONUNBUFFERED=1 para que a saída não passe por buffer.

Devo usar pip ou uv?

Os dois funcionam. uv é rápido e usa uv.lock para fixar as versões de todos os pacotes. Neste capítulo, testamos as duas formas.

Atualizado 14 de outubro de 2026