diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index af7e496..49a72e3 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -91,47 +91,49 @@ jobs: docker-compose.prod.yml \ ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}:/opt/saas-imobiliaria/docker-compose.prod.yml - - name: Deploy on server + - name: Deploy on server (Docker Swarm stack) run: | ssh -i ~/.ssh/deploy_key \ -p ${{ vars.SSH_PORT || '22' }} \ ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} \ - bash -s << 'ENDSSH' + bash -s << ENDSSH set -e DEPLOY_DIR="/opt/saas-imobiliaria" - mkdir -p "$DEPLOY_DIR" - cd "$DEPLOY_DIR" + mkdir -p "\$DEPLOY_DIR" + cd "\$DEPLOY_DIR" - # Write .env for docker compose - cat > .env << EOF - IMAGE_TAG=${{ needs.build.outputs.image_tag }} - REGISTRY=${{ vars.REGISTRY }} - DOMAIN=${{ vars.DOMAIN }} - POSTGRES_DB=${{ secrets.POSTGRES_DB }} - POSTGRES_USER=${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} - SECRET_KEY=${{ secrets.SECRET_KEY }} - JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} - EOF - - # Log in to registry on the server + # Log in to registry echo "${{ secrets.REGISTRY_PASSWORD }}" | \ docker login ${{ vars.REGISTRY }} \ -u ${{ secrets.REGISTRY_USER }} \ --password-stdin - # Pull new images - docker compose -f docker-compose.prod.yml pull + # Pull images explicitly so swarm has them cached + docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-backend:${{ needs.build.outputs.image_tag }} + docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-frontend:${{ needs.build.outputs.image_tag }} - # Rolling restart (zero-downtime: db stays up) - docker compose -f docker-compose.prod.yml up -d --remove-orphans + # Deploy stack with env vars inline + IMAGE_TAG=${{ needs.build.outputs.image_tag }} \ + REGISTRY=${{ vars.REGISTRY }} \ + DOMAIN=${{ vars.DOMAIN }} \ + POSTGRES_DB=${{ secrets.POSTGRES_DB }} \ + POSTGRES_USER=${{ secrets.POSTGRES_USER }} \ + POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} \ + SECRET_KEY=${{ secrets.SECRET_KEY }} \ + JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} \ + docker stack deploy \ + --compose-file docker-compose.prod.yml \ + --with-registry-auth \ + --prune \ + saas-imobiliaria - # Clean up old images - docker image prune -f + echo "Stack deployed: ${{ needs.build.outputs.image_tag }}" - echo "Deploy concluído: ${{ needs.build.outputs.image_tag }}" + # Wait for services to converge + sleep 10 + docker stack services saas-imobiliaria ENDSSH diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0780dce..c49f13e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -15,10 +15,13 @@ services: retries: 10 networks: - internal + deploy: + replicas: 1 + restart_policy: + condition: on-failure backend: image: ${REGISTRY}/saas-imobiliaria-backend:${IMAGE_TAG:-latest} - restart: unless-stopped environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} SECRET_KEY: ${SECRET_KEY} @@ -27,55 +30,50 @@ services: FLASK_APP: app CORS_ORIGINS: https://${DOMAIN} IMAGE_TAG: ${IMAGE_TAG:-latest} - depends_on: - db: - condition: service_healthy networks: - internal - - traefik-public - labels: - - "traefik.enable=true" - - "traefik.docker.network=traefik-public" - # Router - - "traefik.http.routers.imob-api.rule=Host(`${DOMAIN}`) && PathPrefix(`/api`)" - - "traefik.http.routers.imob-api.entrypoints=websecure" - - "traefik.http.routers.imob-api.tls=true" - - "traefik.http.routers.imob-api.tls.certresolver=letsencrypt" - # Service - - "traefik.http.services.imob-api.loadbalancer.server.port=5000" - # Strip /api prefix before forwarding to Flask - - "traefik.http.middlewares.imob-api-strip.stripprefix.prefixes=/api" - - "traefik.http.routers.imob-api.middlewares=imob-api-strip" + - proxy + deploy: + replicas: 1 + restart_policy: + condition: on-failure + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + # Router HTTPS + - "traefik.http.routers.imob-api.rule=Host(`${DOMAIN}`) && PathPrefix(`/api`)" + - "traefik.http.routers.imob-api.entrypoints=websecure" + - "traefik.http.routers.imob-api.tls=true" + - "traefik.http.routers.imob-api.tls.certresolver=letsencrypt" + - "traefik.http.routers.imob-api.middlewares=imob-strip-api" + # Strip /api prefix antes de chegar no Flask + - "traefik.http.middlewares.imob-strip-api.stripprefix.prefixes=/api" + # Service + - "traefik.http.services.imob-api.loadbalancer.server.port=5000" frontend: image: ${REGISTRY}/saas-imobiliaria-frontend:${IMAGE_TAG:-latest} - restart: unless-stopped - depends_on: - - backend networks: - - internal - - traefik-public - labels: - - "traefik.enable=true" - - "traefik.docker.network=traefik-public" - # Router - - "traefik.http.routers.imob-frontend.rule=Host(`${DOMAIN}`)" - - "traefik.http.routers.imob-frontend.entrypoints=websecure" - - "traefik.http.routers.imob-frontend.tls=true" - - "traefik.http.routers.imob-frontend.tls.certresolver=letsencrypt" - # Redirect HTTP → HTTPS - - "traefik.http.routers.imob-frontend-http.rule=Host(`${DOMAIN}`)" - - "traefik.http.routers.imob-frontend-http.entrypoints=web" - - "traefik.http.routers.imob-frontend-http.middlewares=redirect-to-https" - - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" - # Service - - "traefik.http.services.imob-frontend.loadbalancer.server.port=80" + - proxy + deploy: + replicas: 1 + restart_policy: + condition: on-failure + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + # Router HTTPS + - "traefik.http.routers.imob-frontend.rule=Host(`${DOMAIN}`)" + - "traefik.http.routers.imob-frontend.entrypoints=websecure" + - "traefik.http.routers.imob-frontend.tls=true" + - "traefik.http.routers.imob-frontend.tls.certresolver=letsencrypt" + # Service + - "traefik.http.services.imob-frontend.loadbalancer.server.port=80" networks: internal: - driver: bridge - traefik-public: + driver: overlay + proxy: external: true volumes: