DEV Community

Cover image for Podman Rootless em Produção: Substituindo Docker
Lincoln Zocateli
Lincoln Zocateli

Posted on • Originally published at zocate.li

Podman Rootless em Produção: Substituindo Docker

Introdução

Podman rootless em produção deixou de ser uma curiosidade técnica e virou uma exigência prática em ambientes que precisam atender a controles de compliance como CIS Benchmarks, PCI-DSS e LGPD. Quando rodo containers Docker em um servidor, o dockerd precisa de um socket privilegiado e qualquer usuário no grupo docker é, na prática, root na máquina. Em auditorias internas eu vi esse ponto barrar deploys inteiros, e a resposta mais limpa que encontrei nos últimos dois anos foi migrar para Podman rodando sem daemon e sem privilégios.

A tese deste artigo é direta: Podman rootless substitui Docker em produção sem perda funcional na maioria dos cenários web e de workers, desde que você aceite três trocas explícitas — usar systemd (via Quadlet) no lugar de docker compose, mapear UIDs com subuid/subgid, e tratar redes e volumes pensando em namespaces de usuário. Em troca, o servidor passa a ser auditável: nenhum processo de container roda como root no host, o blast radius de uma RCE cai drasticamente e a superfície de ataque do daemon simplesmente some.

Vou mostrar como saí de um host Docker tradicional para um host Podman 5.x rootless rodando workloads .NET, n8n e Postgres em produção, qual é a estrutura de diretórios que sobreviveu a três meses de operação, como fica o pipeline de deploy via SSH e quais armadilhas custam horas se você não souber onde olhar (porta 80, linger, pasta networking, SELinux). Tudo testado em Ubuntu 24.04 LTS e RHEL 9 com crun como runtime padrão.

Pré-requisitos

  • Linux com kernel ≥ 5.13 (Ubuntu 22.04+, Debian 12+, RHEL 9+)
  • Podman ≥ 4.4 (para suporte completo a Quadlet); idealmente 5.x
  • Usuário não-root com subuid/subgid configurados
  • systemd em modo user habilitado (loginctl enable-linger <user>)
  • Conhecimento básico de Dockerfile e docker compose

Por Que Rootless Não É “Docker com Outro Nome”

A confusão mais comum que encontro é tratar Podman como um drop-in de Docker apenas porque alias docker=podman funciona. Funciona para run, build e pull, mas o modelo de execução é radicalmente diferente.

Docker depende de um daemon (dockerd) que escuta em /var/run/docker.sock rodando como root. Esse daemon é quem executa os containers. Se um atacante consegue qualquer escrita no socket, ele consegue um container --privileged montando / do host — game over. É por isso que CIS Docker Benchmark 1.2.x dedica uma seção inteira ao socket.

Podman é daemonless. Cada podman run é um processo filho do seu shell (ou do systemd --user). Não existe socket privilegiado por padrão. Em modo rootless, o container roda dentro de um user namespace mapeado via subuid/subgid: o UID 0 dentro do container é, na verdade, um UID alto e sem privilégios no host (ex: 100000). Se o processo escapa do container, ele aterrissa como um usuário comum sem permissão de escrita em quase nada.

ℹ️ Informação: Em testes do Red Hat com CVE-2019-5736 (escape clássico do runc), o exploit em Docker rootful conseguia sobrescrever o binário do runc no host. Em Podman rootless o mesmo exploit aterrissa como UID 100000 sem permissão de escrita — o container “escapa”, mas não causa dano.

Essa diferença muda como você pensa em backups, logs e orquestração. Logs de container rootless vão para o journal do usuário, não para /var/log/. Backups de volumes ficam em ~/.local/share/containers/storage/volumes/. E systemctl restart precisa do flag --user.

Configurando o Host: subuid, subgid e Linger

A configuração base do host é o ponto onde 80% dos problemas aparecem em produção. Sem ela, o container até sobe, mas reinicia depois do logout, perde acesso a portas, ou falha em montar volumes com o erro confuso lchown: operation not permitted.

# Criar usuário dedicado para a aplicação
sudo useradd -m -s /bin/bash app

# Reservar 65536 UIDs e GIDs subordinados para o usuário 'app'
# Sem isso, podman rootless falha ao mapear o UID do container
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 app

# Habilitar systemd --user mesmo sem sessão SSH ativa
# Essencial para containers em produção que precisam sobreviver ao logout
sudo loginctl enable-linger app

# Permitir bind em portas baixas (80/443) sem CAP_NET_BIND_SERVICE
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-podman.conf
sudo sysctl --system
Enter fullscreen mode Exit fullscreen mode

O loginctl enable-linger é o comando que mais esqueço — sem ele, qualquer systemctl --user start morre quando a sessão SSH cai. A linha ip_unprivileged_port_start=80 evita ter que recorrer a setcap ou proxy reverso só para servir HTTP.

⚠️ Atenção: Se você reaproveitar UIDs subordinados que já estão alocados a outro usuário, o Podman vai recusar o run com IDs already in use. Sempre rode cat /etc/subuid /etc/subgid antes de adicionar ranges.

Quadlet: o Substituto Real do docker-compose

docker-compose foi a peça mais difícil de substituir na minha migração. podman-compose existe, mas é mantido pela comunidade e tem inconsistências em redes e healthchecks. A solução oficial é Quadlet, integrado ao Podman 4.4+.

Quadlet permite descrever containers, redes e volumes em arquivos .container, .network e .volume que o systemd traduz automaticamente em units. Isso significa: restart policies, dependências (After=, Requires=), logs no journal, healthchecks supervisionados pelo systemd. Tudo grátis, sem dockerd.

A localização para units rootless é ~/.config/containers/systemd/. O systemd --user faz parsing toda vez que você roda systemctl --user daemon-reload.

# ~/.config/containers/systemd/api.container
[Unit]
Description=API .NET 9 em Podman rootless
After=network-online.target

[Container]
Image=ghcr.io/lincoln/api:1.4.0
AutoUpdate=registry
ContainerName=api
PublishPort=8080:8080
Environment=ASPNETCORE_ENVIRONMENT=Production
Volume=api-data.volume:/data:Z
Network=apps.network
HealthCmd=curl -f http://localhost:8080/health || exit 1
HealthInterval=30s

[Service]
Restart=always
TimeoutStartSec=900

[Install]
WantedBy=default.target
Enter fullscreen mode Exit fullscreen mode

Após criar o arquivo, basta systemctl --user daemon-reload && systemctl --user start api.service. O systemd cria a unit, baixa a imagem, sobe o container e monitora o healthcheck.

Migrando docker-compose.yml para Quadlet

Para projetos com vários serviços, traduzir manualmente é viável, mas há atalhos. A ferramenta podlet converte um docker-compose.yml em arquivos Quadlet em segundos:

# Instalar podlet (binário Go, sem dependências)
curl -L -o /tmp/podlet.tar.gz \
  https://github.com/containers/podlet/releases/latest/download/podlet-x86_64-unknown-linux-gnu.tar.gz
tar -xzf /tmp/podlet.tar.gz -C ~/.local/bin/

# Converter compose existente para Quadlet
podlet --file ~/.config/containers/systemd/ compose docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

A conversão acerta volumes, redes, dependências e portas em ~90% dos casos. Os 10% restantes geralmente são depends_on com condition: service_healthy (Quadlet usa Requires= + After=) e extra_hosts (que vira AddHost= no bloco [Container]).

💡 Dica: Mantenha o docker-compose.yml original no repositório por enquanto, com um README apontando para os arquivos Quadlet. Devs locais continuam usando Docker Desktop; o servidor de produção usa Podman/Quadlet. Os dois descrevem a mesma topologia.

Rede, Volumes e SELinux: As Três Pegadinhas

Rede em modo rootless

Por padrão, Podman rootless usa pasta (em versões 5.x) ou slirp4netns (versões anteriores) como backend de rede. Funciona, mas tem limitações: o IP de origem do tráfego visto pelo container é sempre 10.0.2.100 (com slirp4netns), o que quebra rate-limiting e logs de auditoria baseados em IP. Com pasta, o IP real é preservado, e por isso eu sempre forço Podman 5.x em produção nova.

Volumes e o flag :Z

Em hosts com SELinux (RHEL, Fedora, Rocky), montar um volume sem o flag :Z no Quadlet ou -v ./data:/data:Z no run causa Permission denied silencioso dentro do container. O :Z aplica a label SELinux correta no diretório do host. Sem ele, debugging vira pesadelo porque ls -l mostra permissões corretas, mas o processo do container não consegue ler.

Mapeamento de UID em volumes

Quando o container escreve num volume bind-mounted, o arquivo no host aparece com UID 100000 (o UID subordinado mapeado). Para sincronizar com seu usuário local, use podman unshare:

# Entrar no namespace de usuário do podman
podman unshare chown -R 1000:1000 /home/app/data

# Equivalente a "chown 100999:100999" visto do host
Enter fullscreen mode Exit fullscreen mode

Configuração Otimizada: containers.conf, registries.conf, storage.conf

Os defaults do Podman funcionam, mas em produção rootless há três arquivos que decidem performance e segurança operacional. Em modo rootless, ficam em ~/.config/containers/. Esta é a configuração que rodo num RHEL 9.7 com disco dedicado para /userapps e SSD limitado em IOPS — ela elimina escritas temporárias massivas e usa journald em RAM como sink de log.

containers.conf — runtime e logging

# ~/.config/containers/containers.conf
# Referência: man 5 containers.conf

[containers]
tz = "America/Sao_Paulo"

# Driver de log: journald armazena em RAM via journal, não em arquivo no disco
log_driver = "journald"
log_size_max = 10485760    # 10 MB por container

# Limite de PIDs por container (mitigação de fork bomb)
pids_limit = 2048

[engine]
# TMPDIR em /dev/shm (tmpfs em RAM) elimina milhões de writes temporários no disco
env = ["TMPDIR=/dev/shm/podman-tmp"]

# Eventos do Podman vão para journald, não para arquivos no disco
events_logger = "journald"

[network]
# Netavark é o backend moderno (substitui CNI desde Podman 4.x)
network_backend = "netavark"
network_config_dir = "/userapps/zocateli/containers/networks"
Enter fullscreen mode Exit fullscreen mode

O ganho prático mais visível é o TMPDIR=/dev/shm/...: em servidor com muitos podman pull e podman build, o disco recebia milhões de arquivos temporários que sumiam segundos depois. Movendo para tmpfs, o I/O do disco caiu drasticamente sem perder funcionalidade.

registries.conf — anti supply-chain

# ~/.config/containers/registries.conf
[registries.search]
registries = ['meu-docker.artifacts.zocate.li']

# Recusa qualquer "podman run nginx" sem prefixo de registry
short-name-mode = "enforcing"
Enter fullscreen mode Exit fullscreen mode

O short-name-mode = "enforcing" é um controle subestimado: sem ele, podman run nginx pode resolver para qualquer registry da lista de busca, abrindo brecha para supply-chain attack. Com enforcing, o usuário precisa especificar o registry completo (docker.io/library/nginx) — força que toda imagem seja explicitamente atribuída a uma fonte.

storage.conf — overlay nativo (60–80% menos I/O)

# ~/.config/containers/storage.conf
# Referência: man 5 containers-storage.conf

[storage]
driver = "overlay"

# Storage persistente (imagens, layers) → disco dedicado
graphroot = "/userapps/zocateli/containers/storage"
rootless_storage_path = "/userapps/zocateli/containers/storage"

# runroot (PIDs, sockets, locks) é resolvido automaticamente via $XDG_RUNTIME_DIR
# que aponta para /run/user/<UID>/containers (tmpfs em RAM) — não definir aqui

[storage.options]
pull_options = {enable_partial_images = "false", use_hard_links = "false"}

[storage.options.overlay]
# SEM mount_program → Podman usa overlay NATIVO do kernel (5.11+)
# Habilitar fuse-overlayfs apenas se overlay nativo não funcionar:
# mount_program = "/usr/bin/fuse-overlayfs"

# SEM metacopy=on → evita copy-up overhead em workloads write-heavy
mountopt = "nodev"
Enter fullscreen mode Exit fullscreen mode

O ponto crítico é a ausência de mount_program. Em kernel 5.11+ (RHEL 9, Ubuntu 22.04+, Fedora 35+), o overlay nativo suporta rootless sem fuse-overlayfs. Cortar o FUSE elimina syscalls de userspace para cada operação de filesystem — em workloads I/O-bound, vejo 60–80% de redução no tempo de operações de layer (build, pull, run de imagens grandes).

⚠️ Atenção: Se o backing filesystem for ext4 sem suporte a overlay rootless, ou kernel < 5.11, descomente mount_program = "/usr/bin/fuse-overlayfs". Sem essa fallback, o podman pull vai falhar com mount: permission denied. Para detectar suporte: podman info | grep graphDriverName deve mostrar overlay, não vfs.

Exemplo Prático: API .NET + Postgres + Worker em Produção

Um caso real que rodo hoje: API .NET 9, worker de processamento e Postgres 17, todos em Podman rootless, supervisionados por systemd --user. Estrutura:

~/.config/containers/systemd/
├── apps.network
├── postgres.volume
├── postgres.container
├── api.container
└── worker.container
Enter fullscreen mode Exit fullscreen mode
# postgres.container — banco persistente
[Unit]
Description=Postgres 17
After=network-online.target

[Container]
Image=docker.io/library/postgres:17-alpine
ContainerName=postgres
Environment=POSTGRES_PASSWORD_FILE=/run/secrets/pg_pass
Secret=pg_pass,type=mount,target=pg_pass
Volume=postgres.volume:/var/lib/postgresql/data:Z
Network=apps.network
HealthCmd=pg_isready -U postgres
HealthInterval=10s

[Service]
Restart=always

[Install]
WantedBy=default.target
Enter fullscreen mode Exit fullscreen mode
# api.container — depende do Postgres saudável
[Unit]
Description=API .NET 9
Requires=postgres.service
After=postgres.service

[Container]
Image=ghcr.io/lincoln/api:1.4.0
AutoUpdate=registry
ContainerName=api
PublishPort=8080:8080
Environment=ConnectionStrings__Db=Host=postgres;Username=postgres;Password=...
Network=apps.network

[Service]
Restart=always

[Install]
WantedBy=default.target
Enter fullscreen mode Exit fullscreen mode

Deploy é um git pull && systemctl --user restart api.service. Sem daemon, sem sudo, sem socket aberto. O update da imagem é orquestrado por podman auto-update (habilitado com AutoUpdate=registry no Quadlet) que checa registry diariamente e reinicia containers automaticamente em caso de nova tag.

📂 Código Fonte: Os manifests Quadlet completos e o Containerfile do worker estão no repositório de exemplos do blog:
BlogSamples/Workers/

Auto-Update na Prática: Quadlet .image, Timer e o Que .network/.volume Não Fazem

O podman auto-update é disparado pelo timer podman-auto-update.timer do systemd --user (diário por padrão). Ele varre todas as units com a label io.containers.autoupdate e:

  • Faz skopeo inspect no registry para comparar o digest atual com o publicado.
  • Se mudou, faz podman pull da nova imagem.
  • Reinicia o serviço systemd correspondente.
  • Se o restart falhar (healthcheck negativo), faz rollback automático para a imagem anterior. Habilitar o timer:
systemctl --user enable --now podman-auto-update.timer
systemctl --user list-timers | grep auto-update
Enter fullscreen mode Exit fullscreen mode

Mudando a cadência para 5 em 5 minutos

O timer default roda uma vez por dia (OnCalendar=daily + RandomizedDelaySec=900). Para deploys mais agressivos, dá para baixar para 5 minutos sem editar arquivos do pacote — basta criar um override do systemd user:

systemctl --user edit podman-auto-update.timer
Enter fullscreen mode Exit fullscreen mode

Adicione apenas:

# ~/.config/systemd/user/podman-auto-update.timer.d/override.conf
[Timer]
# Reset OnCalendar herdado do timer original
OnCalendar=
OnCalendar=*:0/5
# Sem janela aleatória — queremos previsibilidade no deploy
RandomizedDelaySec=0
# Recupera execuções perdidas (ex: máquina hibernando)
Persistent=true
Enter fullscreen mode Exit fullscreen mode

Recarregue e confirme:

systemctl --user daemon-reload
systemctl --user restart podman-auto-update.timer
systemctl --user list-timers podman-auto-update.timer
Enter fullscreen mode Exit fullscreen mode

⚠️ Atenção: Os dois OnCalendar= são intencionais. A primeira linha vazia limpa o valor herdado do timer original (daily). Sem ela, o timer dispara nas duas cadências — diariamente e a cada 5 minutos. Esse é o padrão de override de timers do systemd documentado em systemd.timer(5).

Tradeoffs com 15 containers no mesmo host

Reduzir o intervalo de 1 dia para 5 minutos significa multiplicar o número de execuções por 288×. Para 15 containers compartilhando o mesmo host, vale revisar quatro pontos antes de decidir:

Eixo 1× por dia 1× a cada 5 minutos (15 containers)
Requisições ao registry 15/dia 4.320/dia
Tráfego se nada mudou ~3 KB/inspect × 15 ~12 MB/dia (manifest HEAD + JSON)
Restart simultâneo 1 janela/dia 288 janelas/dia
Janela de exposição a CVE até 24h até 5 min

1. Rate limit do registry. Docker Hub anônimo: 100 pulls/6h por IP. Cada podman auto-update faz skopeo inspect (não conta como pull, mas conta como request) — 15 containers × 12 verificações/h = 180 req/h. Em GHCR autenticado o limite é 5.000 req/h, sem dor. Em registry interno, geralmente irrelevante. Em Docker Hub anônimo com muitos containers, vai bater rate limit.

2. Tempestade de restart simultâneo. Quando um digest novo é publicado e múltiplos containers da stack dependem dele (ex: API + worker compartilhando a mesma base image), o auto-update reinicia todos juntos. Se o healthcheck demora 30s e há Requires= em cadeia, a stack inteira pode ficar 1–2 minutos degradada — só que agora 288 vezes por dia em vez de 1. Sem load balancer com drain, isso é downtime visível.

3. CPU/IO no host. Cada execução é leve isoladamente (~50ms por container, sem mudança). Para 15 containers a cada 5 min, o overhead agregado fica em torno de 0,3% de CPU constante e ~10 MB extras de tráfego de log no journald. Aceitável em produção, mas em hosts com muitos containers (50+), considere RandomizedDelaySec=60 para evitar picos coincidentes.

4. Risco de rollback em cascata. O auto-update faz rollback por container, não atômico para a stack. Se o digest novo da imagem da API quebra o contrato com o worker (que ainda está na versão antiga), 5 minutos depois o worker também pega a nova versão e quebra junto. Em janela diária dá tempo de detectar; em janela de 5 minutos, não.

💡 Dica: Para 15 containers em produção real eu uso 15 minutos (OnCalendar=*:0/15) com RandomizedDelaySec=300, e ainda assim só em containers que cumprem dois critérios: tag semver imutável + healthcheck rápido (< 10s) + zero dependência de outro container. Para o resto, mantenho cadência diária e faço deploy explícito via CI quando preciso de janela menor. A cadência de 5 minutos faz sentido em pré-produção/canary, raramente em produção heterogênea.

A chave AutoUpdate= aceita dois valores no bloco [Container]:

Valor Quando usar
registry Pull do registry remoto. Tag precisa ser versão imutável (semver, calendar ou digest).
local Imagem foi rebuilt localmente (podman build). Compara digest local.

Quadlet .image — separar pull do start

Quando várias .container units usam a mesma imagem (ex: API + sidecar de migration sharing a mesma base), vale extrair o pull para um arquivo .image dedicado. O auto-update opera no arquivo de imagem e os containers consomem por referência:

# ~/.config/containers/systemd/api-image.image
[Image]
Image=ghcr.io/lincoln/api:1.4.0
AutoUpdate=registry
Enter fullscreen mode Exit fullscreen mode
# api.container — consome a image unit
[Container]
Image=api-image.image    # referência, não duplica a definição
Enter fullscreen mode Exit fullscreen mode

.network e .volume não suportam AutoUpdate=

Essa é a confusão mais comum: a chave AutoUpdate= não existe em .network nem .volume. Eles não fazem pull de nada — são recursos estáticos do Podman, reconciliados quando você edita o arquivo e roda systemctl --user daemon-reload seguido do restart do serviço que os usa.

Um exemplo concreto migrando o serviço cloudbeaver do meu docker-compose.yml (parte da stack de dev do blog) para Quadlet:

# Antes — docker-compose.yml
cloudbeaver:
  image: lzocateli/dbeaver:25.2.0
  ports:
    - "8978:8978"
  volumes:
    - C:/Users/lzoca/cloudbeaver:/opt/cloudbeaver/workspace
  networks:
    - lzo
  depends_on:
    postgres:
      condition: service_healthy
Enter fullscreen mode Exit fullscreen mode

Vira três arquivos Quadlet, cada um com sua responsabilidade:

# ~/.config/containers/systemd/lzo.network
[Network]
NetworkName=lzo
Driver=bridge
Label=app=blog-stack
# Sem AutoUpdate — rede é reconciliada via daemon-reload
Enter fullscreen mode Exit fullscreen mode
# ~/.config/containers/systemd/cloudbeaver-workspace.volume
[Volume]
VolumeName=cloudbeaver-workspace
Label=app=blog-stack

# Bind para diretório no host (equivalente ao volume do docker-compose)
# Em Linux/WSL2 esse path é /mnt/c/Users/lzoca/cloudbeaver
Driver=local
Options=type=none,o=bind
Device=C:/Users/lzoca/cloudbeaver

# Sem AutoUpdate — volume é reconciliado via daemon-reload
Enter fullscreen mode Exit fullscreen mode
# ~/.config/containers/systemd/cloudbeaver.container
[Unit]
Description=CloudBeaver — gerenciador de banco
Requires=postgres.service
After=postgres.service

[Container]
Image=docker.io/lzocateli/dbeaver:25.2.0
AutoUpdate=registry            # ← única unit que faz pull
ContainerName=cloudbeaver
PublishPort=8978:8978
Environment=TZ=America/Sao_Paulo
Volume=cloudbeaver-workspace.volume:/opt/cloudbeaver/workspace:Z
Network=lzo.network
HealthCmd=curl -f http://localhost:8978/ || exit 1
HealthInterval=30s

[Service]
Restart=unless-stopped

[Install]
WantedBy=default.target
Enter fullscreen mode Exit fullscreen mode

Quando o cloudbeaver recebe nova versão no Docker Hub (lzocateli/dbeaver:25.2.0 republicada com novo digest), o timer faz pull, restart e healthcheck — o lzo.network e o cloudbeaver-workspace.volume continuam intocados. Se eu editar o lzo.network para mudar driver, preciso systemctl --user daemon-reload && systemctl --user restart cloudbeaver.service manualmente — não existe “auto-update de network”.

Rollback manual

Quando o auto-update sobe uma versão quebrada e o healthcheck demora a falhar:

podman auto-update rollback     # volta para a imagem anterior em todas as units
systemctl --user restart cloudbeaver.service
Enter fullscreen mode Exit fullscreen mode

💡 Dica: Para máxima reprodutibilidade, troque a tag por digest pinning no .image: Image=docker.io/lzocateli/dbeaver@sha256:abc…. O auto-update continua funcionando (ele compara digest publicado vs digest atual) e você ganha imutabilidade garantida pelo conteúdo da imagem, não pelo contrato da tag.

Compliance: O Que Auditoria Realmente Olha

O argumento de venda interno mais forte do Podman rootless não é técnico — é regulatório. Em três auditorias que acompanhei nos últimos 18 meses, os pontos atendidos automaticamente foram:

  • CIS Docker Benchmark 5.x — controles de “non-root user inside container” e “no privileged containers” passam por construção
  • PCI-DSS 4.0 req. 7.2.5 — least privilege para componentes de sistema é satisfeito porque o container nunca tem privilégio no host
  • NIST SP 800-190 seção 4.2 — isolamento de container via user namespaces é recomendação explícita
  • ISO 27001 A.8.2 — controle de acesso privilegiado fica trivial de evidenciar quando não há grupo docker na máquina A evidência prática que as auditorias pedem é simples: ps -ef | grep -v podman | grep <container-pid> mostrando processo rodando com UID alto, e getent group docker retornando vazio.

Dicas e Boas Práticas

  • Sempre habilitar linger antes de criar units systemd --user. Sem isso, todos os containers morrem no primeiro logout e você só descobre na próxima manhã quando o monitoramento gritar.
  • Usar Podman 5.x para ter pasta como backend de rede padrão. O ganho de preservar IP de origem evita reescrever regras de rate-limit, WAF e logs.
  • Versionar os arquivos Quadlet em Git junto do código da aplicação. Tratar *.container como infra-as-code: pull request, code review e CI validando sintaxe via quadlet -dryrun.
  • Habilitar AutoUpdate=registry apenas com tags versão imutáveis. Default: semver (:1.4.0, :2.0.1) — alinha com SemVer 2.0, encaixa em SBOM/SCA e é o que a maioria dos registries de mercado já publica. Variante: calendar versioning (:v2026.05.10-sha) para apps internas com release diário. Gold standard: digest pinning (Image=ghcr.io/lincoln/api@sha256:abc…) — imutabilidade garantida pelo conteúdo, não pelo contrato. Nunca :latest com auto-update: é convite para incidente.
  • Centralizar logs com journald + vector ou promtail. Logs rootless ficam em journalctl --user -u <service>. Encaminhar para Loki/Elastic é trivial e mais barato que docker logging driver.
  • Rodar podman system reset em homologação semanalmente. Limpa storage corrompido, volumes órfãos e overlay layers obsoletos. Fica óbvio quando alguma unit está com state inconsistente.

Resumo Objetivo

  • Podman rootless elimina o daemon root e o socket privilegiado do Docker, reduzindo o blast radius de uma RCE em container para o de um usuário comum no host.
  • Quadlet é a substituição oficial para docker-compose em produção: arquivos .container/.network/.volume em ~/.config/containers/systemd/ viram units do systemd --user automaticamente.
  • subuid/subgid + loginctl enable-linger são pré-requisitos não-negociáveis: sem eles o container não mapeia UIDs ou morre no primeiro logout.
  • Backend pasta (Podman 5.x) preserva o IP de origem do cliente, ao contrário do slirp4netns que mascara tudo como 10.0.2.100.
  • Flag :Z em volumes aplica label SELinux correta em hosts RHEL/Fedora; sem ele, container falha em ler/escrever sem mensagem clara.
  • CIS Docker Benchmark, PCI-DSS 4.0 e NIST SP 800-190 têm controles satisfeitos automaticamente por Podman rootless que exigem trabalho manual em Docker.
  • podman auto-update com tags versão imutáveis (semver :1.4.0, calendar :v2026.05.10-sha ou digest @sha256:…) habilita atualização contínua sem CI, supervisionada por timer do systemd. As chaves AutoUpdate=registry|local só existem no bloco [Container] ou em arquivos .image — não em .network ou .volume.
  • podlet compose docker-compose.yml converte ~90% dos manifests Compose existentes para Quadlet automaticamente, reduzindo o esforço de migração.

Leia Também

  • Keycloak: Autenticação Grátis com Container e C#
  • Configuração .NET 8+: IOptions, Secrets e Docker
  • .NET Worker e Background Service: Alto Volume
  • Makefile: Automatizando tarefas para Python, Hugo e Docker
  • Terminal vs Pipeline: diferenças ao executar scripts

Referências

  • Podman Documentation — Rootless Containers — referência oficial para configuração e limitações do modo rootless.
  • Quadlet Manual (podman-systemd.unit) — especificação completa dos arquivos .container, .network, .volume, .kube e .pod.
  • CIS Docker Benchmark v1.6.0 — controles de hardening para Docker e equivalentes em Podman.
  • NIST SP 800-190 — Application Container Security Guide — guia oficial dos EUA sobre segurança de containers, com foco em user namespaces.
  • Red Hat — Podman vs Docker: A Detailed Comparison — comparativo técnico aprofundado mantido pela equipe que desenvolve Podman.
  • Containers/Podlet GitHub — ferramenta oficial para converter docker-compose.yml em Quadlet.
  • pasta(1) — passt/pasta networking — backend de rede usado por Podman 5.x para preservar IP de origem em modo rootless. 📬

👉 Artigo completo com todos os exemplos de código: Podman Rootless em Produção: Substituindo Docker

Top comments (0)