惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
Spread Privacy
Spread Privacy
D
Docker
Stack Overflow Blog
Stack Overflow Blog
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
F
Full Disclosure
美团技术团队
Y
Y Combinator Blog
N
Netflix TechBlog - Medium
Security Latest
Security Latest
C
Check Point Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
罗磊的独立博客
A
Arctic Wolf
S
Schneier on Security
T
Threatpost
C
CERT Recently Published Vulnerability Notes
L
LangChain Blog
博客园 - 叶小钗
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 聂微东
T
The Exploit Database - CXSecurity.com
W
WeLiveSecurity
Engineering at Meta
Engineering at Meta
C
Cybersecurity and Infrastructure Security Agency CISA
GbyAI
GbyAI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
H
Heimdal Security Blog
L
LINUX DO - 热门话题
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
O
OpenAI News
G
GRAHAM CLULEY
M
MIT News - Artificial intelligence
S
Security @ Cisco Blogs
博客园 - 司徒正美
N
News and Events Feed by Topic
Microsoft Azure Blog
Microsoft Azure Blog
Cisco Talos Blog
Cisco Talos Blog
P
Palo Alto Networks Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Schneier on Security
Schneier on Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
月光博客
月光博客
The Last Watchdog
The Last Watchdog
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
C
Cisco Blogs
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Podman Rootless em Produção: Substituindo Docker
Lincoln Zoca · 2026-05-11 · via DEV Community

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