Staging to Production #51

Merged
claude merged 165 commits from main into production 2026-06-08 18:28:46 +00:00
3 changed files with 64 additions and 21 deletions
Showing only changes of commit ba92c9f793 - Show all commits
+22 -10
View File
@@ -35,7 +35,7 @@ This applies to both the staging and production repos as appropriate.
- Entity Framework Core + SQL Server (multi-schema)
- Refit for typed HTTP clients between services
- Serilog (JSON structured logging, Console + File + Email sinks)
- MailKit for SMTP
- MailKit for SMTP (used exclusively in `email-api`)
- Docker Compose for local and production deployment
- Watchtower for automatic container updates in production
@@ -63,6 +63,8 @@ This applies to both the staging and production repos as appropriate.
Apis/
api/ Public-facing proxy API (port 8080). Handles CORS, rate limiting, captcha, email.
api-models/ DTOs and settings for api only.
email-api/ Internal SMTP email relay (no public port). All email sending goes here.
email-api-models/ Refit client + SendEmailRequest + EmailApiSettings (shared by api and cv-search-job).
cv-matcher-api/ Internal CV match engine (port 8082). Runs CvMatcher + CvSearch + MyAi DB migrations.
cv-matcher-api-models/ DTOs shared between api and cv-matcher-api (incl. JobSearchSettings).
rag-api/ Internal RAG/vector-search service (port 8081).
@@ -148,26 +150,36 @@ EF tools version warning ("older than runtime") is expected and harmless. The `H
```
web → api → cv-matcher-api → rag-api
cv-search-job
↓ ↓
| email-api
↓ ↑
cv-search-job
```
`api` and `cv-search-job` both call `email-api` for all outbound email (SMTP).
`api` never talks directly to `rag-api` — always via `cv-matcher-api`.
## Internal API key auth
All internal service-to-service calls require the `X-Internal-Api-Key` header.
The key is shared via the `CvMatcherApi__InternalApiKey` and `RagApi__InternalApiKey` env vars.
`startup-helpers` provides `UseInternalApiKeyProtection()` middleware that enforces it on `cv-matcher-api` and `rag-api`.
All internal service-to-service calls require the `X-Internal-Api-Key` header.
| Caller | Target | Env var for key |
|--------|--------|-----------------|
| `api`, `cv-search-job` | `email-api` | `EmailApi__InternalApiKey` |
| `api`, `cv-search-job` | `cv-matcher-api` | `CvMatcherApi__InternalApiKey` |
| `cv-matcher-api` | `rag-api` | `RagApi__InternalApiKey` |
`startup-helpers` provides `UseInternalApiKeyProtection()` middleware (reads `InternalApi:ApiKey`); enforced on `cv-matcher-api`, `rag-api`, and `email-api`.
## Shared file storage
CV PDFs are written by `api` to `Apis/api/Files/` and read by `cv-cleanup-job` and `cv-search-job`.
All three containers mount the same bind volume:
CV PDFs are written by `api` to `Apis/api/Files/` and read by `cv-cleanup-job`, `cv-search-job`, and `email-api` (for email attachments).
All four containers mount the same bind volume:
```yaml
- ../Apis/api/Files:/app/Files
- ${FILES_PATH:-/opt/myai/files}:/app/Files
```
The path inside containers is controlled by `FileStorage__Path` (default: `Files`).
The path inside containers is controlled by `FileStorage__Path` (default: `Files`).
`email-api` receives only the relative filename (e.g. `abc123.pdf`) and resolves it against `FileStorage__Path`.
## Job task pattern
+41 -10
View File
@@ -100,11 +100,47 @@ services:
labels:
- "com.centurylinklabs.watchtower.enable=true"
email-api:
image: registry.easysoft.ro/apps/myai-email-api:${IMAGE_TAG:-staging}
container_name: myai-email-api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
- InternalApi__ApiKey=${EmailApi__InternalApiKey:-}
- InternalApi__RequireApiKey=true
- Smtp__Host=${Smtp__Host:-}
- Smtp__Port=${Smtp__Port:-587}
- Smtp__Username=${Smtp__Username:-}
- Smtp__Password=${Smtp__Password:-}
- Smtp__UseStartTls=${Smtp__UseStartTls:-false}
- FileStorage__Path=${FileStorage__Path:-Files}
- Serilog__WriteTo__2__Args__fromEmail=${Serilog__WriteTo__2__Args__fromEmail:-}
- Serilog__WriteTo__2__Args__toEmail=${Serilog__WriteTo__2__Args__toEmail:-}
- Serilog__WriteTo__2__Args__mailServer=${Serilog__WriteTo__2__Args__mailServer:-}
- Serilog__WriteTo__2__Args__networkCredential__userName=${Serilog__WriteTo__2__Args__networkCredential__userName:-}
- Serilog__WriteTo__2__Args__networkCredential__password=${Serilog__WriteTo__2__Args__networkCredential__password:-}
- Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587}
- Serilog__WriteTo__2__Args__enableSsl=${Serilog__WriteTo__2__Args__enableSsl:-true}
volumes:
- ${LOGS_PATH:-/opt/myai/logs}/email-api:/app/logs
- ${FILES_PATH:-/opt/myai/files}:/app/Files
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
api:
image: registry.easysoft.ro/apps/myai-api:${IMAGE_TAG:-staging}
container_name: myai-api
depends_on:
- cv-matcher-api
- email-api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
@@ -126,11 +162,8 @@ services:
- Subscribe__ToEmail=${Subscribe__ToEmail:-}
- Subscribe__SubjectPrefix=${Subscribe__SubjectPrefix:-}
- Smtp__Host=${Smtp__Host:-}
- Smtp__Port=${Smtp__Port:-587}
- Smtp__Username=${Smtp__Username:-}
- Smtp__Password=${Smtp__Password:-}
- Smtp__UseStartTls=${Smtp__UseStartTls:-false}
- EmailApi__BaseUrl=${EmailApi__BaseUrl:-http://email-api:8080}
- EmailApi__InternalApiKey=${EmailApi__InternalApiKey:-}
- Captcha__Provider=${Captcha__Provider:-Recaptcha}
- Captcha__SecretKey=${Captcha__SecretKey:-}
@@ -210,6 +243,7 @@ services:
container_name: myai-cv-search-job
depends_on:
- cv-matcher-api
- email-api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
@@ -224,11 +258,8 @@ services:
- CvMatcherApi__BaseUrl=${CvMatcherApi__BaseUrl:-http://cv-matcher-api:8080}
- CvMatcherApi__InternalApiKey=${CvMatcherApi__InternalApiKey:-}
- Smtp__Host=${Smtp__Host:-}
- Smtp__Port=${Smtp__Port:-587}
- Smtp__Username=${Smtp__Username:-}
- Smtp__Password=${Smtp__Password:-}
- Smtp__UseStartTls=${Smtp__UseStartTls:-false}
- EmailApi__BaseUrl=${EmailApi__BaseUrl:-http://email-api:8080}
- EmailApi__InternalApiKey=${EmailApi__InternalApiKey:-}
- Contact__ToEmail=${Contact__ToEmail:-}
+1 -1
View File
@@ -368,7 +368,7 @@ Global
{92CA82EB-E558-44E7-9185-6FF8B8299C2A} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012}
{02DE69CD-19E6-43C0-8916-DB98E5B5CA89} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
{069365DB-1916-4C38-A90D-5E909BD9EDD0} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
{BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
{BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
{434119EA-2FFC-4433-9B8E-1E6D94006413} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution