feat: split into 3 docker-compose stacks (main / gitea / wedding_photo)
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.
This commit is contained in:
parent
f882f8a1c8
commit
5a19753013
57
.env.example
57
.env.example
@ -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://<account-id>.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=
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@ -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/
|
||||
|
||||
25
Caddyfile
25
Caddyfile
@ -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}
|
||||
}
|
||||
}
|
||||
94
Makefile
94
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
|
||||
|
||||
@ -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:
|
||||
32
infra/gitea/.env.example
Normal file
32
infra/gitea/.env.example
Normal file
@ -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
|
||||
67
infra/gitea/docker-compose.yml
Normal file
67
infra/gitea/docker-compose.yml
Normal file
@ -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
|
||||
7
infra/gitea/runner/Dockerfile
Normal file
7
infra/gitea/runner/Dockerfile
Normal file
@ -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
|
||||
41
infra/main/.env.example
Normal file
41
infra/main/.env.example
Normal file
@ -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
|
||||
50
infra/main/caddy/Caddyfile
Normal file
50
infra/main/caddy/Caddyfile
Normal file
@ -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}
|
||||
}
|
||||
}
|
||||
121
infra/main/docker-compose.yml
Normal file
121
infra/main/docker-compose.yml
Normal file
@ -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
|
||||
13
infra/main/minio/init.sh
Executable file
13
infra/main/minio/init.sh
Executable file
@ -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)"
|
||||
13
infra/main/pgadmin/servers.json
Normal file
13
infra/main/pgadmin/servers.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"Servers": {
|
||||
"1": {
|
||||
"Name": "Postgres (infra)",
|
||||
"Group": "Servers",
|
||||
"Host": "postgres",
|
||||
"Port": 5432,
|
||||
"MaintenanceDB": "postgres",
|
||||
"Username": "postgres",
|
||||
"SSLMode": "prefer"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
infra/main/postgres/init/01-create-databases.sh
Executable file
17
infra/main/postgres/init/01-create-databases.sh
Executable file
@ -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})"
|
||||
53
infra/wedding_photo/.env.example
Normal file
53
infra/wedding_photo/.env.example
Normal file
@ -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://<account-id>.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=
|
||||
26
infra/wedding_photo/Makefile
Normal file
26
infra/wedding_photo/Makefile
Normal file
@ -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
|
||||
81
infra/wedding_photo/docker-compose.yml
Normal file
81
infra/wedding_photo/docker-compose.yml
Normal file
@ -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
|
||||
11
network.sh
Executable file
11
network.sh
Executable file
@ -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
|
||||
Reference in New Issue
Block a user