From 9030d1a8b029b26ec8e1838092b9a54402c9e2f1 Mon Sep 17 00:00:00 2001 From: Andrija Stevanovic Date: Sun, 8 Mar 2026 16:11:23 +0100 Subject: [PATCH 1/3] deploy --- .dockerignore | 29 +++++++++ deploy/.env.example | 17 ++++++ deploy/Dockerfile.front | 14 +++++ deploy/Dockerfile.rt | 23 +++++++ deploy/Dockerfile.webapi | 23 +++++++ deploy/Dockerfile.worker | 22 +++++++ deploy/docker-compose.yml | 115 +++++++++++++++++++++++++++++++++++ deploy/nginx/nginx.conf | 46 ++++++++++++++ dotnet/AipsRT/Program.cs | 5 +- dotnet/AipsWebApi/Program.cs | 5 +- 10 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 deploy/.env.example create mode 100644 deploy/Dockerfile.front create mode 100644 deploy/Dockerfile.rt create mode 100644 deploy/Dockerfile.webapi create mode 100644 deploy/Dockerfile.worker create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/nginx/nginx.conf 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..77e7bc4 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,17 @@ +# PostgreSQL +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..8b685dd --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,115 @@ +services: + postgres: + image: postgres:18 + container_name: aips-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + 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} + ports: + - "15672:15672" + 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}" + depends_on: + postgres: + condition: service_healthy + 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}" + depends_on: + postgres: + condition: service_healthy + 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}" + depends_on: + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + + nginx: + build: + context: .. + dockerfile: deploy/Dockerfile.front + container_name: aips-nginx + restart: unless-stopped + ports: + - "80:80" + depends_on: + - webapi + - rt + +volumes: + pgdata: + rabbitmqdata: 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/dotnet/AipsRT/Program.cs b/dotnet/AipsRT/Program.cs index 8d0d68e..c5db8c8 100644 --- a/dotnet/AipsRT/Program.cs +++ b/dotnet/AipsRT/Program.cs @@ -8,7 +8,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); From 4ccb6303f3c4efea8299fd58283492ada0238c0d Mon Sep 17 00:00:00 2001 From: Andrija Stevanovic Date: Sun, 8 Mar 2026 16:24:38 +0100 Subject: [PATCH 2/3] windows start scripts --- dos-start-back.bat | 27 +++++++++++++++++++++++++++ dos-start-front.bat | 4 ++++ dos-start-infra.bat | 4 ++++ 3 files changed, 35 insertions(+) create mode 100644 dos-start-back.bat create mode 100644 dos-start-front.bat create mode 100644 dos-start-infra.bat 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 From 18e7287a172e58de1e8a1899f509ab47e518acaf Mon Sep 17 00:00:00 2001 From: Andrija Stevanovic Date: Sun, 8 Mar 2026 16:51:43 +0100 Subject: [PATCH 3/3] deploy adaptation for vps --- deploy/.env.example | 2 +- deploy/docker-compose.yml | 40 +++++++++++-------------------- deploy/nginx/aips-global.conf | 45 +++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 27 deletions(-) create mode 100644 deploy/nginx/aips-global.conf diff --git a/deploy/.env.example b/deploy/.env.example index 77e7bc4..54c3f0c 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,4 +1,4 @@ -# PostgreSQL +# PostgreSQL (shared VPS instance — create DB/user manually) POSTGRES_DB=aips_db POSTGRES_USER=aips_user POSTGRES_PASSWORD=CHANGE_ME_strong_password_here diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 8b685dd..3de064a 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,20 +1,4 @@ services: - postgres: - image: postgres:18 - container_name: aips-postgres - restart: unless-stopped - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 5 - rabbitmq: image: rabbitmq:3-management container_name: aips-rabbitmq @@ -23,8 +7,6 @@ services: RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_DEFAULT_VHOST} - ports: - - "15672:15672" volumes: - rabbitmqdata:/var/lib/rabbitmq healthcheck: @@ -50,9 +32,10 @@ services: 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: - postgres: - condition: service_healthy rabbitmq: condition: service_healthy @@ -73,9 +56,10 @@ services: 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: - postgres: - condition: service_healthy rabbitmq: condition: service_healthy @@ -92,9 +76,10 @@ services: JWT_ISSUER: "${JWT_ISSUER}" JWT_AUDIENCE: "${JWT_AUDIENCE}" JWT_KEY: "${JWT_KEY}" + networks: + - default + - back_network depends_on: - postgres: - condition: service_healthy rabbitmq: condition: service_healthy @@ -105,11 +90,14 @@ services: container_name: aips-nginx restart: unless-stopped ports: - - "80:80" + - "8090:80" depends_on: - webapi - rt +networks: + back_network: + external: true + volumes: - pgdata: 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; + } +}