diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5f84966 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +.git +.gitignore + +# IDE +.idea +.vs +.vscode +**/.idea + +# Build artifacts +**/bin +**/obj +front/node_modules +front/dist + +# Documentation +Docs/ + +# Dev files +docker/ +*.sh + +# Env files (secrets must not be in the image) +.env +deploy/.env + +# Misc +**/*.user +**/*.DotSettings.user diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..54c3f0c --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,17 @@ +# PostgreSQL (shared VPS instance — create DB/user manually) +POSTGRES_DB=aips_db +POSTGRES_USER=aips_user +POSTGRES_PASSWORD=CHANGE_ME_strong_password_here + +# RabbitMQ +RABBITMQ_DEFAULT_USER=aips_rabbit +RABBITMQ_DEFAULT_PASS=CHANGE_ME_rabbit_password +RABBITMQ_DEFAULT_VHOST=/ +RABBITMQ_EXCHANGE=aips + +# JWT +JWT_ISSUER=AIPS +JWT_AUDIENCE=AIPSWebApi +JWT_KEY=CHANGE_ME_generate_a_64_char_random_string_here +JWT_EXPIRATION_MINUTES=60 +JWT_REFRESH_TOKEN_EXPIRATION_DAYS=7 diff --git a/deploy/Dockerfile.front b/deploy/Dockerfile.front new file mode 100644 index 0000000..abfd905 --- /dev/null +++ b/deploy/Dockerfile.front @@ -0,0 +1,14 @@ +FROM oven/bun:1 AS build +WORKDIR /app + +COPY front/package.json front/bun.lock ./ +RUN bun install --frozen-lockfile + +COPY front/ . +RUN bun run build + +FROM nginx:alpine +RUN rm /etc/nginx/conf.d/default.conf +COPY deploy/nginx/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/deploy/Dockerfile.rt b/deploy/Dockerfile.rt new file mode 100644 index 0000000..28894d2 --- /dev/null +++ b/deploy/Dockerfile.rt @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +COPY dotnet/dotnet.sln dotnet/dotnet.sln +COPY dotnet/AipsCore/AipsCore.csproj dotnet/AipsCore/ +COPY dotnet/AipsWebApi/AipsWebApi.csproj dotnet/AipsWebApi/ +COPY dotnet/AipsRT/AipsRT.csproj dotnet/AipsRT/ +COPY dotnet/AipsWorker/AipsWorker.csproj dotnet/AipsWorker/ + +WORKDIR /src/dotnet +RUN dotnet restore dotnet.sln + +WORKDIR /src +COPY dotnet/ dotnet/ + +WORKDIR /src/dotnet +RUN dotnet publish AipsRT/AipsRT.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 8080 +ENTRYPOINT ["dotnet", "AipsRT.dll"] diff --git a/deploy/Dockerfile.webapi b/deploy/Dockerfile.webapi new file mode 100644 index 0000000..1d9b9b1 --- /dev/null +++ b/deploy/Dockerfile.webapi @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +COPY dotnet/dotnet.sln dotnet/dotnet.sln +COPY dotnet/AipsCore/AipsCore.csproj dotnet/AipsCore/ +COPY dotnet/AipsWebApi/AipsWebApi.csproj dotnet/AipsWebApi/ +COPY dotnet/AipsRT/AipsRT.csproj dotnet/AipsRT/ +COPY dotnet/AipsWorker/AipsWorker.csproj dotnet/AipsWorker/ + +WORKDIR /src/dotnet +RUN dotnet restore dotnet.sln + +WORKDIR /src +COPY dotnet/ dotnet/ + +WORKDIR /src/dotnet +RUN dotnet publish AipsWebApi/AipsWebApi.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 8080 +ENTRYPOINT ["dotnet", "AipsWebApi.dll"] diff --git a/deploy/Dockerfile.worker b/deploy/Dockerfile.worker new file mode 100644 index 0000000..d7d7b9d --- /dev/null +++ b/deploy/Dockerfile.worker @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +COPY dotnet/dotnet.sln dotnet/dotnet.sln +COPY dotnet/AipsCore/AipsCore.csproj dotnet/AipsCore/ +COPY dotnet/AipsWebApi/AipsWebApi.csproj dotnet/AipsWebApi/ +COPY dotnet/AipsRT/AipsRT.csproj dotnet/AipsRT/ +COPY dotnet/AipsWorker/AipsWorker.csproj dotnet/AipsWorker/ + +WORKDIR /src/dotnet +RUN dotnet restore dotnet.sln + +WORKDIR /src +COPY dotnet/ dotnet/ + +WORKDIR /src/dotnet +RUN dotnet publish AipsWorker/AipsWorker.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +WORKDIR /app +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "AipsWorker.dll"] diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..3de064a --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,103 @@ +services: + rabbitmq: + image: rabbitmq:3-management + container_name: aips-rabbitmq + restart: unless-stopped + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_DEFAULT_VHOST} + volumes: + - rabbitmqdata:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 10s + retries: 5 + + webapi: + build: + context: .. + dockerfile: deploy/Dockerfile.webapi + container_name: aips-webapi + restart: unless-stopped + environment: + ASPNETCORE_URLS: "http://+:8080" + ASPNETCORE_ENVIRONMENT: "Production" + DB_CONN_STRING: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" + RABBITMQ_AMQP_URI: "amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/${RABBITMQ_DEFAULT_VHOST}" + RABBITMQ_EXCHANGE: "${RABBITMQ_EXCHANGE}" + JWT_ISSUER: "${JWT_ISSUER}" + JWT_AUDIENCE: "${JWT_AUDIENCE}" + JWT_KEY: "${JWT_KEY}" + JWT_EXPIRATION_MINUTES: "${JWT_EXPIRATION_MINUTES}" + JWT_REFRESH_TOKEN_EXPIRATION_DAYS: "${JWT_REFRESH_TOKEN_EXPIRATION_DAYS}" + networks: + - default + - back_network + depends_on: + rabbitmq: + condition: service_healthy + + rt: + build: + context: .. + dockerfile: deploy/Dockerfile.rt + container_name: aips-rt + restart: unless-stopped + environment: + ASPNETCORE_URLS: "http://+:8080" + ASPNETCORE_ENVIRONMENT: "Production" + DB_CONN_STRING: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" + RABBITMQ_AMQP_URI: "amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/${RABBITMQ_DEFAULT_VHOST}" + RABBITMQ_EXCHANGE: "${RABBITMQ_EXCHANGE}" + JWT_ISSUER: "${JWT_ISSUER}" + JWT_AUDIENCE: "${JWT_AUDIENCE}" + JWT_KEY: "${JWT_KEY}" + JWT_EXPIRATION_MINUTES: "${JWT_EXPIRATION_MINUTES}" + JWT_REFRESH_TOKEN_EXPIRATION_DAYS: "${JWT_REFRESH_TOKEN_EXPIRATION_DAYS}" + networks: + - default + - back_network + depends_on: + rabbitmq: + condition: service_healthy + + worker: + build: + context: .. + dockerfile: deploy/Dockerfile.worker + container_name: aips-worker + restart: unless-stopped + environment: + DB_CONN_STRING: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" + RABBITMQ_AMQP_URI: "amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/${RABBITMQ_DEFAULT_VHOST}" + RABBITMQ_EXCHANGE: "${RABBITMQ_EXCHANGE}" + JWT_ISSUER: "${JWT_ISSUER}" + JWT_AUDIENCE: "${JWT_AUDIENCE}" + JWT_KEY: "${JWT_KEY}" + networks: + - default + - back_network + depends_on: + rabbitmq: + condition: service_healthy + + nginx: + build: + context: .. + dockerfile: deploy/Dockerfile.front + container_name: aips-nginx + restart: unless-stopped + ports: + - "8090:80" + depends_on: + - webapi + - rt + +networks: + back_network: + external: true + +volumes: + rabbitmqdata: diff --git a/deploy/nginx/aips-global.conf b/deploy/nginx/aips-global.conf new file mode 100644 index 0000000..f111146 --- /dev/null +++ b/deploy/nginx/aips-global.conf @@ -0,0 +1,45 @@ +server { + listen 80; + server_name aips.stewki.com; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name aips.stewki.com; + + ssl_certificate /etc/letsencrypt/live/aips.stewki.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/aips.stewki.com/privkey.pem; + + client_max_body_size 10M; + + location / { + proxy_pass http://host.docker.internal:8090; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /hubs/ { + proxy_pass http://host.docker.internal:8090; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } +} diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 0000000..b6a7be0 --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,46 @@ +upstream webapi { + server webapi:8080; +} + +upstream rt { + server rt:8080; +} + +server { + listen 80; + server_name _; + + client_max_body_size 10M; + + # REST API + location /api/ { + proxy_pass http://webapi; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SignalR hubs (WebSocket support) + location /hubs/ { + proxy_pass http://rt; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } + + # Vue SPA + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } +} diff --git a/dos-start-back.bat b/dos-start-back.bat new file mode 100644 index 0000000..e4cf66a --- /dev/null +++ b/dos-start-back.bat @@ -0,0 +1,27 @@ +<# : batch portion +@echo off +set "SCRIPT_DIR=%~dp0" +powershell -ExecutionPolicy Bypass "iex((Get-Content '%~f0' -Raw))" +exit /b +#> + +Set-Location (Join-Path $env:SCRIPT_DIR "dotnet") + +$jobs = @() +$jobs += Start-Job -ScriptBlock { Set-Location $using:PWD; dotnet run --project AipsWebApi 2>&1 | ForEach-Object { "[WebApi] $_" } } +$jobs += Start-Job -ScriptBlock { Set-Location $using:PWD; dotnet run --project AipsRT 2>&1 | ForEach-Object { "[RT] $_" } } +$jobs += Start-Job -ScriptBlock { Set-Location $using:PWD; dotnet run --project AipsWorker 2>&1 | ForEach-Object { "[Worker] $_" } } + +try { + while ($jobs | Where-Object { $_.State -eq 'Running' }) { + foreach ($job in $jobs) { + Receive-Job -Job $job + } + Start-Sleep -Milliseconds 200 + } + foreach ($job in $jobs) { + Receive-Job -Job $job + } +} finally { + $jobs | Stop-Job -PassThru | Remove-Job +} diff --git a/dos-start-front.bat b/dos-start-front.bat new file mode 100644 index 0000000..d290a7b --- /dev/null +++ b/dos-start-front.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0front" + +bun dev diff --git a/dos-start-infra.bat b/dos-start-infra.bat new file mode 100644 index 0000000..a1aa235 --- /dev/null +++ b/dos-start-infra.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0docker" + +docker compose -p aips --env-file ..\.env up diff --git a/dotnet/AipsRT/Program.cs b/dotnet/AipsRT/Program.cs index 8d8125e..2f7711f 100644 --- a/dotnet/AipsRT/Program.cs +++ b/dotnet/AipsRT/Program.cs @@ -9,7 +9,10 @@ using AipsRT.Services.Interfaces; using DotNetEnv; using Microsoft.AspNetCore.SignalR; -Env.Load("../../.env"); +if (File.Exists("../../.env")) +{ + Env.Load("../../.env"); +} var builder = WebApplication.CreateBuilder(args); diff --git a/dotnet/AipsWebApi/Program.cs b/dotnet/AipsWebApi/Program.cs index a46a5f4..8795855 100644 --- a/dotnet/AipsWebApi/Program.cs +++ b/dotnet/AipsWebApi/Program.cs @@ -3,7 +3,10 @@ using AipsCore.Infrastructure.Persistence.Db; using AipsWebApi.Middleware; using DotNetEnv; -Env.Load("../../.env"); +if (File.Exists("../../.env")) +{ + Env.Load("../../.env"); +} var builder = WebApplication.CreateBuilder(args);