chore: deploy

This commit is contained in:
Gabriel Santana de Menezes 2026-05-11 22:02:05 -04:00
parent 8e5d29c7a3
commit b87ab63ac2
10 changed files with 326 additions and 13 deletions

147
.github/workflows/deploy-main.yml vendored Normal file
View file

@ -0,0 +1,147 @@
name: Deploy to Hostinger
on:
push:
branches:
- main
workflow_dispatch:
env:
REGISTRY: ghcr.io
FRONTEND_IMAGE_NAME: ${{ github.repository }}-frontend
BACKEND_IMAGE_NAME: ${{ github.repository }}-backend
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Run backend tests
working-directory: backend
run: ./mvnw -B test
- name: Run frontend build
working-directory: frontend
run: |
npm ci
npm run build
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata for frontend
id: meta-frontend
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha
- name: Extract Docker metadata for backend
id: meta-backend
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push frontend image
uses: docker/build-push-action@v6
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
tags: ${{ steps.meta-frontend.outputs.tags }}
labels: ${{ steps.meta-frontend.outputs.labels }}
build-args: |
VITE_API_URL=${{ secrets.FRONTEND_VITE_API_URL }}
- name: Build and push backend image
uses: docker/build-push-action@v6
with:
context: ./backend
file: ./backend/Dockerfile
push: true
tags: ${{ steps.meta-backend.outputs.tags }}
labels: ${{ steps.meta-backend.outputs.labels }}
- name: Copy deploy files to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.HOSTINGER_HOST }}
username: ${{ secrets.HOSTINGER_USER }}
key: ${{ secrets.HOSTINGER_SSH_KEY }}
port: ${{ secrets.HOSTINGER_PORT || 22 }}
source: "deploy/docker-compose.prod.yml"
target: "${{ secrets.HOSTINGER_APP_PATH }}"
strip_components: 1
- name: Deploy on Hostinger
uses: appleboy/ssh-action@v1.2.0
env:
FRONTEND_IMAGE: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:latest
BACKEND_IMAGE: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:latest
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
DB_URL: ${{ secrets.DB_URL }}
MP_JWT_VERIFY_ISSUER: ${{ secrets.MP_JWT_VERIFY_ISSUER }}
SMALLRYE_JWT_SIGN_KEY_LOCATION: ${{ secrets.SMALLRYE_JWT_SIGN_KEY_LOCATION }}
MP_JWT_VERIFY_PUBLICKEY_LOCATION: ${{ secrets.MP_JWT_VERIFY_PUBLICKEY_LOCATION }}
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
GHCR_TOKEN: ${{ secrets.GHCR_READ_TOKEN }}
with:
host: ${{ secrets.HOSTINGER_HOST }}
username: ${{ secrets.HOSTINGER_USER }}
key: ${{ secrets.HOSTINGER_SSH_KEY }}
port: ${{ secrets.HOSTINGER_PORT || 22 }}
envs: FRONTEND_IMAGE,BACKEND_IMAGE,POSTGRES_USER,POSTGRES_PASSWORD,DB_URL,MP_JWT_VERIFY_ISSUER,SMALLRYE_JWT_SIGN_KEY_LOCATION,MP_JWT_VERIFY_PUBLICKEY_LOCATION,GHCR_USERNAME,GHCR_TOKEN
script: |
set -eu
APP_DIR="${{ secrets.HOSTINGER_APP_PATH }}"
mkdir -p "$APP_DIR"
cd "$APP_DIR"
cat > .env.production <<EOF
POSTGRES_USER=${POSTGRES_USER}
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
DB_URL=${DB_URL}
FRONTEND_IMAGE=${FRONTEND_IMAGE}
BACKEND_IMAGE=${BACKEND_IMAGE}
MP_JWT_VERIFY_ISSUER=${MP_JWT_VERIFY_ISSUER}
SMALLRYE_JWT_SIGN_KEY_LOCATION=${SMALLRYE_JWT_SIGN_KEY_LOCATION}
MP_JWT_VERIFY_PUBLICKEY_LOCATION=${MP_JWT_VERIFY_PUBLICKEY_LOCATION}
EOF
echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
docker compose --env-file .env.production -f docker-compose.prod.yml pull
docker compose --env-file .env.production -f docker-compose.prod.yml up -d
docker image prune -f

View file

@ -0,0 +1,32 @@
# GitHub Secrets Checklist
Use esta lista para acompanhar quais `GitHub Secrets` do deploy ja foram cadastrados no repositório.
## Acesso ao servidor
- [x] `HOSTINGER_HOST`
- [x] `HOSTINGER_PORT`
- [x] `HOSTINGER_USER`
- [x] `HOSTINGER_SSH_KEY`
- [x] `HOSTINGER_APP_PATH`
## Registro de imagens
- [x] `GHCR_USERNAME`
- [x] `GHCR_READ_TOKEN`
## Frontend
- [x] `FRONTEND_VITE_API_URL`
## Banco de dados
- [x] `POSTGRES_USER`
- [x] `POSTGRES_PASSWORD`
- [x] `DB_URL`
## Backend / JWT
- [ ] `MP_JWT_VERIFY_ISSUER`
- [ ] `SMALLRYE_JWT_SIGN_KEY_LOCATION`
- [ ] `MP_JWT_VERIFY_PUBLICKEY_LOCATION`

View file

@ -105,3 +105,53 @@ Frontend:
cd frontend cd frontend
npm run build npm run build
``` ```
## Deploy automatizado
O repositório agora inclui um workflow em `.github/workflows/deploy-main.yml` para deploy automático quando houver push na branch `main`.
Fluxo:
- roda testes do backend
- roda build do frontend
- gera e publica imagens Docker no GHCR
- copia `deploy/docker-compose.prod.yml` para o servidor
- conecta via SSH na Hostinger e executa `docker compose pull && docker compose up -d`
Arquivos de apoio:
- `backend/Dockerfile`: imagem de produção do backend
- `frontend/Dockerfile`: build do Vite + entrega por Nginx
- `frontend/nginx.conf`: fallback SPA
- `deploy/docker-compose.prod.yml`: stack de produção
- `deploy/.env.production.example`: variáveis esperadas no servidor
Secrets necessários no GitHub:
- `HOSTINGER_HOST`: IP ou domínio do servidor
- `HOSTINGER_PORT`: porta SSH, normalmente `22`
- `HOSTINGER_USER`: usuário SSH
- `HOSTINGER_SSH_KEY`: chave privada SSH usada no acesso ao servidor
- `HOSTINGER_APP_PATH`: diretório remoto onde o deploy será mantido, por exemplo `/home/user/apps/beetask`
- `GHCR_USERNAME`: usuário dono do pacote no GHCR
- `GHCR_READ_TOKEN`: token com permissão de `read:packages` para o servidor fazer pull das imagens
- `FRONTEND_VITE_API_URL`: URL que o frontend usará para a API, por exemplo `https://api.seudominio.com` ou `/api`
- `POSTGRES_USER`
- `POSTGRES_PASSWORD`
- `DB_URL`: JDBC URL completa do banco, por exemplo `jdbc:postgresql://203.0.113.10:5432/beetask`
- `MP_JWT_VERIFY_ISSUER`
- `SMALLRYE_JWT_SIGN_KEY_LOCATION`
- `MP_JWT_VERIFY_PUBLICKEY_LOCATION`
Pré-requisitos no servidor:
- Docker instalado
- Docker Compose plugin disponível via `docker compose`
- pasta definida em `HOSTINGER_APP_PATH` com permissão de escrita para o usuário SSH
- caso as imagens GHCR sejam privadas, o `GHCR_READ_TOKEN` precisa conseguir fazer pull
Observações:
- o workflow usa a tag `latest` para o deploy em produção
- a rota do frontend no container está exposta na porta `3000` do servidor e o backend na `8080`; normalmente você vai colocar Nginx, Caddy ou o proxy da Hostinger na frente dessas portas
- se quiser servir frontend e backend em domínios diferentes, ajuste `FRONTEND_VITE_API_URL`

View file

@ -2,10 +2,3 @@
DB_USER=beetask DB_USER=beetask
DB_PASSWORD=beetask DB_PASSWORD=beetask
DB_URL=jdbc:postgresql://localhost:5432/beetask DB_URL=jdbc:postgresql://localhost:5432/beetask
# Optional overrides
# QUARKUS_HTTP_PORT=8080
# QUARKUS_HTTP_CORS_ORIGINS=http://localhost:5173
# MP_JWT_VERIFY_ISSUER=https://beetech.net
# SMALLRYE_JWT_SIGN_KEY_LOCATION=privateKey.pem
# MP_JWT_VERIFY_PUBLICKEY_LOCATION=publicKey.pem

28
backend/Dockerfile Normal file
View file

@ -0,0 +1,28 @@
FROM maven:3.9.11-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw
RUN ./mvnw -B dependency:go-offline
COPY src src
RUN ./mvnw -B clean package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/quarkus-app/lib/ ./lib/
COPY --from=build /app/target/quarkus-app/*.jar ./
COPY --from=build /app/target/quarkus-app/app/ ./app/
COPY --from=build /app/target/quarkus-app/quarkus/ ./quarkus/
EXPOSE 8080
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENTRYPOINT ["java", "-jar", "/app/quarkus-run.jar"]

View file

@ -1,7 +1,6 @@
quarkus.http.port=8080 quarkus.http.port=8080
quarkus.http.cors=true quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:5173 quarkus.http.cors.origins=*
quarkus.datasource.db-kind=postgresql quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=${DB_USER:beetask} quarkus.datasource.username=${DB_USER:beetask}
quarkus.datasource.password=${DB_PASSWORD:beetask} quarkus.datasource.password=${DB_PASSWORD:beetask}
@ -17,6 +16,6 @@ quarkus.swagger-ui.always-include=true
quarkus.smallrye-health.root-path=/status quarkus.smallrye-health.root-path=/status
# JWT Security # JWT Security
mp.jwt.verify.issuer=https://beetech.net mp.jwt.verify.issuer=${MP_JWT_VERIFY_ISSUER}
smallrye.jwt.sign.key.location=privateKey.pem smallrye.jwt.sign.key.location=${SMALLRYE_JWT_SIGN_KEY_LOCATION}
mp.jwt.verify.publickey.location=publicKey.pem mp.jwt.verify.publickey.location=${MP_JWT_VERIFY_PUBLICKEY_LOCATION}

View file

@ -0,0 +1,11 @@
POSTGRES_USER=beetask
POSTGRES_PASSWORD=change-me
DB_URL=jdbc:postgresql://203.0.113.10:5432/beetask
BACKEND_IMAGE=ghcr.io/owner/repo-backend:latest
FRONTEND_IMAGE=ghcr.io/owner/repo-frontend:latest
QUARKUS_HTTP_CORS_ORIGINS=https://app.example.com
MP_JWT_VERIFY_ISSUER=https://beetech.net
SMALLRYE_JWT_SIGN_KEY_LOCATION=privateKey.pem
MP_JWT_VERIFY_PUBLICKEY_LOCATION=publicKey.pem

View file

@ -0,0 +1,22 @@
services:
backend:
image: ${BACKEND_IMAGE}
restart: unless-stopped
environment:
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_URL: ${DB_URL}
QUARKUS_HTTP_PORT: 8080
MP_JWT_VERIFY_ISSUER: ${MP_JWT_VERIFY_ISSUER}
SMALLRYE_JWT_SIGN_KEY_LOCATION: ${SMALLRYE_JWT_SIGN_KEY_LOCATION}
MP_JWT_VERIFY_PUBLICKEY_LOCATION: ${MP_JWT_VERIFY_PUBLICKEY_LOCATION}
ports:
- "8080:8080"
frontend:
image: ${FRONTEND_IMAGE}
restart: unless-stopped
depends_on:
- backend
ports:
- "3000:80"

20
frontend/Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG VITE_API_URL=/api
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
FROM nginx:1.29-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

11
frontend/nginx.conf Normal file
View file

@ -0,0 +1,11 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}