From 8600e31797176dc725041f2f512dd79566cfc51d Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:20:04 -0300 Subject: [PATCH 1/5] Add production docker compose and deploy guide --- .env.example | 20 ++++++++++++++++++++ DEPLOY.md | 45 ++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 .env.example create mode 100644 DEPLOY.md create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2e94369 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Backend +ASPNETCORE_ENVIRONMENT=Production +APP_USE_HTTPS_REDIRECTION=false + +# JWT +JWT_KEY=CHANGE_ME_TO_A_LONG_RANDOM_SECRET +JWT_ISSUER=LineGestao +JWT_AUDIENCE=LineGestao + +# Seed admin (first login) +SEED_ADMIN_EMAIL=admin@seu-dominio.com +SEED_ADMIN_PASSWORD=CHANGE_ME_TO_A_STRONG_PASSWORD + +# PostgreSQL +POSTGRES_DB=linegestao +POSTGRES_USER=linegestao_app +POSTGRES_PASSWORD=CHANGE_ME_TO_A_STRONG_PASSWORD + +# Front-end URL used by CORS +FRONTEND_PUBLIC_URL=https://seu-dominio.com diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..cdef371 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,45 @@ +# Deploy rápido (API + Postgres) + +## Erro: `no configuration file provided: not found` +Esse erro acontece quando o `docker compose` é executado em uma pasta sem `docker-compose.yml`. + +Este repositório agora inclui `docker-compose.yml` na raiz do projeto. + +## Passo a passo + +1. Entre na pasta da API: + +```bash +cd ~/apps/line-gestao-api +``` + +2. Crie o `.env` a partir do exemplo: + +```bash +cp .env.example .env +nano .env +``` + +3. Suba os containers: + +```bash +docker compose up -d --build +``` + +4. Verifique o status e logs: + +```bash +docker compose ps +docker compose logs -f --tail=200 +``` + +5. Teste healthcheck da API: + +```bash +curl -I http://SEU_SERVIDOR:4000/health +``` + +## Observações +- O Postgres **não é exposto** para fora do Docker (sem `5432:5432`). +- A API sobe na porta `4000` do host (`4000:8080`). +- Defina valores fortes para `JWT_KEY`, `SEED_ADMIN_PASSWORD` e `POSTGRES_PASSWORD`. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bcbe69d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +services: + db: + image: postgres:16-alpine + container_name: linegestao-db + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-linegestao} + POSTGRES_USER: ${POSTGRES_USER:-linegestao_app} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-linegestao_app} -d ${POSTGRES_DB:-linegestao}"] + interval: 10s + timeout: 5s + retries: 10 + + api: + build: + context: . + dockerfile: Dockerfile + container_name: linegestao-api + restart: unless-stopped + depends_on: + db: + condition: service_healthy + ports: + - "4000:8080" + environment: + ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production} + App__UseHttpsRedirection: ${APP_USE_HTTPS_REDIRECTION:-false} + ConnectionStrings__Default: Host=db;Port=5432;Database=${POSTGRES_DB:-linegestao};Username=${POSTGRES_USER:-linegestao_app};Password=${POSTGRES_PASSWORD} + Jwt__Key: ${JWT_KEY:?JWT_KEY is required} + Jwt__Issuer: ${JWT_ISSUER:-LineGestao} + Jwt__Audience: ${JWT_AUDIENCE:-LineGestao} + Seed__AdminEmail: ${SEED_ADMIN_EMAIL:?SEED_ADMIN_EMAIL is required} + Seed__AdminPassword: ${SEED_ADMIN_PASSWORD:?SEED_ADMIN_PASSWORD is required} + Cors__AllowedOrigins__0: ${FRONTEND_PUBLIC_URL:?FRONTEND_PUBLIC_URL is required} + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"] + interval: 20s + timeout: 5s + retries: 10 + start_period: 20s + +volumes: + pg_data: From 518a846a7edde32b23e3860b33c325d9302400eb Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:46:42 -0300 Subject: [PATCH 2/5] Fix API container healthcheck command --- Dockerfile | 4 ++++ docker-compose.yml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bdf7133..6b5b13b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,10 @@ RUN dotnet publish "./line-gestao-api.csproj" -c Release -o /app/publish /p:UseA FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + ENV ASPNETCORE_URLS=http://+:8080 ENV ASPNETCORE_ENVIRONMENT=Production diff --git a/docker-compose.yml b/docker-compose.yml index bcbe69d..e72ce42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,7 @@ services: Seed__AdminPassword: ${SEED_ADMIN_PASSWORD:?SEED_ADMIN_PASSWORD is required} Cors__AllowedOrigins__0: ${FRONTEND_PUBLIC_URL:?FRONTEND_PUBLIC_URL is required} healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"] + test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] interval: 20s timeout: 5s retries: 10 From edceb48da083c0eb681b3c08dd4ffd9f0a3c189b Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:03:50 -0300 Subject: [PATCH 3/5] Document post-deploy health and domain setup steps --- DEPLOY.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/DEPLOY.md b/DEPLOY.md index cdef371..1e36704 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -3,7 +3,7 @@ ## Erro: `no configuration file provided: not found` Esse erro acontece quando o `docker compose` é executado em uma pasta sem `docker-compose.yml`. -Este repositório agora inclui `docker-compose.yml` na raiz do projeto. +Este repositório inclui `docker-compose.yml` na raiz do projeto. ## Passo a passo @@ -39,6 +39,40 @@ docker compose logs -f --tail=200 curl -I http://SEU_SERVIDOR:4000/health ``` +## Checklist de confirmação (API realmente no ar) +Considere que o deploy está concluído quando **todos** os itens abaixo estiverem OK: + +```bash +docker compose ps +curl -i http://127.0.0.1:4000/health +curl -i http://SEU_IP_PUBLICO:4000/health +``` + +Resultado esperado: +- container `linegestao-api` com status `healthy`; +- container `linegestao-db` com status `healthy`; +- endpoint `/health` respondendo `HTTP/1.1 200 OK`. + +## Domínio (DNS + proxy reverso) +Com o compose atual, a API está disponível em `http://SEU_IP:4000`. +Para usar domínio em produção, faça: + +1. Crie/ajuste o DNS do domínio para apontar para o IP do servidor (registro `A`). +2. Coloque um proxy reverso na frente da API (Nginx, Traefik ou Caddy). +3. Emita TLS/HTTPS (Let's Encrypt). +4. Atualize `FRONTEND_PUBLIC_URL` no `.env` para a URL pública do front-end. + +Sem proxy+HTTPS, o domínio pode até abrir via HTTP, mas não é recomendado para produção. + +## Atualizando um clone já existente no servidor + +```bash +cd ~/apps/line-gestao-api +git fetch --all --prune +git pull --rebase origin NOME_DA_BRANCH +docker compose up -d --build +``` + ## Observações - O Postgres **não é exposto** para fora do Docker (sem `5432:5432`). - A API sobe na porta `4000` do host (`4000:8080`). From 3202899d76eff995a2714310e5cceef3f5369f3c Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:03:55 -0300 Subject: [PATCH 4/5] Add production Traefik compose and domain deployment steps --- .env.example | 4 +++ DEPLOY.md | 67 ++++++++++++++++++++++++++++++-------- docker-compose.prod.yml | 71 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 docker-compose.prod.yml diff --git a/.env.example b/.env.example index 2e94369..e0a8902 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,7 @@ POSTGRES_PASSWORD=CHANGE_ME_TO_A_STRONG_PASSWORD # Front-end URL used by CORS FRONTEND_PUBLIC_URL=https://seu-dominio.com + +# Domain / HTTPS (docker-compose.prod.yml) +API_DOMAIN=api.seu-dominio.com +ACME_EMAIL=seu-email@seu-dominio.com diff --git a/DEPLOY.md b/DEPLOY.md index 1e36704..9cd4a18 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -5,7 +5,7 @@ Esse erro acontece quando o `docker compose` é executado em uma pasta sem `dock Este repositório inclui `docker-compose.yml` na raiz do projeto. -## Passo a passo +## Opção 1: subir via IP (rápido) 1. Entre na pasta da API: @@ -39,6 +39,41 @@ docker compose logs -f --tail=200 curl -I http://SEU_SERVIDOR:4000/health ``` +## Opção 2: subir já com domínio + HTTPS automático (Traefik) + +### Pré-requisitos +- O DNS do domínio/subdomínio da API deve apontar para o IP do servidor (registro `A`). +- Portas 80 e 443 abertas no firewall/security group. + +### 1) Ajuste o `.env` +Na pasta da API: + +```bash +cd ~/apps/line-gestao-api +cp .env.example .env +nano .env +``` + +Preencha obrigatoriamente: +- `API_DOMAIN` (ex.: `api.seudominio.com`) +- `ACME_EMAIL` (email para emitir certificado) +- `FRONTEND_PUBLIC_URL` (URL pública do front) +- `JWT_KEY`, `SEED_ADMIN_*`, `POSTGRES_PASSWORD` + +### 2) Suba em modo produção com Traefik + +```bash +docker compose -f docker-compose.prod.yml up -d --build +``` + +### 3) Verifique saúde e certificado + +```bash +docker compose -f docker-compose.prod.yml ps +docker compose -f docker-compose.prod.yml logs -f --tail=200 +curl -i https://SEU_API_DOMAIN/health +``` + ## Checklist de confirmação (API realmente no ar) Considere que o deploy está concluído quando **todos** os itens abaixo estiverem OK: @@ -48,32 +83,38 @@ curl -i http://127.0.0.1:4000/health curl -i http://SEU_IP_PUBLICO:4000/health ``` +Para produção com domínio: + +```bash +docker compose -f docker-compose.prod.yml ps +curl -i https://SEU_API_DOMAIN/health +``` + Resultado esperado: - container `linegestao-api` com status `healthy`; - container `linegestao-db` com status `healthy`; - endpoint `/health` respondendo `HTTP/1.1 200 OK`. -## Domínio (DNS + proxy reverso) -Com o compose atual, a API está disponível em `http://SEU_IP:4000`. -Para usar domínio em produção, faça: - -1. Crie/ajuste o DNS do domínio para apontar para o IP do servidor (registro `A`). -2. Coloque um proxy reverso na frente da API (Nginx, Traefik ou Caddy). -3. Emita TLS/HTTPS (Let's Encrypt). -4. Atualize `FRONTEND_PUBLIC_URL` no `.env` para a URL pública do front-end. - -Sem proxy+HTTPS, o domínio pode até abrir via HTTP, mas não é recomendado para produção. - ## Atualizando um clone já existente no servidor ```bash cd ~/apps/line-gestao-api git fetch --all --prune git pull --rebase origin NOME_DA_BRANCH +``` + +Depois de atualizar, suba com o arquivo que você usa: + +```bash +# modo IP docker compose up -d --build + +# modo domínio + HTTPS +docker compose -f docker-compose.prod.yml up -d --build ``` ## Observações - O Postgres **não é exposto** para fora do Docker (sem `5432:5432`). -- A API sobe na porta `4000` do host (`4000:8080`). +- No modo IP, a API sobe na porta `4000` do host (`4000:8080`). +- No modo domínio, a API fica atrás do Traefik em `443` (HTTPS). - Defina valores fortes para `JWT_KEY`, `SEED_ADMIN_PASSWORD` e `POSTGRES_PASSWORD`. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..ce15ba8 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,71 @@ +services: + traefik: + image: traefik:v3.3 + container_name: linegestao-traefik + restart: unless-stopped + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.letsencrypt.acme.tlschallenge=true + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:?ACME_EMAIL is required} + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik_letsencrypt:/letsencrypt + + db: + image: postgres:16-alpine + container_name: linegestao-db + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-linegestao} + POSTGRES_USER: ${POSTGRES_USER:-linegestao_app} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-linegestao_app} -d ${POSTGRES_DB:-linegestao}"] + interval: 10s + timeout: 5s + retries: 10 + + api: + build: + context: . + dockerfile: Dockerfile + container_name: linegestao-api + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production} + App__UseHttpsRedirection: ${APP_USE_HTTPS_REDIRECTION:-false} + ConnectionStrings__Default: Host=db;Port=5432;Database=${POSTGRES_DB:-linegestao};Username=${POSTGRES_USER:-linegestao_app};Password=${POSTGRES_PASSWORD} + Jwt__Key: ${JWT_KEY:?JWT_KEY is required} + Jwt__Issuer: ${JWT_ISSUER:-LineGestao} + Jwt__Audience: ${JWT_AUDIENCE:-LineGestao} + Seed__AdminEmail: ${SEED_ADMIN_EMAIL:?SEED_ADMIN_EMAIL is required} + Seed__AdminPassword: ${SEED_ADMIN_PASSWORD:?SEED_ADMIN_PASSWORD is required} + Cors__AllowedOrigins__0: ${FRONTEND_PUBLIC_URL:?FRONTEND_PUBLIC_URL is required} + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] + interval: 20s + timeout: 5s + retries: 10 + start_period: 20s + labels: + - traefik.enable=true + - traefik.http.routers.linegestao-api.rule=Host(`${API_DOMAIN:?API_DOMAIN is required}`) + - traefik.http.routers.linegestao-api.entrypoints=websecure + - traefik.http.routers.linegestao-api.tls.certresolver=letsencrypt + - traefik.http.services.linegestao-api.loadbalancer.server.port=8080 + +volumes: + pg_data: + traefik_letsencrypt: From fd6e99e18bb68ab276cf08b4c341c709dbf39abe Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:36:30 -0300 Subject: [PATCH 5/5] Replace Traefik with Caddy in production compose --- .env.example | 2 +- Caddyfile | 8 ++++++++ DEPLOY.md | 8 +++++--- docker-compose.prod.yml | 34 ++++++++++++++-------------------- 4 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 Caddyfile diff --git a/.env.example b/.env.example index e0a8902..dd789cf 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,6 @@ POSTGRES_PASSWORD=CHANGE_ME_TO_A_STRONG_PASSWORD # Front-end URL used by CORS FRONTEND_PUBLIC_URL=https://seu-dominio.com -# Domain / HTTPS (docker-compose.prod.yml) +# Domain / HTTPS (docker-compose.prod.yml with Caddy) API_DOMAIN=api.seu-dominio.com ACME_EMAIL=seu-email@seu-dominio.com diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..f320404 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,8 @@ +{ + email {$ACME_EMAIL} +} + +{$API_DOMAIN} { + encode zstd gzip + reverse_proxy api:8080 +} diff --git a/DEPLOY.md b/DEPLOY.md index 9cd4a18..5a715fe 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -39,12 +39,14 @@ docker compose logs -f --tail=200 curl -I http://SEU_SERVIDOR:4000/health ``` -## Opção 2: subir já com domínio + HTTPS automático (Traefik) +## Opção 2: subir já com domínio + HTTPS automático (Caddy) ### Pré-requisitos - O DNS do domínio/subdomínio da API deve apontar para o IP do servidor (registro `A`). - Portas 80 e 443 abertas no firewall/security group. +> Se você viu erro do tipo `client version 1.24 is too old` no Traefik, use este modo com Caddy: ele não depende da API Docker para criar rotas e evita esse problema. + ### 1) Ajuste o `.env` Na pasta da API: @@ -60,7 +62,7 @@ Preencha obrigatoriamente: - `FRONTEND_PUBLIC_URL` (URL pública do front) - `JWT_KEY`, `SEED_ADMIN_*`, `POSTGRES_PASSWORD` -### 2) Suba em modo produção com Traefik +### 2) Suba em modo produção com Caddy ```bash docker compose -f docker-compose.prod.yml up -d --build @@ -116,5 +118,5 @@ docker compose -f docker-compose.prod.yml up -d --build ## Observações - O Postgres **não é exposto** para fora do Docker (sem `5432:5432`). - No modo IP, a API sobe na porta `4000` do host (`4000:8080`). -- No modo domínio, a API fica atrás do Traefik em `443` (HTTPS). +- No modo domínio, a API fica atrás do Caddy em `443` (HTTPS). - Defina valores fortes para `JWT_KEY`, `SEED_ADMIN_PASSWORD` e `POSTGRES_PASSWORD`. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ce15ba8..d347633 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,22 +1,21 @@ services: - traefik: - image: traefik:v3.3 - container_name: linegestao-traefik + caddy: + image: caddy:2.10-alpine + container_name: linegestao-caddy restart: unless-stopped - command: - - --providers.docker=true - - --providers.docker.exposedbydefault=false - - --entrypoints.web.address=:80 - - --entrypoints.websecure.address=:443 - - --certificatesresolvers.letsencrypt.acme.tlschallenge=true - - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:?ACME_EMAIL is required} - - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json ports: - "80:80" - "443:443" + environment: + API_DOMAIN: ${API_DOMAIN:?API_DOMAIN is required} + ACME_EMAIL: ${ACME_EMAIL:?ACME_EMAIL is required} volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - traefik_letsencrypt:/letsencrypt + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + api: + condition: service_healthy db: image: postgres:16-alpine @@ -59,13 +58,8 @@ services: timeout: 5s retries: 10 start_period: 20s - labels: - - traefik.enable=true - - traefik.http.routers.linegestao-api.rule=Host(`${API_DOMAIN:?API_DOMAIN is required}`) - - traefik.http.routers.linegestao-api.entrypoints=websecure - - traefik.http.routers.linegestao-api.tls.certresolver=letsencrypt - - traefik.http.services.linegestao-api.loadbalancer.server.port=8080 volumes: pg_data: - traefik_letsencrypt: + caddy_data: + caddy_config: