Podman na prática: PostgreSQL e Redis para o Nostr Relay Server
- **PostgreSQL** — armazenamento obrigatório (eventos, perfis, NIP-29, Blossom, jobs). - **Redis** — cache e fila de jobs (opcional, mas recomendado em produção). Tudo com **Podman** (rootless), incluindo os equivalentes em `podman run`, rede nomeada, pod, Compose, Quadlet/systemd e as configurações exatas que o projeto espera em `conf.yaml`. > Referências usadas como base: > `docker-compose.yml`, `deploy/postgres/postgresql.conf`, > `deploy/redis/redis.conf`, `deploy/conf.yaml`, `docs/data-schema.md` e > `README.md`. --- ## Sumário 1. [Visão geral](#1-visão-geral) 2. [Pré-requisitos](#2-pré-requisitos) 3. [Estrutura local, segredos e volumes](#3-estrutura-local-segredos-e-volumes) 4. [Subir o PostgreSQL](#4-subir-o-postgresql) 5. [Subir o Redis](#5-subir-o-redis) 6. [Rede, pod e comunicação entre containers](#6-rede-pod-e-comunicação-entre-containers) 7. [Atalho com Podman Compose](#7-atalho-com-podman-compose) 8. [Configurar o relay (`conf.yaml`)](#8-configurar-o-relay-confyaml) 9. [Migrations e primeira subida](#9-migrations-e-primeira-subida) 10. [Operação do dia a dia](#10-operação-do-dia-a-dia) 11. [Backup e restauração](#11-backup-e-restauração) 12. [Persistência, rootless e SELinux](#12-persistência-rootless-e-selinux) 13. [Autostart com Quadlet/systemd](#13-autostart-com-quadletsystemd) 14. [Checklist de segurança](#14-checklist-de-segurança) 15. [Troubleshooting](#15-troubleshooting) --- ## 1. Visão geral Arquitetura mínima em desenvolvimento: ```text ┌──────────────────────┐ ┌───────────────────────────┐ │ nrserver (host ou │ TCP │ Podman (rootless) │ │ container) │───────▶│ ┌─────────────┐ │ │ porta 9090 (relay) │ │ │ PostgreSQL │ :5432 │ │ porta 9091 (admin) │───────▶│ └─────────────┘ │ │ │ │ ┌─────────────┐ │ └──────────────────────┘ │ │ Redis │ :6379 │ │ └─────────────┘ │ └───────────────────────────┘ ``` Portas e nomes usados neste guia (os mesmos do `docker-compose.yml`): | Serviço | Container | Porta publicada | Volume | |------------|---------------------|------------------------|-------------------| | PostgreSQL | `nrserver-postgres` | `127.0.0.1:5432:5432` | `nrserver-pg-data`| | Redis | `nrserver-redis` | `127.0.0.1:6379:6379` | `nrserver-redis-data` | | Relay | `nrserver` | `9090` e `9091` | `conf.yaml` montado | Publicar em `127.0.0.1` (e não em `0.0.0.0`) é intencional: banco e cache não devem ficar expostos na rede local/servidor sem firewall. --- ## 2. Pré-requisitos ### 2.1 Instalar o Podman ```bash # Fedora / RHEL / CentOS Stream sudo dnf install -y podman # Debian / Ubuntu sudo apt update && sudo apt install -y podman # Arch sudo pacman -S --needed podman ``` Opcional, para compatibilidade com comandos `docker ...`: ```bash sudo dnf install -y podman-docker # ou: apt install podman-docker ``` > O `podman-docker` cria um wrapper `docker`; o daemon continua sem existir. > Prefira escrever `podman` para não confundir o restante da equipe. ### 2.2 Verificações ```bash podman --version podman info --format '{{.Host.Security.Rootless}}' # true = rootless podman run --rm docker.io/library/alpine:3.23 echo ok ``` Para Compose (necessário apenas na seção 7): ```bash podman compose version # usa podman-compose ou docker-compose como provider # alternativa explícita: pipx install podman-compose ``` ### 2.3 Imagens usadas pelo projeto ```bash podman pull docker.io/library/postgres:18.4-alpine3.23 podman pull docker.io/library/redis:8.8.0-alpine3.23 ``` As tags acima são as do `docker-compose.yml`. Se preferir fixar por digest: ```bash podman pull docker.io/library/postgres:18.4-alpine3.23 podman image inspect docker.io/library/postgres:18.4-alpine3.23 \ --format '{{.Digest}}' ``` --- ## 3. Estrutura local, segredos e volumes Crie um diretório local (fora do Git) para senhas e arquivos gerados: ```bash mkdir -p .local/podman printf '.local/\n' >> .gitignore # garante que não vai para o repositório ``` Gere a senha do PostgreSQL **em hexadecimal** para evitar caracteres que precisam de URL-encoding dentro de `postgres://...`: ```bash openssl rand -hex 24 > .local/podman/pg-password.txt chmod 600 .local/podman/pg-password.txt ``` Crie os volumes nomeados e a rede: ```bash podman volume create nrserver-pg-data podman volume create nrserver-redis-data podman network create nrserver-net ``` Inspecione o que foi criado: ```bash podman volume ls podman network inspect nrserver-net --format '{{range .Subnets}}{{.Subnet}}{{end}}' ``` > **Por que volume nomeado e não bind mount?** O Podman gerencia permissões, > labels SELinux e o ciclo de vida; além disso, em rootless o volume fica em > `~/.local/share/containers/storage/volumes/...`, sem depender do diretório do > projeto. --- ## 4. Subir o PostgreSQL ### 4.1 Configuração do projeto O arquivo `deploy/postgres/postgresql.conf` já vem com um ajuste razoável para relay (mais memória de cache e WAL do que o default): ```ini listen_addresses = '*' max_connections = 100 shared_buffers = 256MB effective_cache_size = 768MB maintenance_work_mem = 64MB checkpoint_completion_target = 0.9 wal_buffers = 16MB default_statistics_target = 100 random_page_cost = 1.1 effective_io_concurrency = 200 work_mem = 4MB huge_pages = off min_wal_size = 1GB max_wal_size = 4GB ``` Como o relay abre até `db.max_conns` conexões (50 no exemplo do projeto), deixe `max_connections` sempre **acima** desse valor, com margem para manutenção (`psql`, `pg_dump`, migrations): ```text max_connections >= db.max_conns + 10 ``` ### 4.2 Comando `podman run` ```bash podman run -d \ --name nrserver-postgres \ --network nrserver-net \ --restart=unless-stopped \ -p 127.0.0.1:5432:5432 \ -e POSTGRES_DB=nostr \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password \ -e POSTGRES_INITDB_ARGS="--data-checksums" \ -v nrserver-pg-data:/var/lib/postgresql \ -v "$PWD/deploy/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro,Z" \ -v "$PWD/.local/podman/pg-password.txt:/run/secrets/postgres_password:ro,Z" \ --health-cmd "pg_isready -U postgres -d nostr" \ --health-interval 10s \ --health-timeout 5s \ --health-retries 10 \ --health-start-period 10s \ docker.io/library/postgres:18.4-alpine3.23 \ postgres -c config_file=/etc/postgresql/postgresql.conf ``` O que cada parte faz: | Item | Efeito | |------|--------| | `POSTGRES_DB=nostr` | Cria o banco `nostr` no primeiro `initdb`. | | `POSTGRES_USER=postgres` | Define o superusuário. | | `POSTGRES_PASSWORD_FILE` | Lê a senha do arquivo montado, evitando senha em `podman inspect`. | | `POSTGRES_INITDB_ARGS="--data-checksums"` | Habilita checksums de página (detecta corrupção). | | `-v ...:/var/lib/postgresql` | **PostgreSQL 18+**: o `PGDATA` é `.../18/docker`; monte o pai. Em imagens ≤ 17 use `/var/lib/postgresql/data`. | | `config_file=...` | Faz o servidor usar o `postgresql.conf` montado. | | `:ro,Z` | Somente leitura + relabel SELinux do arquivo. | | `--health-cmd` | Healthcheck real (`pg_isready`), usado por Compose e por `podman healthcheck run`. | > **Atenção (PostgreSQL 18):** montar o volume em > `/var/lib/postgresql/data` na imagem 18 faz o `initdb` falhar com diretório > não vazio ou dados em local inesperado. Use `/var/lib/postgresql`. ### 4.3 Primeiro start e validação ```bash podman logs -f nrserver-postgres # em outro terminal: podman healthcheck run nrserver-postgres podman exec -it nrserver-postgres pg_isready -U postgres -d nostr ``` Teste autenticação com senha (lendo do arquivo, sem imprimir no histórico): ```bash PGPASSWORD="$(cat .local/podman/pg-password.txt)" \ podman exec -i -e PGPASSWORD nrserver-postgres \ psql -U postgres -h 127.0.0.1 -d nostr -c 'select version();' ``` Confira os parâmetros efetivos: ```bash podman exec -it nrserver-postgres \ psql -U postgres -d nostr -c 'show shared_buffers; show max_connections;' ``` ### 4.4 Scripts de inicialização (opcional) Arquivos `.sql`, `.sh` ou `.sql.gz` montados em `/docker-entrypoint-initdb.d` rodam **apenas quando o volume está vazio**: ```bash mkdir -p .local/podman/initdb cat > .local/podman/initdb/001-extensions.sql <<'SQL' CREATE EXTENSION IF NOT EXISTS pg_stat_statements; SQL ``` E adicione ao `podman run`: ```bash -v "$PWD/.local/podman/initdb:/docker-entrypoint-initdb.d:ro,Z" ``` > O schema do relay **não** é criado por aqui: ele é versionado em > `infra/db/migrations` e aplicado com `nrserver migrate up` (seção 9). ### 4.5 Alterar senha depois de criado o volume As variáveis `POSTGRES_*` só valem no primeiro `initdb`. Para trocar a senha: ```bash NEW_PASS="$(openssl rand -hex 24)" printf '%s' "$NEW_PASS" > .local/podman/pg-password.txt podman exec -it nrserver-postgres \ psql -U postgres -d nostr \ -c "ALTER USER postgres WITH PASSWORD '$NEW_PASS';" ``` Depois atualize `db.postgres_uri` em `conf.yaml` e reinicie o relay. --- ## 5. Subir o Redis ### 5.1 Configuração do projeto `deploy/redis/redis.conf`: ```ini bind 0.0.0.0 port 6379 protected-mode no appendonly yes save 900 1 save 300 10 save 60 10000 dir /data loglevel warning ``` Pontos importantes: - `bind 0.0.0.0` **dentro** do container é necessário para o port mapping e para outros containers da rede. A exposição externa é controlada pelo `-p 127.0.0.1:6379:6379`. - `protected-mode no` só é aceitável porque o acesso está limitado ao loopback ou à rede interna do Podman. Nunca publique `6379` em `0.0.0.0` sem `requirepass`/ACL e firewall. - `appendonly yes` grava o AOF em `/data` (volume), reduzindo perda de dados. - Os três `save` mantêm snapshots RDB complementares. Versão endurecida com senha e limites de memória: ```ini bind 0.0.0.0 port 6379 protected-mode no appendonly yes appendfsync everysec dir /data loglevel warning # autenticação (Redis 6+ ACL) requirepass TROQUE_ESTA_SENHA # cache + fila de jobs convivem no mesmo Redis: # não use allkeys-lru, pois isso pode descartar streams da fila maxmemory 512mb maxmemory-policy volatile-lru # desconecta clientes ociosos timeout 300 tcp-keepalive 60 ``` > **Por que `volatile-lru` e não `allkeys-lru`?** O relay também usa Redis como > fila (`queue.enabled`), baseada em Streams. `allkeys-lru` pode despejar > mensagens da fila. `volatile-lru` só descarta chaves com TTL — que são > justamente as chaves de cache. ### 5.2 Comando `podman run` (sem senha) ```bash podman run -d \ --name nrserver-redis \ --network nrserver-net \ --restart=unless-stopped \ -p 127.0.0.1:6379:6379 \ -v nrserver-redis-data:/data \ -v "$PWD/deploy/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z" \ --health-cmd "redis-cli ping" \ --health-interval 10s \ --health-timeout 3s \ --health-retries 5 \ docker.io/library/redis:8.8.0-alpine3.23 \ redis-server /usr/local/etc/redis/redis.conf ``` ### 5.3 Comando `podman run` (com senha via variável de ambiente) Crie a senha: ```bash openssl rand -hex 24 > .local/podman/redis-password.txt chmod 600 .local/podman/redis-password.txt ``` Suba com `requirepass` injetado pelo shell do container (a senha não aparece nos argumentos do processo `redis-server`, apenas no ambiente do container): ```bash podman run -d \ --name nrserver-redis \ --network nrserver-net \ --restart=unless-stopped \ -p 127.0.0.1:6379:6379 \ -e REDIS_PASSWORD="$(cat .local/podman/redis-password.txt)" \ -v nrserver-redis-data:/data \ -v "$PWD/deploy/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z" \ --health-cmd 'redis-cli --no-auth-warning -a "$REDIS_PASSWORD" ping' \ --health-interval 10s \ --health-timeout 3s \ --health-retries 5 \ docker.io/library/redis:8.8.0-alpine3.23 \ sh -c 'exec redis-server /usr/local/etc/redis/redis.conf --requirepass "$REDIS_PASSWORD"' ``` E em `conf.yaml`: ```yaml redis: enabled: true addr: "127.0.0.1:6379" password: "a-mesma-senha" db: 0 ``` > A variável ainda aparece em `podman inspect` para quem tem acesso ao usuário > que roda o container. Para não deixá-la no histórico do shell, use > `--env-file .local/podman/redis.env` (permissão `600`) no lugar de `-e`. > A imagem oficial do Redis não suporta `*_FILE`, então `podman secret` só > funciona com um entrypoint wrapper que leia o arquivo e exporte a variável. ### 5.4 Validação ```bash podman logs -f nrserver-redis podman healthcheck run nrserver-redis # sem senha podman exec -it nrserver-redis redis-cli ping # com senha podman exec -it nrserver-redis \ redis-cli --no-auth-warning -a "$(cat .local/podman/redis-password.txt)" ping ``` Comandos úteis de diagnóstico: ```bash podman exec -it nrserver-redis redis-cli INFO server | head -20 podman exec -it nrserver-redis redis-cli INFO memory podman exec -it nrserver-redis redis-cli CONFIG GET maxmemory maxmemory-policy appendonly podman exec -it nrserver-redis redis-cli --scan --pattern 'event:*' | head podman exec -it nrserver-redis redis-cli TTL 'event:ALGUM_ID' podman exec -it nrserver-redis redis-cli DBSIZE ``` Fila de jobs (Streams). As chaves seguem `rq:{<fila>}:...`, com `rq:{default}:stream:{high|normal|low}`, `rq:{default}:dead` e `rq:{default}:jobs`: ```bash podman exec -it nrserver-redis redis-cli XLEN 'rq:{default}:stream:normal' podman exec -it nrserver-redis redis-cli XINFO GROUPS 'rq:{default}:stream:normal' podman exec -it nrserver-redis redis-cli XPENDING 'rq:{default}:stream:normal' nrserver podman exec -it nrserver-redis redis-cli LRANGE 'rq:{default}:dead' 0 9 podman exec -it nrserver-redis redis-cli SLOWLOG GET 10 ``` > `KEYS *` funciona, mas bloqueia o servidor. Use `SCAN`/`--scan` em qualquer > ambiente com dados reais. ### 5.5 Chaves que o relay cria O relay usa Redis como acelerador, com TTLs definidos em `redis.cache.*` (schema completo em `docs/data-schema.md`): | Chave | Tipo | TTL padrão | Campo de configuração | |-------|------|-----------|------------------------| | `ban:{pubkey}` | string | 1h | `cache.ban_ttl` | | `profile:{pubkey}` | hash | 5m | `cache.profile_ttl` | | `query:{hash}` | string | 30s | `cache.query_ttl` | | `query:meta:{hash}` | hash | 30s | `cache.query_meta_ttl` | | `event:{id}` | string | 10m | `cache.event_ttl` | | `dedup:{id}` | string | 1h | `cache.dedup_ttl` | | `nip05:doc` | string | 24h | fixo no código | | `sub:filter:{hash}` | string | 5m | fixo no código | | `ws:last_seen:{id}` | string | 2m | fixo no código | | `nip29:*` | hash/string/list | configurável | `nip29.*_cache_ttl_seconds` | Chaves da fila de jobs (`rq` = relay queue, definidas em `infra/queue/redis/keys.go`): | Chave | Tipo | Papel | |-------|------|-------| | `rq:{<fila>}:stream:{high\|normal\|low}` | stream | Mensagens por prioridade | | `rq:{<fila>}:jobs` | hash | Estado/índice dos jobs | | `rq:{<fila>}:body:<id>` | string | Payload do job (TTL `queue.body_ttl_seconds`) | | `rq:{<fila>}:result:<id>` | string | Resultado (TTL `queue.result_ttl_seconds`) | | `rq:{<fila>}:meta:<id>` | hash | Metadados do job | | `rq:{<fila>}:delayed` | zset | Jobs agendados/retry | | `rq:{<fila>}:dead` | list | Dead-letter | | `rq:{<fila>}:state`, `:attempts`, `:seq` | hash/string | Controle interno | | `rq:{<fila>}:metrics:*`, `:workers:*` | hash | Métricas por minuto (TTL `queue.metrics_ttl_seconds`) | As filas usadas pelo projeto são `default`, `admin` e `cron` (`jobs.default_queue` e `jobs.*.queue`). Por isso o volume do Redis é importante: AOF preserva cache e, principalmente, o estado da fila entre reinícios. --- ## 6. Rede, pod e comunicação entre containers ### 6.1 Rede bridge nomeada (recomendado) O `podman network create nrserver-net` da seção 3 já permite que containers se encontrem pelo nome: ```text postgres:5432 redis:6379 ``` Um container de aplicação na mesma rede usa esses hostnames no `conf.yaml`. Do **host**, use `127.0.0.1:5432` e `127.0.0.1:6379` (portas publicadas). ### 6.2 Podman pod (tudo em `localhost`) Um pod compartilha o namespace de rede. Útil quando o relay também roda em container e você quer um único endereço: ```bash podman pod create --name nrserver-pod \ -p 127.0.0.1:5432:5432 \ -p 127.0.0.1:6379:6379 \ -p 9090:9090 \ -p 127.0.0.1:9091:9091 ``` Depois, em cada `podman run`, troque `--network nrserver-net` por `--pod nrserver-pod` e **remova** os `-p` individuais (as portas são do pod): ```bash podman run -d --pod nrserver-pod --name nrserver-postgres \ -e POSTGRES_DB=nostr -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password \ -v nrserver-pg-data:/var/lib/postgresql \ -v "$PWD/deploy/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro,Z" \ -v "$PWD/.local/podman/pg-password.txt:/run/secrets/postgres_password:ro,Z" \ docker.io/library/postgres:18.4-alpine3.23 \ postgres -c config_file=/etc/postgresql/postgresql.conf podman run -d --pod nrserver-pod --name nrserver-redis \ -v nrserver-redis-data:/data \ -v "$PWD/deploy/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z" \ docker.io/library/redis:8.8.0-alpine3.23 \ redis-server /usr/local/etc/redis/redis.conf ``` Dentro do pod, `127.0.0.1:5432` e `127.0.0.1:6379` funcionam para o relay. Inspecione o pod com: ```bash podman pod ps podman pod inspect nrserver-pod --format '{{.InfraContainerID}}' podman pod stats nrserver-pod ``` ### 6.3 Acessar serviços do host a partir de um container Se o PostgreSQL estiver no host (fora do Podman) e o relay em container: ```bash podman run --add-host host.containers.internal:host-gateway ... # e use host.containers.internal:5432 em db.postgres_uri ``` Alternativa: `--network=host` (perde isolamento; evite em produção). --- ## 7. Atalho com Podman Compose Crie `compose.dev.yaml` (o `*.yaml` já é ignorado pelo `.gitignore`): ```yaml services: postgres: image: docker.io/library/postgres:18.4-alpine3.23 container_name: nrserver-postgres restart: unless-stopped environment: POSTGRES_DB: nostr POSTGRES_USER: postgres POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password POSTGRES_INITDB_ARGS: "--data-checksums" command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] ports: - "127.0.0.1:5432:5432" volumes: - pg_data:/var/lib/postgresql - ./deploy/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro,Z - ./.local/podman/pg-password.txt:/run/secrets/postgres_password:ro,Z healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d nostr"] interval: 10s timeout: 5s retries: 10 start_period: 10s redis: image: docker.io/library/redis:8.8.0-alpine3.23 container_name: nrserver-redis restart: unless-stopped command: ["redis-server", "/usr/local/etc/redis/redis.conf"] ports: - "127.0.0.1:6379:6379" volumes: - redis_data:/data - ./deploy/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 volumes: pg_data: redis_data: ``` Subida e verificação: ```bash podman compose -p nrserver-dev -f compose.dev.yaml up -d podman compose -p nrserver-dev -f compose.dev.yaml ps podman compose -p nrserver-dev -f compose.dev.yaml logs -f postgres redis ``` Para subir a stack completa do repositório (relay + migrations + Tor), use o `docker-compose.yml` oficial — ele já espera `deploy/conf.yaml`: ```bash podman compose -f docker-compose.yml up -d postgres redis podman compose -f docker-compose.yml run --rm nrserver-migrate podman compose -f docker-compose.yml up -d nrserver ``` > Se `podman compose` falhar por falta de provider, instale > `podman-compose` (`pipx install podman-compose`) ou habilite o socket e use o > provider `docker-compose`: > > ```bash > systemctl --user enable --now podman.socket > podman compose version > ``` Notas específicas do Podman: - `restart: unless-stopped` em rootless depende do `podman-restart.service` (`systemctl --user enable --now podman-restart`). Para produção, prefira Quadlet (seção 13). - `:Z` nos bind mounts é obrigatório em hosts com SELinux (Fedora, RHEL). - Use `podman unshare` para inspecionar/altear arquivos de volumes rootless. --- ## 8. Configurar o relay (`conf.yaml`) O binário procura `conf.yaml` no diretório atual, em `../..` e em `/etc/nrs` (`config/config.go`). O container oficial monta em `/app/conf.yaml`. ### 8.1 Exemplo mínimo para desenvolvimento no host ```yaml port: 9090 app_env: development relay_information: url: http://127.0.0.1:9090 name: Nostr Relay Server description: Relay local de desenvolvimento canonical_url: ws://127.0.0.1:9090 db: max_conns: 20 min_conns: 1 max_conn_lifetime_minutes: 30 max_conn_idle_minutes: 5 health_check_period_seconds: 30 postgres_uri: "postgres://postgres:SENHA_HEX@127.0.0.1:5432/nostr?sslmode=disable" redis: enabled: true addr: "127.0.0.1:6379" password: "" db: 0 pool_size: 10 subscription_cleanup_interval_seconds: 60 subscription_stale_after_seconds: 120 cache: ban_ttl: 3600 profile_ttl: 300 query_ttl: 30 query_meta_ttl: 30 event_ttl: 600 dedup_ttl: 3600 queue: enabled: true consumer_group: nrserver worker_count: 2 block_ms: 5000 batch_size: 32 max_len_approx: 100000 body_ttl_seconds: 259200 result_ttl_seconds: 259200 metrics_ttl_seconds: 172800 reclaim_idle_seconds: 120 reclaim_batch_size: 100 promote_batch_size: 100 promote_interval_ms: 1000 recent_jobs_list_limit: 1000 ``` Se o relay também estiver na rede do Podman/Compose, troque para os hostnames: ```yaml db: postgres_uri: "postgres://postgres:SENHA_HEX@postgres:5432/nostr?sslmode=disable" redis: addr: "redis:6379" ``` ### 8.2 Campos do PostgreSQL | Campo | Papel | Observação | |-------|-------|------------| | `postgres_uri` | DSN completo | Obrigatório. Formato `postgres://user:pass@host:port/db?sslmode=...` | | `max_conns` | Conexões máximas do pool | Deve ser menor que `max_connections` do servidor | | `min_conns` | Conexões mantidas | `1` é suficiente para dev | | `max_conn_lifetime_minutes` | Idade máxima da conexão | Recicla conexões atrás de PgBouncer/proxy | | `max_conn_idle_minutes` | Tempo ocioso máximo | Libera recursos em ociosidade | | `health_check_period_seconds` | Intervalo do ping | `30s` padrão | **Codificação da senha:** caracteres como `@`, `:`, `/`, `#` e `%` precisam de URL-encoding dentro da URI (`@` → `%40`, `:` → `%3A`, `/` → `%2F`). Usar senha hexadecimal (`openssl rand -hex 24`) evita o problema por completo. **TLS:** com `sslmode=disable` o tráfego é texto puro — aceitável em loopback ou rede interna. Em rede compartilhada, configure TLS no PostgreSQL e use `sslmode=verify-full` com `sslrootcert`. ### 8.3 Campos do Redis | Campo | Papel | |-------|-------| | `enabled` | Liga/desliga todo o uso de Redis; `false` mantém o relay funcionando só com PostgreSQL | | `addr` | `host:porta` | | `password` | Senha (`requirepass`/ACL); vazio se não houver | | `db` | Índice do banco lógico (0–15 por padrão) | | `pool_size` | Conexões no pool | | `subscription_cleanup_interval_seconds` | Frequência da limpeza de inscrições | | `subscription_stale_after_seconds` | Idade para considerar inscrição abandonada | | `cache.*` | TTLs por tipo de chave (ver tabela 5.5) | | `queue.*` | Fila de jobs em Streams: grupo, workers, batch, retenção e reclaim | > `redis.enabled: false` é suportado, mas você perde cache de consultas e a > fila de jobs. Em produção com tráfego real, mantenha habilitado. ### 8.4 Segurança do arquivo ```bash chmod 600 conf.yaml grep -n 'postgres_uri\|priv_key\|admin_token' conf.yaml # revise antes de commitar ``` Nunca versione `conf.yaml` real: ele já está no `.gitignore` do projeto. --- ## 9. Migrations e primeira subida O schema **não** é criado automaticamente pelo PostgreSQL; ele é versionado em `infra/db/migrations` e embutido no binário. ### 9.1 Binário local ```bash # com as variáveis apontando para os containers ./nrserver migrate status ./nrserver migrate up ``` Ou via Go: ```bash go run ./cmd/nrserver migrate up ``` ### 9.2 Com a imagem do projeto ```bash podman build -t nostr-relay-server:local . podman run --rm \ --network nrserver-net \ -v "$PWD/conf.yaml:/app/conf.yaml:ro,Z" \ nostr-relay-server:local migrate up ``` ### 9.3 Subir o relay ```bash ./nrserver server # ou: podman run -d --name nrserver \ --network nrserver-net \ -p 9090:9090 -p 127.0.0.1:9091:9091 \ -v "$PWD/conf.yaml:/app/conf.yaml:ro,Z" \ nostr-relay-server:local ``` ### 9.4 Validar a stack ```bash curl -i -H 'Accept: application/nostr+json' http://127.0.0.1:9090/ curl -i http://127.0.0.1:9090/.well-known/nostr.json curl -i http://127.0.0.1:9091/metrics curl -i http://127.0.0.1:9091/panel ``` Esperado: - `GET /` sem o header Nostr retorna `426 Upgrade Required` (normal). - `/.well-known/nostr.json` retorna JSON com `names`. - `/metrics` expõe as métricas Prometheus. - `/panel` serve o painel administrativo embutido. Confirme que o relay está gravando no banco e usando o cache: ```bash podman exec -it nrserver-postgres psql -U postgres -d nostr -c '\dt' podman exec -it nrserver-postgres psql -U postgres -d nostr -c 'select count(*) from event;' podman exec -it nrserver-redis redis-cli DBSIZE ``` --- ## 10. Operação do dia a dia ```bash # estado podman ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' podman stats --no-stream nrserver-postgres nrserver-redis podman healthcheck run nrserver-postgres podman healthcheck run nrserver-redis # logs podman logs --tail 100 nrserver-postgres podman logs -f nrserver-redis # shell podman exec -it nrserver-postgres psql -U postgres -d nostr podman exec -it nrserver-redis redis-cli # ciclo de vida podman stop nrserver-postgres nrserver-redis podman start nrserver-postgres nrserver-redis podman restart nrserver-redis # inspeção podman inspect nrserver-postgres --format '{{json .Config.Env}}' podman volume inspect nrserver-pg-data --format '{{.Mountpoint}}' podman port nrserver-postgres ``` Monitoramento rápido do PostgreSQL: ```bash podman exec -it nrserver-postgres psql -U postgres -d nostr -c ' select state, count(*) from pg_stat_activity where datname = $$nostr$$ group by state;' podman exec -it nrserver-postgres psql -U postgres -d nostr -c ' select relname, n_live_tup from pg_stat_user_tables order by n_live_tup desc limit 10;' podman exec -it nrserver-postgres psql -U postgres -d nostr -c ' select pg_size_pretty(pg_database_size($$nostr$$));' ``` Ajuste de parâmetros sem recriar o container (persistente no volume): ```bash podman exec -it nrserver-postgres psql -U postgres -d nostr -c \ 'alter system set work_mem = $$8MB$$;' podman exec -it nrserver-postgres psql -U postgres -d nostr -c \ 'select pg_reload_conf();' ``` > `shared_buffers`, `max_connections` e `wal_buffers` exigem **restart** > (`podman restart nrserver-postgres`). Estatísticas e manutenção: ```bash podman exec -it nrserver-postgres psql -U postgres -d nostr -c 'VACUUM (ANALYZE);' podman exec -it nrserver-postgres psql -U postgres -d nostr -c 'REINDEX DATABASE nostr;' ``` O relay já agenda `VACUUM/ANALYZE`/`REINDEX` em `cron.db_optimization`; use os comandos acima apenas para manutenção manual pontual. --- ## 11. Backup e restauração ### 11.1 PostgreSQL — dump lógico ```bash podman exec nrserver-postgres \ pg_dump -U postgres -Fc -d nostr > "backup-nostr-$(date +%F-%H%M).dump" ``` Restaurar em um banco vazio (ou recriado): ```bash podman exec -i nrserver-postgres \ pg_restore -U postgres -d nostr --clean --if-exists < backup-nostr-XXXX.dump ``` Ou em SQL puro: ```bash podman exec nrserver-postgres \ pg_dump -U postgres -d nostr | gzip > nostr-$(date +%F).sql.gz ``` ### 11.2 PostgreSQL — base física (parar o serviço) ```bash podman stop nrserver-postgres podman run --rm \ -v nrserver-pg-data:/from:ro \ -v "$PWD/.local/backup:/to" \ docker.io/library/alpine:3.23 \ sh -c 'tar czf /to/pg-data-$(date +%F).tgz -C /from .' podman start nrserver-postgres ``` ### 11.3 Redis ```bash # força um snapshot podman exec nrserver-redis redis-cli BGSAVE sleep 2 podman cp nrserver-redis:/data/dump.rdb ./.local/backup/dump.rdb # restaurar podman stop nrserver-redis podman cp ./.local/backup/dump.rdb nrserver-redis:/data/dump.rdb podman start nrserver-redis ``` Com AOF habilitado, copie também `appendonlydir/`: ```bash podman cp nrserver-redis:/data/appendonlydir ./.local/backup/appendonlydir ``` > Redis é cache/fila, não fonte da verdade. Perder o volume não corrompe o > relay: o pior efeito é perda de jobs pendentes e cache frio. --- ## 12. Persistência, rootless e SELinux ### 12.1 Onde ficam os dados ```bash podman volume inspect nrserver-pg-data --format '{{.Mountpoint}}' podman volume inspect nrserver-redis-data --format '{{.Mountpoint}}' ``` Rootless: sob `~/.local/share/containers/storage/volumes/...`, cujo acesso direto pelo host é limitado pelo user namespace. Use `podman unshare`: ```bash podman unshare ls -la \ "$(podman volume inspect nrserver-pg-data --format '{{.Mountpoint}}')" ``` ### 12.2 SELinux Em hosts com SELinux, bind mount sem label gera `Permission denied`: ```bash # correto -v "$PWD/deploy/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z" # diagnóstico ls -Z deploy/redis/redis.conf podman logs nrserver-redis sudo ausearch -m avc -ts recent ``` `Z` aplica label privado (só aquele container acessa); `z` aplica label compartilhado. Para arquivos de configuração de um único serviço, use `Z`. ### 12.3 Portas privilegiadas Rootless não publica portas < 1024 por padrão. Alternativas: ```bash # 1) aumentar o mínimo permitido sudo sysctl net.ipv4.ip_unprivileged_port_start=80 # 2) redirecionar com firewall/socat/proxy reverso para a porta alta ``` O relay usa `9090/9091`, então isso normalmente não é um problema. ### 12.4 Limites de recurso ```bash podman run ... \ --memory 2g --memory-swap 2g \ --cpus 2 \ --pids-limit 512 \ ... ``` Para PostgreSQL, não limite memória abaixo de `shared_buffers + (work_mem × max_connections)`; caso contrário o OOM killer derruba o processo. --- ## 13. Autostart com Quadlet/systemd `podman generate systemd` está depreciado. O caminho atual é **Quadlet** (Podman 4.4+), que transforma arquivos declarativos em units systemd. Diretório (rootless): ```bash mkdir -p ~/.config/containers/systemd ``` > Substitua `/CAMINHO/ABSOLUTO/nostr-relay-server` pelo caminho real do clone > neste host. Em Quadlet, `%h` expande para o home do usuário, mas caminhos > absolutos são mais previsíveis. ### 13.1 Rede `~/.config/containers/systemd/nrserver.network`: ```ini [Network] Subnet=10.89.10.0/24 ``` ### 13.2 PostgreSQL `~/.config/containers/systemd/nrserver-postgres.container`: ```ini [Unit] Description=PostgreSQL para o Nostr Relay Server Wants=network-online.target After=network-online.target [Container] Image=docker.io/library/postgres:18.4-alpine3.23 ContainerName=nrserver-postgres Network=nrserver.network PublishPort=127.0.0.1:5432:5432 Environment=POSTGRES_DB=nostr Environment=POSTGRES_USER=postgres Environment=POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password Environment=POSTGRES_INITDB_ARGS=--data-checksums Volume=nrserver-pg-data:/var/lib/postgresql Volume=/CAMINHO/ABSOLUTO/nostr-relay-server/deploy/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro,Z Volume=/CAMINHO/ABSOLUTO/nostr-relay-server/.local/podman/pg-password.txt:/run/secrets/postgres_password:ro,Z Exec=postgres -c config_file=/etc/postgresql/postgresql.conf HealthCmd=pg_isready -U postgres -d nostr HealthInterval=10s HealthTimeout=5s HealthRetries=10 HealthStartPeriod=10s [Service] Restart=always [Install] WantedBy=default.target ``` ### 13.3 Redis `~/.config/containers/systemd/nrserver-redis.container`: ```ini [Unit] Description=Redis para o Nostr Relay Server Wants=network-online.target After=network-online.target [Container] Image=docker.io/library/redis:8.8.0-alpine3.23 ContainerName=nrserver-redis Network=nrserver.network PublishPort=127.0.0.1:6379:6379 Volume=nrserver-redis-data:/data Volume=/CAMINHO/ABSOLUTO/nostr-relay-server/deploy/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z Exec=redis-server /usr/local/etc/redis/redis.conf [Service] Restart=always [Install] WantedBy=default.target ``` > Se você habilitar `requirepass`, o healthcheck precisa de autenticação. > Adicione `Environment=REDIS_PASSWORD=...` e use > `HealthCmd=sh -c 'redis-cli --no-auth-warning -a "$REDIS_PASSWORD" ping'`. ### 13.4 Ativar ```bash systemctl --user daemon-reload systemctl --user enable --now nrserver-postgres.service systemctl --user enable --now nrserver-redis.service systemctl --user status nrserver-postgres.service journalctl --user -u nrserver-postgres -f ``` Para os serviços subirem sem sessão aberta (boot do servidor), habilite o linger: ```bash loginctl enable-linger "$USER" ``` > Os arquivos `.container` geram units com o mesmo nome do arquivo. Alterações > exigem `systemctl --user daemon-reload`. --- ## 14. Checklist de segurança - [ ] PostgreSQL e Redis publicados apenas em `127.0.0.1` (ou sem `PublishPort`, acessíveis só pela rede do Podman). - [ ] Senhas fortes e distintas, em arquivos `chmod 600` ou `podman secret`. - [ ] `POSTGRES_PASSWORD_FILE` em vez de `POSTGRES_PASSWORD` na linha de comando. - [ ] `requirepass`/ACL no Redis sempre que houver mais de um container na rede. - [ ] `maxmemory-policy volatile-lru` (ou `noeviction`) quando a fila estiver ativa. - [ ] `conf.yaml` com `chmod 600` e fora do Git (já ignorado). - [ ] Imagens fixadas por tag/digest; `podman auto-update` só com política definida. - [ ] Usuário não-root no host, rootless Podman. - [ ] Backup automatizado do PostgreSQL (dump diário + retenção). - [ ] `journalctl`/logs com rotação; disco do volume monitorado. - [ ] `sslmode=disable` apenas em loopback/rede interna confiável. - [ ] Migrations aplicadas com `nrserver migrate up` antes de trocar o binário. --- ## 15. Troubleshooting | Sintoma | Causa provável | Correção | |---------|----------------|----------| | `initdb: directory "/var/lib/postgresql/data" exists but is not empty` | Volume no caminho errado para PG 18 | Monte em `/var/lib/postgresql` | | `role "postgres" does not exist` | Volume já inicializado com outro `POSTGRES_USER` | Recrie o volume ou use `createuser`/`ALTER ROLE` | | `password authentication failed for user "postgres"` | Senha com caractere especial não escapado na URI, ou senha antiga no volume | Use `%40`/`%3A`/`%2F` ou senha hex; se necessário `ALTER USER` | | Variável `POSTGRES_PASSWORD` alterada não tem efeito | Só vale no primeiro `initdb` | `ALTER USER postgres WITH PASSWORD ...` | | `Permission denied` ao montar `redis.conf` | SELinux | Adicione `:ro,Z` ao bind mount | | `Permission denied` em arquivos do volume rootless | User namespace | `podman unshare chown -R 0:0 <mountpoint>` | | `NOAUTH Authentication required` no relay | Redis com `requirepass`, `conf.yaml` sem `redis.password` | Preencha `redis.password` | | `MISCONF Redis is configured to save RDB snapshots...` | Disco cheio ou `/data` sem permissão de escrita | Libere espaço; garanta o volume em `/data` | | `OOM`/container do Postgres morto | Limite de memória baixo | Aumente `--memory` ou reduza `shared_buffers`/`work_mem` | | `bind: address already in use` | Porta ocupada no host | `ss -ltnp \| grep -E '5432\|6379'` e ajuste o `-p` | | Relay não conecta no banco | `127.0.0.1` usado de dentro de container | Use `postgres:5432` (rede) ou `host.containers.internal` | | `too many connections` | `db.max_conns` ≥ `max_connections` | Aumente `max_connections` ou reduza o pool | | Redis perde jobs após restart | AOF desligado ou volume ausente | `appendonly yes` + volume em `/data` | | Healthcheck do Redis falha com senha | `redis-cli ping` sem auth | Use `-a "$REDIS_PASSWORD" --no-auth-warning` | Diagnóstico geral: ```bash podman inspect nrserver-postgres --format '{{.State.Health.Status}}' podman events --filter container=nrserver-postgres --since 30m podman system df podman volume ls --format '{{.Name}}\t{{.Mountpoint}}' journalctl --user -u nrserver-postgres -n 100 --no-pager ``` --- ## Referências no repositório | Arquivo | Conteúdo | |---------|----------| | `docker-compose.yml` | Stack de referência (postgres, redis, relay, tor) | | `deploy/postgres/postgresql.conf` | Tuning do PostgreSQL usado em produção | | `deploy/redis/redis.conf` | Configuração do Redis (AOF + snapshots) | | `deploy/conf.yaml` | Exemplo de configuração com hostnames internos | | `conf.yaml` | Configuração local/desenvolvimento | | `config/conf-data.go` | Structs de `db` e `redis` (todos os campos válidos) | | `docs/data-schema.md` | Schema SQL e chaves/TTLs do Redis | | `README.md` | Requisitos, endpoints e execução com Docker | | `infra/db/migrations/` | Migrations aplicadas por `nrserver migrate up` |
Referendum Propositions
2 Optionsopt_no
opt_yes
Cryptographic & Protocol Identifiers
Chamber Participation
• Democratic Parity: 1 DID : 1 Vote
• Mutation Mode: Immutable Ledger
• Verification: Dual-Layer Schnorr + Ed25519