From 5a19753013dadabcf9c2bcb30df4f74248507807 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 23:34:14 +0000 Subject: [PATCH] feat: split into 3 docker-compose stacks (main / gitea / wedding_photo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repo passa a viver dentro de infra/, com três stacks isoladas que compartilham uma network Docker externa ('infra-net'): main/ infra base: postgres + redis + minio + minio-init + pgadmin + caddy. Postgres roda em modo multi-banco; o init script cria os DBs 'wedding' e 'gitea' com roles dedicadas. MinIO tem um bucket inicial criado pelo minio-init com anonymous download + CORS. Caddy é o único container expondo 80/443 e roteia por hostname: gitea.{DOMAIN_BASE} / wedding.* / pgadmin.* / minio.* / media.* (rewrite de bucket). gitea/ gitea + act_runner. Gitea liga no postgres compartilhado e usa redis pra cache+sessões. O runner ganha um Dockerfile pequeno que adiciona docker CLI por cima do gitea/act_runner pra workflows poderem chamar 'docker build'. Bootstrap do token de runner documentado no .env.example. wedding_photo/ Só a aplicação: 'wedding_app' (FastAPI + SPA) + postgres-backup + media-backup. Os bancos e o MinIO vêm da stack main/. A app usa extra_hosts: host-gateway pra alcançar media.{DOMAIN_BASE} via Caddy mesmo em dev local — assim a assinatura S3 fecha com o host que o browser usa pra fazer PUT. Orquestração: - Makefile no root: 'make up' sobe tudo na ordem (main -> gitea -> wedding_photo). 'make up-{main,gitea,wedding}' pra controle granular. 'make logs-*', 'make down', 'make status', 'make pull-*'. - network.sh cria a 'infra-net' antes de qualquer up; idempotente. - Cada stack tem seu próprio .env.example. As creds compartilhadas (DOMAIN_BASE, MINIO_ROOT_*, WEDDING_DB_*) precisam casar entre main/.env e o consumidor (gitea/.env ou wedding_photo/.env). - .gitignore ignora todas as pastas data/ dos volumes. --- .env.example | 57 ------- .gitignore | 15 +- Caddyfile | 25 --- Makefile | 94 ++++++++--- docker-compose.yml | 150 ------------------ infra/gitea/.env.example | 32 ++++ infra/gitea/docker-compose.yml | 67 ++++++++ infra/gitea/runner/Dockerfile | 7 + infra/main/.env.example | 41 +++++ infra/main/caddy/Caddyfile | 50 ++++++ infra/main/docker-compose.yml | 121 ++++++++++++++ .../{minio-cors.json => main/minio/cors.json} | 0 infra/main/minio/init.sh | 13 ++ infra/main/pgadmin/servers.json | 13 ++ .../main/postgres/init/01-create-databases.sh | 17 ++ .../wedding_photo/.dockerignore | 0 infra/wedding_photo/.env.example | 53 +++++++ .npmrc => infra/wedding_photo/.npmrc | 0 Dockerfile => infra/wedding_photo/Dockerfile | 0 infra/wedding_photo/Makefile | 26 +++ .../wedding_photo/apps}/api/app/__init__.py | 0 .../wedding_photo/apps}/api/app/config.py | 0 .../apps}/api/app/db/__init__.py | 0 .../wedding_photo/apps}/api/app/db/base.py | 0 .../wedding_photo/apps}/api/app/db/migrate.py | 0 .../wedding_photo/apps}/api/app/db/models.py | 0 .../apps}/api/app/lib/__init__.py | 0 .../wedding_photo/apps}/api/app/lib/auth.py | 0 .../wedding_photo/apps}/api/app/lib/ids.py | 0 .../wedding_photo/apps}/api/app/lib/pdf.py | 0 .../apps}/api/app/lib/qrcode_gen.py | 0 .../apps}/api/app/lib/storage.py | 0 .../apps}/api/app/lib/transcode.py | 0 .../wedding_photo/apps}/api/app/main.py | 0 .../apps}/api/app/migrations/0001_initial.sql | 0 .../apps}/api/app/routes/__init__.py | 0 .../apps}/api/app/routes/admin.py | 0 .../apps}/api/app/routes/public.py | 0 .../apps}/api/app/routes/uploads.py | 0 .../apps}/api/app/schemas/__init__.py | 0 .../apps}/api/app/schemas/api.py | 0 .../wedding_photo/apps}/api/pyproject.toml | 0 .../wedding_photo/apps}/web/index.html | 0 .../wedding_photo/apps}/web/package.json | 0 .../wedding_photo/apps}/web/postcss.config.js | 0 .../wedding_photo/apps}/web/src/App.tsx | 0 .../wedding_photo/apps}/web/src/index.css | 0 .../wedding_photo/apps}/web/src/lib/api.ts | 0 .../wedding_photo/apps}/web/src/lib/upload.ts | 0 .../wedding_photo/apps}/web/src/main.tsx | 0 .../apps}/web/src/routes/Gallery.tsx | 0 .../apps}/web/src/routes/Home.tsx | 0 .../apps}/web/src/routes/Upload.tsx | 0 .../apps}/web/src/routes/admin/Dashboard.tsx | 0 .../apps}/web/src/routes/admin/Login.tsx | 0 .../apps}/web/tailwind.config.ts | 0 .../wedding_photo/apps}/web/tsconfig.json | 0 .../wedding_photo/apps}/web/vite.config.ts | 0 infra/wedding_photo/docker-compose.yml | 81 ++++++++++ .../infra}/backup/media-backup-entrypoint.sh | 0 .../infra}/backup/media-backup-run.sh | 0 .../wedding_photo/package.json | 0 .../packages}/shared/package.json | 0 .../packages}/shared/src/index.ts | 0 .../packages}/shared/src/schemas.ts | 0 .../packages}/shared/src/types.ts | 0 .../packages}/shared/tsconfig.json | 0 .../wedding_photo/pnpm-lock.yaml | 0 .../wedding_photo/pnpm-workspace.yaml | 0 .../wedding_photo/tsconfig.base.json | 0 network.sh | 11 ++ 71 files changed, 616 insertions(+), 257 deletions(-) delete mode 100644 .env.example delete mode 100644 Caddyfile delete mode 100644 docker-compose.yml create mode 100644 infra/gitea/.env.example create mode 100644 infra/gitea/docker-compose.yml create mode 100644 infra/gitea/runner/Dockerfile create mode 100644 infra/main/.env.example create mode 100644 infra/main/caddy/Caddyfile create mode 100644 infra/main/docker-compose.yml rename infra/{minio-cors.json => main/minio/cors.json} (100%) create mode 100755 infra/main/minio/init.sh create mode 100644 infra/main/pgadmin/servers.json create mode 100755 infra/main/postgres/init/01-create-databases.sh rename .dockerignore => infra/wedding_photo/.dockerignore (100%) create mode 100644 infra/wedding_photo/.env.example rename .npmrc => infra/wedding_photo/.npmrc (100%) rename Dockerfile => infra/wedding_photo/Dockerfile (100%) create mode 100644 infra/wedding_photo/Makefile rename {apps => infra/wedding_photo/apps}/api/app/__init__.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/config.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/db/__init__.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/db/base.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/db/migrate.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/db/models.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/lib/__init__.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/lib/auth.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/lib/ids.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/lib/pdf.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/lib/qrcode_gen.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/lib/storage.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/lib/transcode.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/main.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/migrations/0001_initial.sql (100%) rename {apps => infra/wedding_photo/apps}/api/app/routes/__init__.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/routes/admin.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/routes/public.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/routes/uploads.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/schemas/__init__.py (100%) rename {apps => infra/wedding_photo/apps}/api/app/schemas/api.py (100%) rename {apps => infra/wedding_photo/apps}/api/pyproject.toml (100%) rename {apps => infra/wedding_photo/apps}/web/index.html (100%) rename {apps => infra/wedding_photo/apps}/web/package.json (100%) rename {apps => infra/wedding_photo/apps}/web/postcss.config.js (100%) rename {apps => infra/wedding_photo/apps}/web/src/App.tsx (100%) rename {apps => infra/wedding_photo/apps}/web/src/index.css (100%) rename {apps => infra/wedding_photo/apps}/web/src/lib/api.ts (100%) rename {apps => infra/wedding_photo/apps}/web/src/lib/upload.ts (100%) rename {apps => infra/wedding_photo/apps}/web/src/main.tsx (100%) rename {apps => infra/wedding_photo/apps}/web/src/routes/Gallery.tsx (100%) rename {apps => infra/wedding_photo/apps}/web/src/routes/Home.tsx (100%) rename {apps => infra/wedding_photo/apps}/web/src/routes/Upload.tsx (100%) rename {apps => infra/wedding_photo/apps}/web/src/routes/admin/Dashboard.tsx (100%) rename {apps => infra/wedding_photo/apps}/web/src/routes/admin/Login.tsx (100%) rename {apps => infra/wedding_photo/apps}/web/tailwind.config.ts (100%) rename {apps => infra/wedding_photo/apps}/web/tsconfig.json (100%) rename {apps => infra/wedding_photo/apps}/web/vite.config.ts (100%) create mode 100644 infra/wedding_photo/docker-compose.yml rename infra/{ => wedding_photo/infra}/backup/media-backup-entrypoint.sh (100%) rename infra/{ => wedding_photo/infra}/backup/media-backup-run.sh (100%) rename package.json => infra/wedding_photo/package.json (100%) rename {packages => infra/wedding_photo/packages}/shared/package.json (100%) rename {packages => infra/wedding_photo/packages}/shared/src/index.ts (100%) rename {packages => infra/wedding_photo/packages}/shared/src/schemas.ts (100%) rename {packages => infra/wedding_photo/packages}/shared/src/types.ts (100%) rename {packages => infra/wedding_photo/packages}/shared/tsconfig.json (100%) rename pnpm-lock.yaml => infra/wedding_photo/pnpm-lock.yaml (100%) rename pnpm-workspace.yaml => infra/wedding_photo/pnpm-workspace.yaml (100%) rename tsconfig.base.json => infra/wedding_photo/tsconfig.base.json (100%) create mode 100755 network.sh diff --git a/.env.example b/.env.example deleted file mode 100644 index ff70192..0000000 --- a/.env.example +++ /dev/null @@ -1,57 +0,0 @@ -# ============================================================================= -# Edite essas variáveis e salve como .env (mesmo diretório do docker-compose.yml) -# ============================================================================= - -# ----- Domínios (apontar DNS A/AAAA pra IP do VPS antes do `docker compose up`) -DOMAIN=casamento.exemplo.com.br -MEDIA_DOMAIN=midia.exemplo.com.br -ACME_EMAIL=voce@exemplo.com - -# ----- Postgres -POSTGRES_USER=wedding -POSTGRES_PASSWORD=troque-isso-por-uma-senha-forte -POSTGRES_DB=wedding - -# ----- MinIO (root credentials = também usadas pelo app como S3 access keys) -MINIO_ROOT_USER=minioadmin -MINIO_ROOT_PASSWORD=troque-isso-por-uma-senha-forte -S3_BUCKET=wedding-media -S3_REGION=us-east-1 -# Endpoint INTERNO usado pelo app pra falar com o MinIO. Use o do Caddy -# (https://${MEDIA_DOMAIN}) — necessário porque a assinatura S3 inclui o host -# e o browser faz upload direto pra MEDIA_DOMAIN. Hairpin via DNS público é OK. -# Pra evitar hairpin, ajuste extra_hosts no compose pra resolver MEDIA_DOMAIN -# pra IP interno e set S3_INTERNAL_ENDPOINT=http://minio:9000 (avançado). -S3_INTERNAL_ENDPOINT= - -# ----- App -COUPLE_NAMES=Stefanie & Leandro -EVENT_DATE=07 de junho de 2026 - -# ----- Admin (painel dos noivos) -ADMIN_PASSWORD=senha-forte-compartilhada-entre-os-dois -SESSION_SECRET=gere-um-uuid-longo-aqui-min-32-chars -ALLOWED_ADMIN_EMAILS=voce@exemplo.com,outro@exemplo.com - -# ----- Backup (vai pra ./backups/ no host por padrão) -TZ=America/Sao_Paulo -# Cron schedule para o Postgres: aceita formato cron OR @daily/@hourly/etc -BACKUP_SCHEDULE_DB=@daily -# Cron para o MinIO (mc mirror é incremental, pode rodar mais vezes) -BACKUP_SCHEDULE_MEDIA=0 3 * * * -# Retenção (camadas: dias / semanas / meses) — só pro Postgres -BACKUP_KEEP_DAYS=14 -BACKUP_KEEP_WEEKS=4 -BACKUP_KEEP_MONTHS=6 - -# Backup remoto OPCIONAL — espelha o bucket pra outro provedor S3 (R2, -# B2, AWS S3, outro MinIO). Deixe vazio pra ficar só com backup local. -# Exemplo com Cloudflare R2: -# BACKUP_REMOTE_ENDPOINT=https://.r2.cloudflarestorage.com -# BACKUP_REMOTE_ACCESS_KEY=... -# BACKUP_REMOTE_SECRET_KEY=... -# BACKUP_REMOTE_BUCKET=wedding-media-backup -BACKUP_REMOTE_ENDPOINT= -BACKUP_REMOTE_ACCESS_KEY= -BACKUP_REMOTE_SECRET_KEY= -BACKUP_REMOTE_BUCKET= diff --git a/.gitignore b/.gitignore index f61fc05..da25c3f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,9 +11,6 @@ build/ .env.*.local !.env.example -# Backup output on the VPS -backups/ - # Python __pycache__/ *.pyc @@ -27,5 +24,17 @@ venv/ .ruff_cache/ *.egg-info/ +# Diretórios de dados (bind mounts dos volumes Docker) +infra/main/postgres/data/ +infra/main/redis/data/ +infra/main/minio/data/ +infra/main/pgadmin/data/ +infra/main/caddy/data/ +infra/main/caddy/config/ +infra/gitea/gitea/data/ +infra/gitea/gitea/config/ +infra/gitea/runner/data/ +infra/wedding_photo/backups/ + .vscode/ .idea/ diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index 2774d45..0000000 --- a/Caddyfile +++ /dev/null @@ -1,25 +0,0 @@ -{ - email {$ACME_EMAIL} -} - -{$DOMAIN} { - encode zstd gzip - - request_body { - max_size 600MB - } - - reverse_proxy app:3000 { - transport http { - response_header_timeout 10m - dial_timeout 30s - } - } -} - -{$MEDIA_DOMAIN} { - encode gzip - reverse_proxy minio:9000 { - header_up Host {upstream_hostport} - } -} diff --git a/Makefile b/Makefile index 1da9a36..a3e12d3 100644 --- a/Makefile +++ b/Makefile @@ -1,32 +1,82 @@ -.PHONY: install dev-web dev-api migrate build docker-up docker-down docker-logs lint typecheck +.PHONY: help network up down restart status \ + up-main up-gitea up-wedding \ + down-main down-gitea down-wedding \ + logs-main logs-gitea logs-wedding \ + pull-main pull-gitea pull-wedding \ + rebuild-wedding -install: - pnpm install - cd apps/api && uv sync +NET := infra-net +DC_MAIN := docker compose -f infra/main/docker-compose.yml --env-file infra/main/.env +DC_GITEA := docker compose -f infra/gitea/docker-compose.yml --env-file infra/gitea/.env +DC_WEDDING := docker compose -f infra/wedding_photo/docker-compose.yml --env-file infra/wedding_photo/.env -dev-web: - pnpm -F @leetete/web dev +help: + @echo "Stacks: main (postgres+redis+minio+pgadmin+caddy) | gitea | wedding_photo" + @echo "" + @echo " make up Sobe tudo na ordem (main -> gitea -> wedding)" + @echo " make down Desce tudo na ordem inversa" + @echo " make restart down + up" + @echo " make status ps de todas as stacks" + @echo "" + @echo " make up-main Sobe só a stack main" + @echo " make up-gitea Sobe só o gitea" + @echo " make up-wedding Build + up do app wedding" + @echo " make rebuild-wedding Force rebuild do app wedding" + @echo " make down-{main,gitea,wedding}" + @echo " make logs-{main,gitea,wedding}" + @echo " make pull-{main,gitea,wedding}" + @echo " make network Cria a network external '$(NET)' (idempotente)" -dev-api: - cd apps/api && uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 3000 +network: + @./network.sh -migrate: - cd apps/api && uv run python -m app.db.migrate +up: network up-main up-gitea up-wedding -build: - pnpm -F @leetete/web build +down: down-wedding down-gitea down-main -typecheck: - pnpm -F @leetete/web typecheck +restart: down up -lint: - cd apps/api && uv run ruff check app +status: + @$(DC_MAIN) ps || true + @$(DC_GITEA) ps || true + @$(DC_WEDDING) ps || true -docker-up: - docker compose up -d --build +up-main: network + $(DC_MAIN) up -d -docker-down: - docker compose down +up-gitea: network + $(DC_GITEA) up -d -docker-logs: - docker compose logs -f app +up-wedding: network + $(DC_WEDDING) up -d --build + +rebuild-wedding: network + $(DC_WEDDING) build --no-cache + $(DC_WEDDING) up -d + +down-main: + $(DC_MAIN) down + +down-gitea: + $(DC_GITEA) down + +down-wedding: + $(DC_WEDDING) down + +logs-main: + $(DC_MAIN) logs -f + +logs-gitea: + $(DC_GITEA) logs -f + +logs-wedding: + $(DC_WEDDING) logs -f + +pull-main: + $(DC_MAIN) pull + +pull-gitea: + $(DC_GITEA) pull + +pull-wedding: + $(DC_WEDDING) pull diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 95a805c..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,150 +0,0 @@ -services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER:-wedding} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB:-wedding} - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-wedding} -d ${POSTGRES_DB:-wedding}"] - interval: 10s - timeout: 5s - retries: 10 - - minio: - image: minio/minio:latest - restart: unless-stopped - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} - volumes: - - minio_data:/data - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 15s - timeout: 10s - retries: 10 - - minio-init: - image: minio/mc:latest - restart: "no" - depends_on: - minio: - condition: service_healthy - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} - S3_BUCKET: ${S3_BUCKET:-wedding-media} - entrypoint: - - /bin/sh - - -c - - | - set -e - mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" - mc mb --ignore-existing local/"$$S3_BUCKET" - mc anonymous set download local/"$$S3_BUCKET" - mc cors set local/"$$S3_BUCKET" /tmp/cors.json || true - echo "minio init done" - volumes: - - ./infra/minio-cors.json:/tmp/cors.json:ro - - app: - build: - context: . - dockerfile: Dockerfile - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - minio: - condition: service_healthy - environment: - DATABASE_URL: postgres://${POSTGRES_USER:-wedding}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-wedding} - S3_ENDPOINT: ${S3_INTERNAL_ENDPOINT:-https://${MEDIA_DOMAIN}} - S3_BUCKET: ${S3_BUCKET:-wedding-media} - S3_REGION: ${S3_REGION:-us-east-1} - S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER} - S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD} - S3_PUBLIC_BASE_URL: https://${MEDIA_DOMAIN}/${S3_BUCKET:-wedding-media} - S3_FORCE_PATH_STYLE: "true" - COUPLE_NAMES: ${COUPLE_NAMES:-Stefanie & Leandro} - EVENT_DATE: ${EVENT_DATE:-} - PUBLIC_BASE_URL: https://${DOMAIN} - ADMIN_PASSWORD: ${ADMIN_PASSWORD} - SESSION_SECRET: ${SESSION_SECRET} - ALLOWED_ADMIN_EMAILS: ${ALLOWED_ADMIN_EMAILS} - AUTO_MIGRATE: "true" - NODE_ENV: production - PORT: 3000 - expose: - - "3000" - - caddy: - image: caddy:2-alpine - restart: unless-stopped - ports: - - "80:80" - - "443:443" - - "443:443/udp" - environment: - DOMAIN: ${DOMAIN} - MEDIA_DOMAIN: ${MEDIA_DOMAIN} - ACME_EMAIL: ${ACME_EMAIL} - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - depends_on: - - app - - minio - - postgres-backup: - image: prodrigestivill/postgres-backup-local:16-alpine - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - environment: - POSTGRES_HOST: postgres - POSTGRES_DB: ${POSTGRES_DB:-wedding} - POSTGRES_USER: ${POSTGRES_USER:-wedding} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_EXTRA_OPTS: "--clean --if-exists" - SCHEDULE: ${BACKUP_SCHEDULE_DB:-@daily} - BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-14} - BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4} - BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6} - TZ: ${TZ:-America/Sao_Paulo} - volumes: - - ./backups/postgres:/backups - - media-backup: - image: alpine:3.20 - restart: unless-stopped - depends_on: - minio: - condition: service_healthy - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} - S3_BUCKET: ${S3_BUCKET:-wedding-media} - BACKUP_SCHEDULE_MEDIA: ${BACKUP_SCHEDULE_MEDIA:-0 3 * * *} - BACKUP_REMOTE_ENDPOINT: ${BACKUP_REMOTE_ENDPOINT:-} - BACKUP_REMOTE_ACCESS_KEY: ${BACKUP_REMOTE_ACCESS_KEY:-} - BACKUP_REMOTE_SECRET_KEY: ${BACKUP_REMOTE_SECRET_KEY:-} - BACKUP_REMOTE_BUCKET: ${BACKUP_REMOTE_BUCKET:-} - TZ: ${TZ:-America/Sao_Paulo} - volumes: - - ./backups/media:/backups - - ./infra/backup:/scripts:ro - entrypoint: ["/bin/sh", "/scripts/media-backup-entrypoint.sh"] - -volumes: - postgres_data: - minio_data: - caddy_data: - caddy_config: diff --git a/infra/gitea/.env.example b/infra/gitea/.env.example new file mode 100644 index 0000000..a52e985 --- /dev/null +++ b/infra/gitea/.env.example @@ -0,0 +1,32 @@ +# ============================================================================= +# infra/gitea/.env — copie pra .env e edite +# ============================================================================= + +# Casa com infra/main/.env +DOMAIN_BASE=localhost +TZ=America/Sao_Paulo + +# Banco no postgres compartilhado (mesmas creds do main/.env) +GITEA_DB_NAME=gitea +GITEA_DB_USER=gitea +GITEA_DB_PASSWORD=troque-essa-senha-forte + +# Porta SSH no host (pra git@gitea.localhost:user/repo.git) +GITEA_SSH_PORT=2222 + +# Cadastro: true = só admin convida; false = qualquer um cria conta +GITEA_DISABLE_REGISTRATION=true +GITEA_REQUIRE_SIGNIN=false + +# ----- Gitea Actions Runner ----- +# Como pegar o token (bootstrap): +# 1. Suba só o gitea: make up-gitea (runner vai falhar a 1ª vez, ok) +# 2. Acesse https://gitea.localhost e crie o usuário admin (no terminal: +# docker exec -it gitea gitea admin user create --username admin +# --password ... --email ... --admin) +# 3. UI: Site Administration → Actions → Runners → Create new runner +# 4. Copie o token aqui e: make up-gitea (runner registra e fica de pé) +GITEA_RUNNER_TOKEN= +GITEA_RUNNER_NAME=local-runner +# Labels exemplos. Adicione mais se quiser ambientes específicos. +GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-22.04,node:docker://node:22-alpine,python:docker://python:3.12-slim diff --git a/infra/gitea/docker-compose.yml b/infra/gitea/docker-compose.yml new file mode 100644 index 0000000..fb180b3 --- /dev/null +++ b/infra/gitea/docker-compose.yml @@ -0,0 +1,67 @@ +services: + gitea: + image: gitea/gitea:1.22 + container_name: gitea + restart: unless-stopped + environment: + USER_UID: "1000" + USER_GID: "1000" + TZ: ${TZ:-America/Sao_Paulo} + # Banco em postgres compartilhado (stack main/) + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: postgres:5432 + GITEA__database__NAME: ${GITEA_DB_NAME:-gitea} + GITEA__database__USER: ${GITEA_DB_USER:-gitea} + GITEA__database__PASSWD: ${GITEA_DB_PASSWORD} + # Servidor + GITEA__server__DOMAIN: gitea.${DOMAIN_BASE:-localhost} + GITEA__server__ROOT_URL: https://gitea.${DOMAIN_BASE:-localhost}/ + GITEA__server__SSH_DOMAIN: gitea.${DOMAIN_BASE:-localhost} + GITEA__server__SSH_LISTEN_PORT: "22" + GITEA__server__SSH_PORT: "${GITEA_SSH_PORT:-2222}" + GITEA__server__HTTP_PORT: "3000" + # Cache (Redis compartilhado) + GITEA__cache__ENABLED: "true" + GITEA__cache__ADAPTER: redis + GITEA__cache__HOST: redis://redis:6379/0 + # Sessões em Redis também + GITEA__session__PROVIDER: redis + GITEA__session__PROVIDER_CONFIG: redis://redis:6379/1 + # Service + GITEA__service__DISABLE_REGISTRATION: ${GITEA_DISABLE_REGISTRATION:-true} + GITEA__service__REQUIRE_SIGNIN_VIEW: ${GITEA_REQUIRE_SIGNIN:-false} + # Actions + GITEA__actions__ENABLED: "true" + GITEA__actions__DEFAULT_ACTIONS_URL: github + ports: + # SSH pra git push/pull. UI passa pelo Caddy (porta 443). + - "${GITEA_SSH_PORT:-2222}:22" + volumes: + - ./gitea/data:/var/lib/gitea + - ./gitea/config:/etc/gitea + networks: + - infra-net + + runner: + build: + context: ./runner + container_name: gitea_runner + restart: unless-stopped + depends_on: + - gitea + environment: + TZ: ${TZ:-America/Sao_Paulo} + GITEA_INSTANCE_URL: http://gitea:3000 + GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_TOKEN:-} + GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-local-runner} + GITEA_RUNNER_LABELS: ${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://catthehacker/ubuntu:act-22.04,node:docker://node:22-alpine,python:docker://python:3.12-slim} + volumes: + - ./runner/data:/data + - /var/run/docker.sock:/var/run/docker.sock + networks: + - infra-net + +networks: + infra-net: + external: true + name: infra-net diff --git a/infra/gitea/runner/Dockerfile b/infra/gitea/runner/Dockerfile new file mode 100644 index 0000000..23e4389 --- /dev/null +++ b/infra/gitea/runner/Dockerfile @@ -0,0 +1,7 @@ +FROM gitea/act_runner:latest + +# Adiciona docker CLI pro runner conseguir buildar/pushar imagens dentro +# dos workflows (Action `docker/build-push-action` etc.). O socket do host +# é montado via volume no docker-compose.yml. +USER root +RUN apk add --no-cache docker-cli docker-cli-buildx diff --git a/infra/main/.env.example b/infra/main/.env.example new file mode 100644 index 0000000..8a1fc90 --- /dev/null +++ b/infra/main/.env.example @@ -0,0 +1,41 @@ +# ============================================================================= +# infra/main/.env — copie pra .env e edite +# ============================================================================= + +# ----- Domínio + TLS ----- +# Dev local: deixa "localhost". Caddy emite cert interno automático pra *.localhost +# Prod: usa teu domínio (ex.: lronetto.com.br). Apontar DNS: +# wedding.SEU.DOMINIO → IP do VPS +# gitea.SEU.DOMINIO → IP do VPS +# media.SEU.DOMINIO → IP do VPS +# pgadmin.SEU.DOMINIO → IP do VPS +# minio.SEU.DOMINIO → IP do VPS +DOMAIN_BASE=localhost + +# Em produção: email pra Let's Encrypt renovar certs. Em dev pode ficar vazio. +ACME_EMAIL= + +# Timezone (aplica em postgres-backup, cron de media-backup, etc.) +TZ=America/Sao_Paulo + +# ----- Postgres: admin + bancos por app ----- +POSTGRES_ADMIN_PASSWORD=troque-essa-senha-forte + +WEDDING_DB_USER=wedding +WEDDING_DB_PASSWORD=troque-essa-senha-forte +WEDDING_DB_NAME=wedding + +GITEA_DB_USER=gitea +GITEA_DB_PASSWORD=troque-essa-senha-forte +GITEA_DB_NAME=gitea + +# ----- MinIO (root creds) ----- +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=troque-essa-senha-forte + +# Bucket criado já no boot pra ser consumido pela stack wedding_photo +WEDDING_BUCKET=wedding-media + +# ----- pgAdmin (login web) ----- +PGADMIN_EMAIL=admin@localhost +PGADMIN_PASSWORD=troque-essa-senha-forte diff --git a/infra/main/caddy/Caddyfile b/infra/main/caddy/Caddyfile new file mode 100644 index 0000000..34ed6c5 --- /dev/null +++ b/infra/main/caddy/Caddyfile @@ -0,0 +1,50 @@ +{ + email {$ACME_EMAIL} + # Em dev local (*.localhost) Caddy emite cert interno automaticamente. + # Em produção, Let's Encrypt entra pelos hostnames reais. +} + +# ----- Gitea (UI + git over HTTPS) ----- +gitea.{$DOMAIN_BASE} { + encode zstd gzip + reverse_proxy gitea:3000 { + transport http { + response_header_timeout 10m + dial_timeout 30s + } + } +} + +# ----- Wedding (app dos noivos) ----- +wedding.{$DOMAIN_BASE} { + encode zstd gzip + request_body { + max_size 600MB + } + reverse_proxy wedding_app:3000 { + transport http { + response_header_timeout 10m + dial_timeout 30s + } + } +} + +# ----- pgAdmin ----- +pgadmin.{$DOMAIN_BASE} { + encode gzip + reverse_proxy pgadmin:80 +} + +# ----- MinIO console (admin web) ----- +minio.{$DOMAIN_BASE} { + encode gzip + reverse_proxy minio:9001 +} + +# ----- MinIO S3 API público (uploads/downloads do bucket) ----- +media.{$DOMAIN_BASE} { + encode gzip + reverse_proxy minio:9000 { + header_up Host {upstream_hostport} + } +} diff --git a/infra/main/docker-compose.yml b/infra/main/docker-compose.yml new file mode 100644 index 0000000..ba028e2 --- /dev/null +++ b/infra/main/docker-compose.yml @@ -0,0 +1,121 @@ +services: + postgres: + image: postgres:16-alpine + container_name: postgres + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD} + POSTGRES_DB: postgres + TZ: ${TZ:-America/Sao_Paulo} + WEDDING_DB_USER: ${WEDDING_DB_USER:-wedding} + WEDDING_DB_PASSWORD: ${WEDDING_DB_PASSWORD} + WEDDING_DB_NAME: ${WEDDING_DB_NAME:-wedding} + GITEA_DB_USER: ${GITEA_DB_USER:-gitea} + GITEA_DB_PASSWORD: ${GITEA_DB_PASSWORD} + GITEA_DB_NAME: ${GITEA_DB_NAME:-gitea} + volumes: + - ./postgres/data:/var/lib/postgresql/data + - ./postgres/init:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 10 + networks: + - infra-net + + redis: + image: redis:7-alpine + container_name: redis + restart: unless-stopped + command: ["redis-server", "--save", "60", "1", "--appendonly", "yes"] + volumes: + - ./redis/data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + networks: + - infra-net + + minio: + image: minio/minio:latest + container_name: minio + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + volumes: + - ./minio/data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 15s + timeout: 10s + retries: 10 + networks: + - infra-net + + minio-init: + image: minio/mc:latest + container_name: minio-init + restart: "no" + depends_on: + minio: + condition: service_healthy + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + WEDDING_BUCKET: ${WEDDING_BUCKET:-wedding-media} + volumes: + - ./minio/init.sh:/init.sh:ro + - ./minio/cors.json:/cors.json:ro + entrypoint: ["/bin/sh", "/init.sh"] + networks: + - infra-net + + pgadmin: + image: dpage/pgadmin4:latest + container_name: pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} + PGADMIN_DISABLE_POSTFIX: "true" + PGADMIN_CONFIG_SERVER_MODE: "True" + PGADMIN_CONFIG_PROXY_X_FOR_COUNT: "1" + PGADMIN_CONFIG_PROXY_X_PROTO_COUNT: "1" + volumes: + - ./pgadmin/data:/var/lib/pgadmin + - ./pgadmin/servers.json:/pgadmin4/servers.json:ro + depends_on: + postgres: + condition: service_healthy + networks: + - infra-net + + caddy: + image: caddy:2-alpine + container_name: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + environment: + DOMAIN_BASE: ${DOMAIN_BASE:-localhost} + ACME_EMAIL: ${ACME_EMAIL:-} + WEDDING_BUCKET: ${WEDDING_BUCKET:-wedding-media} + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - ./caddy/data:/data + - ./caddy/config:/config + networks: + - infra-net + +networks: + infra-net: + external: true + name: infra-net diff --git a/infra/minio-cors.json b/infra/main/minio/cors.json similarity index 100% rename from infra/minio-cors.json rename to infra/main/minio/cors.json diff --git a/infra/main/minio/init.sh b/infra/main/minio/init.sh new file mode 100755 index 0000000..a62532d --- /dev/null +++ b/infra/main/minio/init.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Inicializa o bucket usado pela stack wedding_photo. Roda como container +# one-shot depois que o MinIO ficou healthy. +set -e + +mc alias set --quiet local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" + +mc mb --ignore-existing "local/$WEDDING_BUCKET" +mc anonymous set download "local/$WEDDING_BUCKET" +mc cors set "local/$WEDDING_BUCKET" /cors.json 2>/dev/null || \ + echo "[minio-init] cors set ignorado (versão antiga do mc?)" + +echo "[minio-init] bucket '$WEDDING_BUCKET' pronto (anonymous download + CORS)" diff --git a/infra/main/pgadmin/servers.json b/infra/main/pgadmin/servers.json new file mode 100644 index 0000000..6a4fb8e --- /dev/null +++ b/infra/main/pgadmin/servers.json @@ -0,0 +1,13 @@ +{ + "Servers": { + "1": { + "Name": "Postgres (infra)", + "Group": "Servers", + "Host": "postgres", + "Port": 5432, + "MaintenanceDB": "postgres", + "Username": "postgres", + "SSLMode": "prefer" + } + } +} diff --git a/infra/main/postgres/init/01-create-databases.sh b/infra/main/postgres/init/01-create-databases.sh new file mode 100755 index 0000000..b5a5b71 --- /dev/null +++ b/infra/main/postgres/init/01-create-databases.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Cria os bancos e roles dedicados a cada app. Rodado pelo postgres entrypoint +# uma única vez, no primeiro boot. Subsequente: ignorado (init scripts só +# rodam quando PGDATA está vazio). +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE ROLE "${WEDDING_DB_USER}" WITH LOGIN PASSWORD '${WEDDING_DB_PASSWORD}'; + CREATE DATABASE "${WEDDING_DB_NAME}" OWNER "${WEDDING_DB_USER}"; + GRANT ALL PRIVILEGES ON DATABASE "${WEDDING_DB_NAME}" TO "${WEDDING_DB_USER}"; + + CREATE ROLE "${GITEA_DB_USER}" WITH LOGIN PASSWORD '${GITEA_DB_PASSWORD}'; + CREATE DATABASE "${GITEA_DB_NAME}" OWNER "${GITEA_DB_USER}"; + GRANT ALL PRIVILEGES ON DATABASE "${GITEA_DB_NAME}" TO "${GITEA_DB_USER}"; +EOSQL + +echo "[postgres-init] roles + databases criados para wedding (${WEDDING_DB_NAME}) e gitea (${GITEA_DB_NAME})" diff --git a/.dockerignore b/infra/wedding_photo/.dockerignore similarity index 100% rename from .dockerignore rename to infra/wedding_photo/.dockerignore diff --git a/infra/wedding_photo/.env.example b/infra/wedding_photo/.env.example new file mode 100644 index 0000000..f138049 --- /dev/null +++ b/infra/wedding_photo/.env.example @@ -0,0 +1,53 @@ +# ============================================================================= +# infra/wedding_photo/.env — copie pra .env e edite +# As variáveis com prefixo compartilhado (MINIO_*, WEDDING_DB_*) precisam ter +# os MESMOS valores aqui e em infra/main/.env (mesmas creds do banco e do MinIO). +# ============================================================================= + +# Compartilhadas com main/ +DOMAIN_BASE=localhost +TZ=America/Sao_Paulo + +# DB (mesmo do main/.env) +WEDDING_DB_USER=wedding +WEDDING_DB_PASSWORD=troque-essa-senha-forte +WEDDING_DB_NAME=wedding + +# MinIO (mesmo do main/.env) +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=troque-essa-senha-forte +WEDDING_BUCKET=wedding-media +S3_REGION=us-east-1 + +# Opcional: sobreescrevem o default se quiser endpoints customizados. +# Default monta a partir de DOMAIN_BASE + WEDDING_BUCKET: +# S3_ENDPOINT=https://media.${DOMAIN_BASE} +# S3_PUBLIC_BASE_URL=https://media.${DOMAIN_BASE}/${WEDDING_BUCKET} +# S3_ENDPOINT= +# S3_PUBLIC_BASE_URL= + +# ----- App (específico desta stack) ----- +COUPLE_NAMES=Stefanie & Leandro +EVENT_DATE=07 de junho de 2026 + +ADMIN_PASSWORD=senha-forte-compartilhada-entre-os-dois +SESSION_SECRET=gere-um-uuid-longo-aqui-min-32-chars +ALLOWED_ADMIN_EMAILS=voce@exemplo.com,outro@exemplo.com + +# ----- Backups (escrevem em ./backups/ no host) ----- +BACKUP_SCHEDULE_DB=@daily +BACKUP_SCHEDULE_MEDIA=0 3 * * * +BACKUP_KEEP_DAYS=14 +BACKUP_KEEP_WEEKS=4 +BACKUP_KEEP_MONTHS=6 + +# Backup remoto OPCIONAL (R2, B2, outro MinIO). Vazio = só backup local. +# Exemplo com Cloudflare R2: +# BACKUP_REMOTE_ENDPOINT=https://.r2.cloudflarestorage.com +# BACKUP_REMOTE_ACCESS_KEY=... +# BACKUP_REMOTE_SECRET_KEY=... +# BACKUP_REMOTE_BUCKET=wedding-media-backup +BACKUP_REMOTE_ENDPOINT= +BACKUP_REMOTE_ACCESS_KEY= +BACKUP_REMOTE_SECRET_KEY= +BACKUP_REMOTE_BUCKET= diff --git a/.npmrc b/infra/wedding_photo/.npmrc similarity index 100% rename from .npmrc rename to infra/wedding_photo/.npmrc diff --git a/Dockerfile b/infra/wedding_photo/Dockerfile similarity index 100% rename from Dockerfile rename to infra/wedding_photo/Dockerfile diff --git a/infra/wedding_photo/Makefile b/infra/wedding_photo/Makefile new file mode 100644 index 0000000..9c1abfa --- /dev/null +++ b/infra/wedding_photo/Makefile @@ -0,0 +1,26 @@ +.PHONY: install dev-web dev-api migrate build typecheck lint + +# Comandos de DESENVOLVIMENTO LOCAL (rodam fora do Docker). +# Pra subir/derrubar a stack, use o Makefile do root: `make up-wedding` etc. + +install: + pnpm install + cd apps/api && uv sync + +dev-web: + pnpm -F @leetete/web dev + +dev-api: + cd apps/api && uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 3000 + +migrate: + cd apps/api && uv run python -m app.db.migrate + +build: + pnpm -F @leetete/web build + +typecheck: + pnpm -F @leetete/web typecheck + +lint: + cd apps/api && uv run ruff check app diff --git a/apps/api/app/__init__.py b/infra/wedding_photo/apps/api/app/__init__.py similarity index 100% rename from apps/api/app/__init__.py rename to infra/wedding_photo/apps/api/app/__init__.py diff --git a/apps/api/app/config.py b/infra/wedding_photo/apps/api/app/config.py similarity index 100% rename from apps/api/app/config.py rename to infra/wedding_photo/apps/api/app/config.py diff --git a/apps/api/app/db/__init__.py b/infra/wedding_photo/apps/api/app/db/__init__.py similarity index 100% rename from apps/api/app/db/__init__.py rename to infra/wedding_photo/apps/api/app/db/__init__.py diff --git a/apps/api/app/db/base.py b/infra/wedding_photo/apps/api/app/db/base.py similarity index 100% rename from apps/api/app/db/base.py rename to infra/wedding_photo/apps/api/app/db/base.py diff --git a/apps/api/app/db/migrate.py b/infra/wedding_photo/apps/api/app/db/migrate.py similarity index 100% rename from apps/api/app/db/migrate.py rename to infra/wedding_photo/apps/api/app/db/migrate.py diff --git a/apps/api/app/db/models.py b/infra/wedding_photo/apps/api/app/db/models.py similarity index 100% rename from apps/api/app/db/models.py rename to infra/wedding_photo/apps/api/app/db/models.py diff --git a/apps/api/app/lib/__init__.py b/infra/wedding_photo/apps/api/app/lib/__init__.py similarity index 100% rename from apps/api/app/lib/__init__.py rename to infra/wedding_photo/apps/api/app/lib/__init__.py diff --git a/apps/api/app/lib/auth.py b/infra/wedding_photo/apps/api/app/lib/auth.py similarity index 100% rename from apps/api/app/lib/auth.py rename to infra/wedding_photo/apps/api/app/lib/auth.py diff --git a/apps/api/app/lib/ids.py b/infra/wedding_photo/apps/api/app/lib/ids.py similarity index 100% rename from apps/api/app/lib/ids.py rename to infra/wedding_photo/apps/api/app/lib/ids.py diff --git a/apps/api/app/lib/pdf.py b/infra/wedding_photo/apps/api/app/lib/pdf.py similarity index 100% rename from apps/api/app/lib/pdf.py rename to infra/wedding_photo/apps/api/app/lib/pdf.py diff --git a/apps/api/app/lib/qrcode_gen.py b/infra/wedding_photo/apps/api/app/lib/qrcode_gen.py similarity index 100% rename from apps/api/app/lib/qrcode_gen.py rename to infra/wedding_photo/apps/api/app/lib/qrcode_gen.py diff --git a/apps/api/app/lib/storage.py b/infra/wedding_photo/apps/api/app/lib/storage.py similarity index 100% rename from apps/api/app/lib/storage.py rename to infra/wedding_photo/apps/api/app/lib/storage.py diff --git a/apps/api/app/lib/transcode.py b/infra/wedding_photo/apps/api/app/lib/transcode.py similarity index 100% rename from apps/api/app/lib/transcode.py rename to infra/wedding_photo/apps/api/app/lib/transcode.py diff --git a/apps/api/app/main.py b/infra/wedding_photo/apps/api/app/main.py similarity index 100% rename from apps/api/app/main.py rename to infra/wedding_photo/apps/api/app/main.py diff --git a/apps/api/app/migrations/0001_initial.sql b/infra/wedding_photo/apps/api/app/migrations/0001_initial.sql similarity index 100% rename from apps/api/app/migrations/0001_initial.sql rename to infra/wedding_photo/apps/api/app/migrations/0001_initial.sql diff --git a/apps/api/app/routes/__init__.py b/infra/wedding_photo/apps/api/app/routes/__init__.py similarity index 100% rename from apps/api/app/routes/__init__.py rename to infra/wedding_photo/apps/api/app/routes/__init__.py diff --git a/apps/api/app/routes/admin.py b/infra/wedding_photo/apps/api/app/routes/admin.py similarity index 100% rename from apps/api/app/routes/admin.py rename to infra/wedding_photo/apps/api/app/routes/admin.py diff --git a/apps/api/app/routes/public.py b/infra/wedding_photo/apps/api/app/routes/public.py similarity index 100% rename from apps/api/app/routes/public.py rename to infra/wedding_photo/apps/api/app/routes/public.py diff --git a/apps/api/app/routes/uploads.py b/infra/wedding_photo/apps/api/app/routes/uploads.py similarity index 100% rename from apps/api/app/routes/uploads.py rename to infra/wedding_photo/apps/api/app/routes/uploads.py diff --git a/apps/api/app/schemas/__init__.py b/infra/wedding_photo/apps/api/app/schemas/__init__.py similarity index 100% rename from apps/api/app/schemas/__init__.py rename to infra/wedding_photo/apps/api/app/schemas/__init__.py diff --git a/apps/api/app/schemas/api.py b/infra/wedding_photo/apps/api/app/schemas/api.py similarity index 100% rename from apps/api/app/schemas/api.py rename to infra/wedding_photo/apps/api/app/schemas/api.py diff --git a/apps/api/pyproject.toml b/infra/wedding_photo/apps/api/pyproject.toml similarity index 100% rename from apps/api/pyproject.toml rename to infra/wedding_photo/apps/api/pyproject.toml diff --git a/apps/web/index.html b/infra/wedding_photo/apps/web/index.html similarity index 100% rename from apps/web/index.html rename to infra/wedding_photo/apps/web/index.html diff --git a/apps/web/package.json b/infra/wedding_photo/apps/web/package.json similarity index 100% rename from apps/web/package.json rename to infra/wedding_photo/apps/web/package.json diff --git a/apps/web/postcss.config.js b/infra/wedding_photo/apps/web/postcss.config.js similarity index 100% rename from apps/web/postcss.config.js rename to infra/wedding_photo/apps/web/postcss.config.js diff --git a/apps/web/src/App.tsx b/infra/wedding_photo/apps/web/src/App.tsx similarity index 100% rename from apps/web/src/App.tsx rename to infra/wedding_photo/apps/web/src/App.tsx diff --git a/apps/web/src/index.css b/infra/wedding_photo/apps/web/src/index.css similarity index 100% rename from apps/web/src/index.css rename to infra/wedding_photo/apps/web/src/index.css diff --git a/apps/web/src/lib/api.ts b/infra/wedding_photo/apps/web/src/lib/api.ts similarity index 100% rename from apps/web/src/lib/api.ts rename to infra/wedding_photo/apps/web/src/lib/api.ts diff --git a/apps/web/src/lib/upload.ts b/infra/wedding_photo/apps/web/src/lib/upload.ts similarity index 100% rename from apps/web/src/lib/upload.ts rename to infra/wedding_photo/apps/web/src/lib/upload.ts diff --git a/apps/web/src/main.tsx b/infra/wedding_photo/apps/web/src/main.tsx similarity index 100% rename from apps/web/src/main.tsx rename to infra/wedding_photo/apps/web/src/main.tsx diff --git a/apps/web/src/routes/Gallery.tsx b/infra/wedding_photo/apps/web/src/routes/Gallery.tsx similarity index 100% rename from apps/web/src/routes/Gallery.tsx rename to infra/wedding_photo/apps/web/src/routes/Gallery.tsx diff --git a/apps/web/src/routes/Home.tsx b/infra/wedding_photo/apps/web/src/routes/Home.tsx similarity index 100% rename from apps/web/src/routes/Home.tsx rename to infra/wedding_photo/apps/web/src/routes/Home.tsx diff --git a/apps/web/src/routes/Upload.tsx b/infra/wedding_photo/apps/web/src/routes/Upload.tsx similarity index 100% rename from apps/web/src/routes/Upload.tsx rename to infra/wedding_photo/apps/web/src/routes/Upload.tsx diff --git a/apps/web/src/routes/admin/Dashboard.tsx b/infra/wedding_photo/apps/web/src/routes/admin/Dashboard.tsx similarity index 100% rename from apps/web/src/routes/admin/Dashboard.tsx rename to infra/wedding_photo/apps/web/src/routes/admin/Dashboard.tsx diff --git a/apps/web/src/routes/admin/Login.tsx b/infra/wedding_photo/apps/web/src/routes/admin/Login.tsx similarity index 100% rename from apps/web/src/routes/admin/Login.tsx rename to infra/wedding_photo/apps/web/src/routes/admin/Login.tsx diff --git a/apps/web/tailwind.config.ts b/infra/wedding_photo/apps/web/tailwind.config.ts similarity index 100% rename from apps/web/tailwind.config.ts rename to infra/wedding_photo/apps/web/tailwind.config.ts diff --git a/apps/web/tsconfig.json b/infra/wedding_photo/apps/web/tsconfig.json similarity index 100% rename from apps/web/tsconfig.json rename to infra/wedding_photo/apps/web/tsconfig.json diff --git a/apps/web/vite.config.ts b/infra/wedding_photo/apps/web/vite.config.ts similarity index 100% rename from apps/web/vite.config.ts rename to infra/wedding_photo/apps/web/vite.config.ts diff --git a/infra/wedding_photo/docker-compose.yml b/infra/wedding_photo/docker-compose.yml new file mode 100644 index 0000000..abc7448 --- /dev/null +++ b/infra/wedding_photo/docker-compose.yml @@ -0,0 +1,81 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: wedding_app + restart: unless-stopped + environment: + DATABASE_URL: postgres://${WEDDING_DB_USER:-wedding}:${WEDDING_DB_PASSWORD}@postgres:5432/${WEDDING_DB_NAME:-wedding} + # Endpoint pra assinar/servir uploads pelo browser. Em prod usa + # https://media.{DOMAIN_BASE}. Em dev local também — o app acessa via + # extra_hosts -> host gateway -> Caddy -> minio. + S3_ENDPOINT: ${S3_ENDPOINT:-https://media.${DOMAIN_BASE:-localhost}} + S3_BUCKET: ${WEDDING_BUCKET:-wedding-media} + S3_REGION: ${S3_REGION:-us-east-1} + S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER} + S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD} + S3_PUBLIC_BASE_URL: ${S3_PUBLIC_BASE_URL:-https://media.${DOMAIN_BASE:-localhost}/${WEDDING_BUCKET:-wedding-media}} + S3_FORCE_PATH_STYLE: "true" + COUPLE_NAMES: ${COUPLE_NAMES:-Stefanie & Leandro} + EVENT_DATE: ${EVENT_DATE:-} + PUBLIC_BASE_URL: https://wedding.${DOMAIN_BASE:-localhost} + ADMIN_PASSWORD: ${ADMIN_PASSWORD} + SESSION_SECRET: ${SESSION_SECRET} + ALLOWED_ADMIN_EMAILS: ${ALLOWED_ADMIN_EMAILS} + AUTO_MIGRATE: "true" + NODE_ENV: production + PORT: "3000" + # Em dev local, faz `media.localhost` (e o domínio do site) apontarem pro + # host gateway, pra o app dentro do container alcançar o Caddy do main/. + extra_hosts: + - "media.${DOMAIN_BASE:-localhost}:host-gateway" + - "wedding.${DOMAIN_BASE:-localhost}:host-gateway" + networks: + - infra-net + + postgres-backup: + image: prodrigestivill/postgres-backup-local:16-alpine + container_name: wedding_pg_backup + restart: unless-stopped + environment: + POSTGRES_HOST: postgres + POSTGRES_DB: ${WEDDING_DB_NAME:-wedding} + POSTGRES_USER: ${WEDDING_DB_USER:-wedding} + POSTGRES_PASSWORD: ${WEDDING_DB_PASSWORD} + POSTGRES_EXTRA_OPTS: "--clean --if-exists" + SCHEDULE: ${BACKUP_SCHEDULE_DB:-@daily} + BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-14} + BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4} + BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6} + TZ: ${TZ:-America/Sao_Paulo} + volumes: + - ./backups/postgres:/backups + networks: + - infra-net + + media-backup: + image: alpine:3.20 + container_name: wedding_media_backup + restart: unless-stopped + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + S3_BUCKET: ${WEDDING_BUCKET:-wedding-media} + BACKUP_SCHEDULE_MEDIA: ${BACKUP_SCHEDULE_MEDIA:-0 3 * * *} + BACKUP_REMOTE_ENDPOINT: ${BACKUP_REMOTE_ENDPOINT:-} + BACKUP_REMOTE_ACCESS_KEY: ${BACKUP_REMOTE_ACCESS_KEY:-} + BACKUP_REMOTE_SECRET_KEY: ${BACKUP_REMOTE_SECRET_KEY:-} + BACKUP_REMOTE_BUCKET: ${BACKUP_REMOTE_BUCKET:-} + TZ: ${TZ:-America/Sao_Paulo} + volumes: + - ./backups/media:/backups + - ./infra/backup:/scripts:ro + entrypoint: ["/bin/sh", "/scripts/media-backup-entrypoint.sh"] + networks: + - infra-net + +networks: + infra-net: + external: true + name: infra-net diff --git a/infra/backup/media-backup-entrypoint.sh b/infra/wedding_photo/infra/backup/media-backup-entrypoint.sh similarity index 100% rename from infra/backup/media-backup-entrypoint.sh rename to infra/wedding_photo/infra/backup/media-backup-entrypoint.sh diff --git a/infra/backup/media-backup-run.sh b/infra/wedding_photo/infra/backup/media-backup-run.sh similarity index 100% rename from infra/backup/media-backup-run.sh rename to infra/wedding_photo/infra/backup/media-backup-run.sh diff --git a/package.json b/infra/wedding_photo/package.json similarity index 100% rename from package.json rename to infra/wedding_photo/package.json diff --git a/packages/shared/package.json b/infra/wedding_photo/packages/shared/package.json similarity index 100% rename from packages/shared/package.json rename to infra/wedding_photo/packages/shared/package.json diff --git a/packages/shared/src/index.ts b/infra/wedding_photo/packages/shared/src/index.ts similarity index 100% rename from packages/shared/src/index.ts rename to infra/wedding_photo/packages/shared/src/index.ts diff --git a/packages/shared/src/schemas.ts b/infra/wedding_photo/packages/shared/src/schemas.ts similarity index 100% rename from packages/shared/src/schemas.ts rename to infra/wedding_photo/packages/shared/src/schemas.ts diff --git a/packages/shared/src/types.ts b/infra/wedding_photo/packages/shared/src/types.ts similarity index 100% rename from packages/shared/src/types.ts rename to infra/wedding_photo/packages/shared/src/types.ts diff --git a/packages/shared/tsconfig.json b/infra/wedding_photo/packages/shared/tsconfig.json similarity index 100% rename from packages/shared/tsconfig.json rename to infra/wedding_photo/packages/shared/tsconfig.json diff --git a/pnpm-lock.yaml b/infra/wedding_photo/pnpm-lock.yaml similarity index 100% rename from pnpm-lock.yaml rename to infra/wedding_photo/pnpm-lock.yaml diff --git a/pnpm-workspace.yaml b/infra/wedding_photo/pnpm-workspace.yaml similarity index 100% rename from pnpm-workspace.yaml rename to infra/wedding_photo/pnpm-workspace.yaml diff --git a/tsconfig.base.json b/infra/wedding_photo/tsconfig.base.json similarity index 100% rename from tsconfig.base.json rename to infra/wedding_photo/tsconfig.base.json diff --git a/network.sh b/network.sh new file mode 100755 index 0000000..bcb9d36 --- /dev/null +++ b/network.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +NET="${INFRA_NETWORK:-infra-net}" + +if docker network inspect "$NET" >/dev/null 2>&1; then + echo "[network] '$NET' já existe" +else + docker network create --driver bridge "$NET" >/dev/null + echo "[network] '$NET' criada" +fi