diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index e59a8c8..cff62bb 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -7,10 +7,11 @@ on: env: REGISTRY: git.gabrielkaszewski.dev - IMAGE: git.gabrielkaszewski.dev/gkaszewski/thoughts + BACKEND_IMAGE: git.gabrielkaszewski.dev/gkaszewski/thoughts + FRONTEND_IMAGE: git.gabrielkaszewski.dev/gkaszewski/thoughts-frontend jobs: - build-and-push: + build-backend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -26,25 +27,61 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.IMAGE }} + images: ${{ env.BACKEND_IMAGE }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - - name: Build and push + - name: Build and push backend uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache - cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max + cache-from: type=registry,ref=${{ env.BACKEND_IMAGE }}:buildcache + cache-to: type=registry,ref=${{ env.BACKEND_IMAGE }}:buildcache,mode=max + + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FRONTEND_IMAGE }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push frontend + uses: docker/build-push-action@v6 + with: + context: ./thoughts-frontend + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} + NEXT_PUBLIC_SERVER_SIDE_API_URL=${{ secrets.NEXT_PUBLIC_SERVER_SIDE_API_URL }} + cache-from: type=registry,ref=${{ env.FRONTEND_IMAGE }}:buildcache + cache-to: type=registry,ref=${{ env.FRONTEND_IMAGE }}:buildcache,mode=max deploy: - needs: build-and-push + needs: [build-backend, build-frontend] runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' steps: @@ -55,5 +92,6 @@ jobs: username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} script: | - docker pull ${{ env.IMAGE }}:latest + docker pull ${{ env.BACKEND_IMAGE }}:latest + docker pull ${{ env.FRONTEND_IMAGE }}:latest docker compose -f /opt/thoughts/docker-compose.yml up -d diff --git a/compose.prod.yml b/compose.prod.yml index da2555f..0d4e9ea 100644 --- a/compose.prod.yml +++ b/compose.prod.yml @@ -1,6 +1,6 @@ services: database: - image: postgres:15-alpine + image: postgres:16-alpine container_name: thoughts-db restart: unless-stopped environment: @@ -17,19 +17,21 @@ services: networks: - internal - backend: - container_name: thoughts-backend - image: thoughts-backend:latest + api: + container_name: thoughts-api + image: registry.gabrielkaszewski.dev/thoughts:latest restart: unless-stopped environment: - - RUST_LOG=info - - RUST_BACKTRACE=1 - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${POSTGRES_DB} - - HOST=0.0.0.0 - - PORT=8000 - - PREFORK=1 - - AUTH_SECRET=${AUTH_SECRET} - - BASE_URL=https://thoughts.gabrielkaszewski.dev + RUST_LOG: info + RUST_ENV: production + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${POSTGRES_DB} + HOST: 0.0.0.0 + PORT: 8000 + JWT_SECRET: ${JWT_SECRET} + BASE_URL: ${BASE_URL} + NATS_URL: ${NATS_URL} + CORS_ORIGINS: ${CORS_ORIGINS:-*} + ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-false} depends_on: database: condition: service_healthy @@ -40,22 +42,41 @@ services: retries: 5 networks: - internal + - nats + + worker: + container_name: thoughts-worker + image: registry.gabrielkaszewski.dev/thoughts:latest + entrypoint: ["./thoughts-worker"] + restart: unless-stopped + environment: + RUST_LOG: info + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${POSTGRES_DB} + BASE_URL: ${BASE_URL} + NATS_URL: ${NATS_URL} + depends_on: + database: + condition: service_healthy + networks: + - internal + - nats frontend: container_name: thoughts-frontend - image: thoughts-frontend:latest + image: registry.gabrielkaszewski.dev/thoughts-frontend:latest restart: unless-stopped + environment: + NEXT_PUBLIC_SERVER_SIDE_API_URL: http://api:8000 + PORT: 3000 + HOSTNAME: 0.0.0.0 depends_on: - - backend + api: + condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000"] interval: 10s timeout: 5s retries: 5 - environment: - - NEXT_PUBLIC_SERVER_SIDE_API_URL=http://proxy/api - - PORT=3000 - - HOSTNAME=0.0.0.0 networks: - internal @@ -66,7 +87,7 @@ services: depends_on: frontend: condition: service_healthy - backend: + api: condition: service_healthy networks: - internal @@ -83,7 +104,13 @@ services: volumes: postgres_data: driver: local + networks: + # Shared NATS network — must already exist on the host (external: true). + # Set NATS_NETWORK env var to match your shared network name (default: nats). + nats: + name: ${NATS_NETWORK:-nats} + external: true traefik: name: traefik external: true diff --git a/thoughts-backend/README.md b/thoughts-backend/README.md index 5f3707e..2993145 100644 --- a/thoughts-backend/README.md +++ b/thoughts-backend/README.md @@ -1,129 +1,17 @@ -# clean-axum +# ⚠️ DEPRECATED — thoughts-backend (v1) -Axum scaffold with clean architecture. +> **This directory is the original v1 implementation and is no longer maintained.** +> It will be removed in a future release. -You probably don't need [Rust on Rails](https://github.com/loco-rs/loco). +## Use v2 instead -Refer to [this post](https://kigawas.me/posts/rustacean-clean-architecture-approach/) for rationale and background. +The active codebase lives at the **repository root** (`/crates/`). It is a complete rewrite with: -## Features +- Hexagonal (Ports & Adapters) architecture +- Full ActivityPub federation +- Remote actor discovery and profile browsing +- NATS JetStream event bus +- Clean REST API with content negotiation +- Next.js frontend (`/thoughts-frontend/`) -- [Axum](https://github.com/tokio-rs/axum) framework -- [SeaORM](https://github.com/SeaQL/sea-orm) domain models -- Completely separated API routers and DB-related logic (named "persistence" layer) -- Completely separated input parameters, queries and output schemas -- OpenAPI documentation ([Swagger UI](https://clean-axum.shuttleapp.rs/docs) and [Scalar](https://clean-axum.shuttleapp.rs/scalar)) powered by [Utoipa](https://github.com/juhaku/utoipa) -- Error handling with [Anyhow](https://github.com/dtolnay/anyhow) -- Custom parameter validation with [validator](https://github.com/Keats/validator) -- Optional [Shuttle](https://www.shuttle.rs/) runtime -- Optional [prefork](https://docs.rs/prefork/latest/prefork/) workers for maximizing performance on Linux - -## Module hierarchy - -### API logic - -- `api::routers`: Axum endpoints -- `api::error`: Models and traits for error handling -- `api::extractor` Custom Axum extractors - - `api::extractor::json`: `Json` for bodies and responses - - `api::extractor::valid`: `Valid` for JSON body validation -- `api::validation`: JSON validation model based on `validator` -- `api::models`: Non domain model API models - - `api::models::response`: JSON error response - -### OpenAPI documentation - -- `doc`: Utoipa doc declaration - -### API-agonistic application logic - -Main concept: Web framework is replaceable. - -All modules here should not include any specific API web framework logic. - -- `app::persistence`: DB manipulation (CRUD) functions -- `app::config`: DB or API server configuration -- `app::state`: APP state, e.g. DB connection -- `app::error`: APP errors used by `api::error`. e.g. "User not found" - -### DB/API-agnostic domain models - -Main concept: Database (Sqlite/MySQL/PostgreSQL) is replaceable. - -Except `models::domains` and `migration`, all modules are ORM library agnostic. - -- `models::domains`: SeaORM domain models -- `models::params`: Serde input parameters for creating/updating domain models in DB -- `models::schemas`: Serde output schemas for combining different domain models -- `models::queries`: Serde queries for filtering domain models -- `migration`: SeaORM migration files - -### Unit and integration tests - -- `tests::api`: API integration tests. Hierarchy is the same as `api::routers` -- `tests::app::persistence`: DB/ORM-related unit tests. Hierarchy is the same as `app::persistence` - -### Others - -- `utils`: Utility functions -- `main`: Tokio and Shuttle conditional entry point - -## Run - -### Start server - -```bash -cp .env.example .env -# touch dev.db -# cargo install sea-orm-cli -# sea-orm-cli migrate up -cargo run - -# or for production -cargo run --release -``` - -### Call API - -```bash -curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"username":"aaa"}' -curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"username":"abc"}' -curl http://localhost:3000/users\?username\=a -``` - -### OpenAPI doc (Swagger UI/Scalar) - -```bash -open http://localhost:3000/docs -open http://localhost:3000/scalar -``` - -## Start Shuttle local server - -```bash -# cargo install cargo-shuttle -cargo shuttle run -``` - -Make sure docker engine is running, otherwise: - -```bash -brew install colima docker -colima start -sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock -``` - -## Shuttle deployment - -```bash -cargo shuttle login -cargo shuttle deploy -``` - -## Benchmark - -```bash -# edit .env to use Postgres -cargo run --release -wrk --latency -t20 -c50 -d10s http://localhost:3000/users\?username\= -``` +Do not build, run, or modify anything in this directory.