Self-hosting do Ente Photos no homelab com Podman Quadlets
Publicado em 18 de abril de 2026
Este post não é um tutorial. É um registro técnico de uma decisão de arquitetura que funcionou bem no meu contexto: montar um backup secundário de fotos com o Ente Photos em um host Debian que eu já usava no homelab.
O ponto importante aqui é "backup secundário". Minha biblioteca principal continua no iCloud. O que eu queria era uma cópia independente, sob meu controle, em hardware próprio, sem transformar isso em mais um serviço de produção para operar.
TL;DR
- O objetivo era ter um backup secundário para um único usuário de iPhone, sem substituir o iCloud.
- A stack roda em um host Debian já existente, com Podman rootless e systemd Quadlets.
- Não usei
podman-compose, reverse proxy, TLS, SMTP nem replicação. - São quatro containers:
ente-museum,ente-web,ente-minioeente-mc-init. - O Postgres 18 já existia no host e foi compartilhado com a stack do Ente.
- Troquei a ideia inicial de construir imagens customizadas por imagens oficiais fixadas por digest.
- O upload inicial com 275 arquivos funcionou sem intervenção manual.
- O stack sobreviveu a um ciclo completo de
systemctl --user stopseguido destart default.targetsem perda de dados.
Esse enquadramento simplificou quase todas as decisões. Quando a pergunta era "isso realmente precisa existir?", a resposta quase sempre dependia de lembrar que eu não estava montando um produto público, e sim um backup adicional para um cenário pequeno e controlado.
Por que fazer self-hosting de um backup secundário
Eu não estava tentando "sair da Apple". O iCloud Photos continua sendo a cópia principal porque já funciona, a família usa e o comportamento operacional dele já é conhecido. O que me interessava era ter uma segunda cópia com um caminho de dados independente.
Essa definição resolveu várias discussões antes mesmo de virarem trabalho:
- Sem TLS, porque o acesso é somente pela LAN.
- Sem SMTP, porque há um único usuário e ler o OTP nos logs do
museumé aceitável. - Sem reverse proxy, porque não existe superfície pública.
- Sem cliente desktop no Mac, porque isso duplicaria a ingestão de arquivos que o iCloud já espelha.
- Sem replicação, porque a outra cópia já está no iCloud.
Boa parte da arquitetura saiu dessas negações. Dizer "não" cedo economiza mais manutenção do que qualquer ferramenta.
A arquitetura
O host tem duas redes Podman. O ente-museum participa das duas:
ente-netpara falar com MinIO e com o frontend web.pg18-netpara falar com o container existente de Postgres.
Os clientes acessam o IP da LAN do host para tudo:
:8687para a API:3000para a interface de fotos:3002para álbuns públicos:3200para o endpoint S3 do MinIO:3201para o console do MinIO
O desenho geral ficou assim:
Debian host (LAN)
┌───────────────────────────────────────────────┐
│ │
│ pg18 (existente) ente-minio │
│ Postgres 18 S3 em bind-mount │
│ │ │ │
│ └───────┐ ┌──────┘ │
│ │ │ │
│ ente-museum --- ente-mc-init │
│ │ │
│ └──────── ente-web │
│ │
└───────────────────────┬───────────────────────┘
│
▼
iPhone / Safari
Nada disso exigiu orquestração pesada. Era um host, poucas unidades, pouco acoplamento e uma fronteira bem clara entre o que precisava existir e o que seria apenas "produção por reflexo".
Por que Quadlets, e não Compose
Eu já uso Podman rootless para outros serviços nesse host. Colocar podman-compose no meio significaria adicionar uma camada que tenta reinventar o ciclo de vida de serviços por cima do que o systemd já faz melhor.
Com Quadlets, cada arquivo *.container vira uma unidade gerada pelo systemd. Isso me deu o que eu realmente queria:
- ordenação com
After=eWants= - política de reinício consistente
- integração direta com journal
- auto-start com
loginctl enable-linger - menos software rodando no host
Os atritos existiram, mas foram localizados:
Memory=ePodmanArgs=--cpus=Nem[Container]não funcionaram como eu esperava no Podman 5.4.2; o caminho correto foi usarMemoryMax=eCPUQuota=em[Service].EnvironmentFile=em[Service]e em[Container]tem semânticas diferentes, e errar a seção gera um tipo ruim de falha: o container sobe, mas com variáveis vazias.- Adicionar o usuário rootless a grupos suplementares não atualiza automaticamente o
user@UID.service; foi preciso reiniciar o user manager.
Nada disso me fez querer voltar para Compose. São problemas reais, mas limitados, e ficam menores depois que você descobre o formato certo.
A decisão que simplificou o projeto
No plano original, eu tinha uma etapa inteira para construir imagens customizadas a partir de debian:trixie-slim. Em abstrato, isso parecia organizado. Na prática, era manutenção demais para um serviço que existe apenas como cópia secundária.
Trocar isso por imagens oficiais fixadas por digest foi a virada correta:
ghcr.io/ente-io/serverghcr.io/ente-io/webquay.io/minio/minio
Se eu estivesse montando uma plataforma mais exposta, com requisitos mais fortes de reprodutibilidade e hardening, eu provavelmente manteria um pipeline próprio. Para este caso, seria apenas cerimônia.
Fixar as imagens por digest entregou o que importava: imutabilidade suficiente sem eu precisar operar um sistema de build paralelo.
O que eu deliberadamente não construí
As ausências importaram mais do que os componentes presentes.
Sem reverse proxy e sem TLS
A instância é acessível apenas pela rede local, e o ufw restringe as portas relevantes à minha faixa interna de IPs. Se um dia eu quiser acesso remoto, a direção natural não é expor isso publicamente com Let's Encrypt. É colocar uma camada como Tailscale e continuar tratando esse stack como um serviço privado.
Sem SMTP
Com SMTP desabilitado, o museum registra o OTP em log. Para um único usuário, isso é aceitável. Adicionar relay, entrega, DKIM e dependência externa só para um login eventual não fechava a conta.
Sem cliente desktop no Mac
O Mac já recebe a biblioteca via iCloud Photos. Instalar o cliente desktop do Ente seria uma forma elegante de duplicar a ingestão e gastar mais disco sem benefício real.
Sem seed inicial e sem replicação
Eu não tinha uma biblioteca separada fora do fluxo iPhone + iCloud. Fazer um seed manual seria duplicação. Replicação também não fazia sentido porque a redundância principal já existe fora do homelab.
Toda vez que eu removia uma peça, o sistema ficava mais coerente com o problema real.
Hardening no nível certo
O hardening aqui foi propositalmente simples:
- limites de memória e CPU via cgroup nas unidades do systemd
- healthchecks apenas onde a própria imagem já traz cliente HTTP
- observação de logs e rotação padrão do
journald, sem reconfigurar o host por ansiedade - script somente de leitura para detectar drift de digest nas imagens
Os limites definidos ficaram bem acima do consumo observado após o primeiro upload:
| Serviço | RSS observado |
|---|---|
ente-museum |
36 MB |
ente-minio |
174 MB |
ente-web |
5 MB |
Como o host tem folga, o objetivo desses caps não era apertar recursos. Era garantir que um comportamento anômalo batesse na própria cerca antes de pressionar o restante da máquina.
O teste de reinício que eu realmente rodei
Eu não quis reiniciar o host naquele momento porque ele também carrega outros serviços da casa. Em vez disso, fiz o equivalente no espaço do usuário:
- parar todas as unidades
ente-* - confirmar que não havia containers remanescentes
- executar
systemctl --user start default.target
Isso não cobre tudo o que um reboot real cobriria. Não testa inicialização de kernel, driver do HDD externo nem corridas de boot do sistema inteiro. Mas cobre o caminho que mais me importava:
- instalação das unidades
- ordenação entre dependências
- recriação das redes rootless
- bind-mount do armazenamento
- saúde dos containers
- comunicação do
museumcom o MinIO via IP da LAN
O resultado foi o que eu precisava ver: unidades ativas, healthchecks saudáveis, respostas HTTP esperadas e nenhuma divergência nos contadores principais do banco e do storage.
Números que importam
| Dimensão | Valor |
|---|---|
| Containers | 4 do Ente + 1 Postgres existente |
| Upload inicial | 275 arquivos |
| Cota de usuário | 250 GiB |
| Objetos no storage após upload | 4493 |
| Linhas no banco após upload | 2244 |
| Armazenamento externo | /ext_hd_wd/ente/minio/ |
| Segredos | ~/containers/ente/config/{.env,.secrets} com chmod 600 |
Esses números foram mais úteis do que qualquer promessa abstrata de "leve" ou "simples". Eles me deram uma linha de base real para decidir se a implementação estava razoável.
O que eu faria diferente
Se eu fosse repetir esse trabalho, mudaria três coisas:
- escolheria a estratégia de imagem logo no início, antes de gastar energia desenhando um build pipeline desnecessário
- colocaria
EnvironmentFile=no lugar certo desde o primeiro dia - escreveria as notas técnicas enquanto implemento, e não no fim, reconstruindo contexto por
git log
Esse último ponto vale para quase qualquer projeto de infraestrutura pessoal. Documentação retrospectiva funciona, mas custa mais caro e tende a apagar as pequenas decisões que realmente explicam o sistema.
Fechando
O stack final ficou menor do que o plano inicial sugeria. Isso não foi um acidente. Foi o resultado de voltar várias vezes à mesma pergunta: "isso existe porque o meu problema pede, ou porque parece uma coisa profissional para fazer?"
Para este caso, Podman Quadlets encaixaram muito bem. O systemd já administra a máquina. Deixar que ele também administre os containers reduziu software, reduziu acoplamento e deixou a operação mais previsível.
Se existe uma lição aqui, não é "use Ente" ou "use Quadlets". É esta: definir com honestidade o escopo do problema economiza mais tempo do que qualquer otimização técnica feita depois.