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);