Self-hosting do Ente Photos no homelab com Podman Quadlets

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-minio e ente-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 stop seguido de start default.target sem 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-net para falar com MinIO e com o frontend web.
  • pg18-net para falar com o container existente de Postgres.

Os clientes acessam o IP da LAN do host para tudo:

  • :8687 para a API
  • :3000 para a interface de fotos
  • :3002 para álbuns públicos
  • :3200 para o endpoint S3 do MinIO
  • :3201 para 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= e Wants=
  • 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= e PodmanArgs=--cpus=N em [Container] não funcionaram como eu esperava no Podman 5.4.2; o caminho correto foi usar MemoryMax= e CPUQuota= 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/server
  • ghcr.io/ente-io/web
  • quay.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:

  1. parar todas as unidades ente-*
  2. confirmar que não havia containers remanescentes
  3. 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 museum com 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.

← Voltar para todos os posts