Files
myAi/docker-compose/docker-compose.yml
T
claude dee6c5a205
Build and Push Docker Images / build (push) Successful in 20s
Build and Push Docker Images / smoke (push) Successful in 30s
ci: make the branch strategy enforced instead of merely intended
Same guard as the four site repos. myAi was the ONLY stack that kept its
environment variables through the 26 July migration, so it alone deployed
correctly all along -- but it shares the ${IMAGE_TAG:-staging} default that
made the others fail silently, so it gets the same treatment.

1. ${IMAGE_TAG:-staging} -> ${IMAGE_TAG:-IMAGE_TAG-NOT-SET} on all eight
   images: an unconfigured stack fails the pull rather than quietly becoming
   staging.

2. A smoke job that asks the deploy host what commit it serves, via a
   /version.json stamped into the web image at build time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpTYCBLH58XzrM7n3xPJ5N
2026-08-24 10:21:18 +00:00

363 lines
16 KiB
YAML

# ⚠️ The IMAGE_TAG fallback is a DELIBERATELY INVALID tag, not `staging`.
# On 2026-07-26 these stacks were recreated by hand and lost their environment
# variables. The old `${IMAGE_TAG:-staging}` then quietly resolved to `staging`, so the
# production host pulled staging images -- with no mail credentials and no recipient
# addresses -- and served them for a month. Nothing failed, because falling back to a
# real tag is indistinguishable from being configured. Now an unset IMAGE_TAG yields
# `IMAGE_TAG-NOT-SET`, the pull fails with "manifest not found", the running container
# is left untouched and the deploy goes red. Loud beats plausible.
services:
rag-api:
image: registry.easysoft.ro/apps/myai-rag-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET}
container_name: myai-rag-api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
- Database__Host=${Database__Host:-sqlserver}
- Database__Port=${Database__Port:-1433}
- Database__Name=${Database__Name:-MyAiDb}
- Database__User=${Database__User:-sa}
- Database__Password=${Database__Password:-}
- Database__TrustServerCertificate=${Database__TrustServerCertificate:-true}
- InternalApi__ApiKey=${RagApi__InternalApiKey:-}
- InternalApi__RequireApiKey=${RagApi__RequireApiKey:-true}
- Rag__MaxFileSizeMb=${Rag__MaxFileSizeMb:-8}
- Rag__ChunkSize=${Rag__ChunkSize:-900}
- Rag__ChunkOverlap=${Rag__ChunkOverlap:-150}
- Rag__MaxTextChars=${Rag__MaxTextChars:-60000}
- Rag__DefaultTopK=${Rag__DefaultTopK:-20}
- Rag__MaxTopK=${Rag__MaxTopK:-50}
- Rag__ClassifyWithAi=${Rag__ClassifyWithAi:-false}
- Ai__Provider=${Ai__Provider:-OpenAI}
- Ai__OpenAI__ApiKey=${Ai__OpenAI__ApiKey:-}
- Ai__OpenAI__ChatModel=${Ai__OpenAI__ChatModel:-gpt-4o-mini}
- Ai__OpenAI__EmbeddingModel=${Ai__OpenAI__EmbeddingModel:-text-embedding-3-small}
- Ai__OpenAI__TimeoutSeconds=${Ai__OpenAI__TimeoutSeconds:-90}
- Ai__Ollama__BaseUrl=${Ai__Ollama__BaseUrl:-http://host.docker.internal:11434}
- Ai__Ollama__ChatModel=${Ai__Ollama__ChatModel:-llama3.1:8b}
- Ai__Ollama__EmbeddingModel=${Ai__Ollama__EmbeddingModel:-nomic-embed-text}
- Ai__Ollama__TimeoutSeconds=${Ai__Ollama__TimeoutSeconds:-180}
- SerilogEmail__From=${SerilogEmail__From:-}
- SerilogEmail__To=${SerilogEmail__To:-}
- SerilogEmail__Host=${SerilogEmail__Host:-}
- SerilogEmail__Port=${SerilogEmail__Port:-587}
- SerilogEmail__UserName=${SerilogEmail__UserName:-}
- SerilogEmail__Password=${SerilogEmail__Password:-}
volumes:
- ${LOGS_PATH:-/opt/myai/logs}/rag-api:/app/logs
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
cv-matcher-api:
image: registry.easysoft.ro/apps/myai-cv-matcher-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET}
container_name: myai-cv-matcher-api
depends_on:
- rag-api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
- Database__Host=${Database__Host:-sqlserver}
- Database__Port=${Database__Port:-1433}
- Database__Name=${Database__Name:-MyAiDb}
- Database__User=${Database__User:-sa}
- Database__Password=${Database__Password:-}
- Database__TrustServerCertificate=${Database__TrustServerCertificate:-true}
- InternalApi__ApiKey=${CvMatcherApi__InternalApiKey:-}
- InternalApi__RequireApiKey=${CvMatcherApi__RequireApiKey:-true}
- RagApi__BaseUrl=${RagApi__BaseUrl:-http://rag-api:8080}
- RagApi__InternalApiKey=${RagApi__InternalApiKey:-}
- PageFetcherApi__BaseUrl=${PageFetcherApi__BaseUrl:-http://page-fetcher-api:8080}
- PageFetcherApi__InternalApiKey=${PageFetcherApi__InternalApiKey:-}
- Ai__Provider=${Ai__Provider:-OpenAI}
- Ai__OpenAI__ApiKey=${Ai__OpenAI__ApiKey:-}
- Ai__OpenAI__ChatModel=${Ai__OpenAI__ChatModel:-gpt-4o-mini}
- Ai__OpenAI__TimeoutSeconds=${Ai__OpenAI__TimeoutSeconds:-90}
- Ai__Ollama__BaseUrl=${Ai__Ollama__BaseUrl:-http://host.docker.internal:11434}
- Ai__Ollama__ChatModel=${Ai__Ollama__ChatModel:-llama3.1:8b}
- Ai__Ollama__TimeoutSeconds=${Ai__Ollama__TimeoutSeconds:-180}
- Matcher__TopK=${Matcher__TopK:-10}
- Matcher__DeepScoreTopN=${Matcher__DeepScoreTopN:-5}
- Matcher__MaxJobTextChars=${Matcher__MaxJobTextChars:-60000}
- SerilogEmail__From=${SerilogEmail__From:-}
- SerilogEmail__To=${SerilogEmail__To:-}
- SerilogEmail__Host=${SerilogEmail__Host:-}
- SerilogEmail__Port=${SerilogEmail__Port:-587}
- SerilogEmail__UserName=${SerilogEmail__UserName:-}
- SerilogEmail__Password=${SerilogEmail__Password:-}
volumes:
- ${LOGS_PATH:-/opt/myai/logs}/cv-matcher-api:/app/logs
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
email-api:
image: registry.easysoft.ro/apps/myai-email-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET}
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}
- Database__Host=${Database__Host:-sqlserver}
- Database__Port=${Database__Port:-1433}
- Database__Name=${Database__Name:-MyAiDb}
- Database__User=${Database__User:-sa}
- Database__Password=${Database__Password:-}
- Database__TrustServerCertificate=${Database__TrustServerCertificate:-true}
- InternalApi__ApiKey=${EmailApi__InternalApiKey:-}
- InternalApi__RequireApiKey=${EmailApi__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}
- SerilogEmail__From=${SerilogEmail__From:-}
- SerilogEmail__To=${SerilogEmail__To:-}
- SerilogEmail__Host=${SerilogEmail__Host:-}
- SerilogEmail__Port=${SerilogEmail__Port:-587}
- SerilogEmail__UserName=${SerilogEmail__UserName:-}
- SerilogEmail__Password=${SerilogEmail__Password:-}
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:-IMAGE_TAG-NOT-SET}
container_name: myai-api
depends_on:
- cv-matcher-api
- email-api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
- Database__Host=${Database__Host:-sqlserver}
- Database__Port=${Database__Port:-1433}
- Database__Name=${Database__Name:-MyAiDb}
- Database__User=${Database__User:-sa}
- Database__Password=${Database__Password:-}
- Database__TrustServerCertificate=${Database__TrustServerCertificate:-true}
- Google__TagManagerId=${Google__TagManagerId:-}
- Google__MapKey=${Google__MapKey:-}
- Contact__ToEmail=${Contact__ToEmail:-}
- Contact__FromEmail=${Contact__FromEmail:-}
- Contact__SubjectPrefix=${Contact__SubjectPrefix:-}
- Subscribe__ToEmail=${Subscribe__ToEmail:-}
- Subscribe__SubjectPrefix=${Subscribe__SubjectPrefix:-}
- EmailApi__BaseUrl=${EmailApi__BaseUrl:-http://email-api:8080}
- EmailApi__InternalApiKey=${EmailApi__InternalApiKey:-}
- Captcha__Provider=${Captcha__Provider:-Recaptcha}
- Captcha__SecretKey=${Captcha__SecretKey:-}
- Captcha__PublicKey=${Captcha__PublicKey:-}
- Captcha__MinimumScore=${Captcha__MinimumScore:-0.5}
- FileStorage__Path=${FileStorage__Path:-Files}
- FileStorage__DefaultFileName=${FileStorage__DefaultFileName:-}
- CvMatcherApi__BaseUrl=${CvMatcherApi__BaseUrl:-http://cv-matcher-api:8080}
- CvMatcherApi__InternalApiKey=${CvMatcherApi__InternalApiKey:-}
- JobSearch__BaseUrl=${JobSearch__JobSearchLinkBaseUrl:-https://myai.ro}
- RateLimiting__Global__PermitLimit=${RateLimiting__Global__PermitLimit:-120}
- RateLimiting__Global__Window=${RateLimiting__Global__Window:-00:01:00}
- RateLimiting__Global__QueueLimit=${RateLimiting__Global__QueueLimit:-0}
- RateLimiting__Policies__contact__PermitLimit=${RateLimiting__Policies__contact__PermitLimit:-5}
- RateLimiting__Policies__contact__Window=${RateLimiting__Policies__contact__Window:-00:01:00}
- RateLimiting__Policies__contact__QueueLimit=${RateLimiting__Policies__contact__QueueLimit:-0}
- RateLimiting__Policies__cvMatcher__PermitLimit=${RateLimiting__Policies__cvMatcher__PermitLimit:-10}
- RateLimiting__Policies__cvMatcher__Window=${RateLimiting__Policies__cvMatcher__Window:-00:10:00}
- RateLimiting__Policies__cvMatcher__QueueLimit=${RateLimiting__Policies__cvMatcher__QueueLimit:-0}
- RateLimiting__Policies__download__PermitLimit=${RateLimiting__Policies__download__PermitLimit:-5}
- RateLimiting__Policies__download__Window=${RateLimiting__Policies__download__Window:-00:01:00}
- RateLimiting__Policies__download__QueueLimit=${RateLimiting__Policies__download__QueueLimit:-0}
- Cors__AllowedOrigins__0=${Cors__AllowedOrigins__0:-}
- Cors__AllowedOrigins__1=${Cors__AllowedOrigins__1:-}
- SerilogEmail__From=${SerilogEmail__From:-}
- SerilogEmail__To=${SerilogEmail__To:-}
- SerilogEmail__Host=${SerilogEmail__Host:-}
- SerilogEmail__Port=${SerilogEmail__Port:-587}
- SerilogEmail__UserName=${SerilogEmail__UserName:-}
- SerilogEmail__Password=${SerilogEmail__Password:-}
volumes:
- ${LOGS_PATH:-/opt/myai/logs}/api:/app/logs
- ${FILES_PATH:-/opt/myai/files}:/app/Files
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
cv-cleanup-job:
image: registry.easysoft.ro/apps/myai-cv-cleanup-job:${IMAGE_TAG:-IMAGE_TAG-NOT-SET}
container_name: myai-cv-cleanup-job
depends_on:
- api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
- FileStorage__Path=${FileStorage__Path:-Files}
- Jobs__Tasks__0__Enabled=${Jobs__CvStorageCleanupEnabled:-true}
- Jobs__Tasks__0__Interval=${Jobs__CvStorageCleanupInterval:-01:00:00}
- Jobs__Tasks__0__Parameters__MaxTotalSizeMegabytes=${Jobs__CvStorageMaxTotalSizeMegabytes:-40}
- SerilogEmail__From=${SerilogEmail__From:-}
- SerilogEmail__To=${SerilogEmail__To:-}
- SerilogEmail__Host=${SerilogEmail__Host:-}
- SerilogEmail__Port=${SerilogEmail__Port:-587}
- SerilogEmail__UserName=${SerilogEmail__UserName:-}
- SerilogEmail__Password=${SerilogEmail__Password:-}
volumes:
- ${LOGS_PATH:-/opt/myai/logs}/cv-cleanup-job:/app/logs
- ${FILES_PATH:-/opt/myai/files}:/app/Files
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
cv-search-job:
image: registry.easysoft.ro/apps/myai-cv-search-job:${IMAGE_TAG:-IMAGE_TAG-NOT-SET}
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}
- Database__Host=${Database__Host:-sqlserver}
- Database__Port=${Database__Port:-1433}
- Database__Name=${Database__Name:-MyAiDb}
- Database__User=${Database__User:-sa}
- Database__Password=${Database__Password:-}
- Database__TrustServerCertificate=${Database__TrustServerCertificate:-true}
- CvMatcherApi__BaseUrl=${CvMatcherApi__BaseUrl:-http://cv-matcher-api:8080}
- CvMatcherApi__InternalApiKey=${CvMatcherApi__InternalApiKey:-}
- EmailApi__BaseUrl=${EmailApi__BaseUrl:-http://email-api:8080}
- EmailApi__InternalApiKey=${EmailApi__InternalApiKey:-}
- PageFetcherApi__BaseUrl=${PageFetcherApi__BaseUrl:-http://page-fetcher-api:8080}
- PageFetcherApi__InternalApiKey=${PageFetcherApi__InternalApiKey:-}
- FileStorage__Path=${FileStorage__Path:-Files}
- JobSearch__Enabled=${JobSearch__Enabled:-true}
- JobSearch__JobSearchLinkBaseUrl=${JobSearch__JobSearchLinkBaseUrl:-https://myai.ro}
- JobSearch__TokenExpiryDays=${JobSearch__TokenExpiryDays:-7}
- JobSearch__MinMatchScore=${JobSearch__MinMatchScore:-15}
- JobSearch__MaxJobsToMatch=${JobSearch__MaxJobsToMatch:-15}
- Jobs__Tasks__0__TaskType=CvSearch
- Jobs__Tasks__0__Enabled=${Jobs__CvSearchEnabled:-true}
- Jobs__Tasks__0__Interval=${Jobs__CvSearchInterval:-00:00:30}
- SerilogEmail__From=${SerilogEmail__From:-}
- SerilogEmail__To=${SerilogEmail__To:-}
- SerilogEmail__Host=${SerilogEmail__Host:-}
- SerilogEmail__Port=${SerilogEmail__Port:-587}
- SerilogEmail__UserName=${SerilogEmail__UserName:-}
- SerilogEmail__Password=${SerilogEmail__Password:-}
volumes:
- ${LOGS_PATH:-/opt/myai/logs}/cv-search-job:/app/logs
- ${FILES_PATH:-/opt/myai/files}:/app/Files
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
page-fetcher-api:
image: registry.easysoft.ro/apps/myai-page-fetcher-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET}
container_name: myai-page-fetcher-api
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
- Database__Host=${Database__Host:-sqlserver}
- Database__Port=${Database__Port:-1433}
- Database__Name=${Database__Name:-MyAiDb}
- Database__User=${Database__User:-sa}
- Database__Password=${Database__Password:-}
- Database__TrustServerCertificate=${Database__TrustServerCertificate:-true}
- InternalApi__ApiKey=${PageFetcherApi__InternalApiKey:-}
- InternalApi__RequireApiKey=true
- SerilogEmail__From=${SerilogEmail__From:-}
- SerilogEmail__To=${SerilogEmail__To:-}
- SerilogEmail__Host=${SerilogEmail__Host:-}
- SerilogEmail__Port=${SerilogEmail__Port:-587}
- SerilogEmail__UserName=${SerilogEmail__UserName:-}
- SerilogEmail__Password=${SerilogEmail__Password:-}
volumes:
- ${LOGS_PATH:-/opt/myai/logs}/page-fetcher-api:/app/logs
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
web:
image: registry.easysoft.ro/apps/myai-web:${IMAGE_TAG:-IMAGE_TAG-NOT-SET}
container_name: myai-web
depends_on:
- api
ports:
- "${WEB_PORT:-5000}:8080"
environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
- Site__Mode=${Site__Mode:-Normal}
networks:
- myai-network
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
myai-network:
driver: bridge