From bd563f6daed37a5c02fc75d44cd06a9c85d7e459 Mon Sep 17 00:00:00 2001 From: Simon Martens Date: Tue, 30 Sep 2025 17:41:04 +0200 Subject: [PATCH] +Docker --- .dockerignore | 11 +++ DOCKER.md | 128 +++++++++++++++++++++++++++ Dockerfile | 66 ++++++++++---- HaWeb/CLAUDE.md | 12 ++- HaWeb/FileHelpers/GitService.cs | 25 ++++++ HaWeb/FileHelpers/XMLFileProvider.cs | 89 +++++++++---------- HaWeb/Program.cs | 15 ++-- HaWeb/appsettings.Development.json | 2 +- HaWeb/appsettings.Staging.json | 2 +- HaWeb/appsettings.json | 6 +- docker-compose.yml | 20 +++++ 11 files changed, 298 insertions(+), 78 deletions(-) create mode 100644 .dockerignore create mode 100644 DOCKER.md create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c5df3a7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +**/bin/ +**/obj/ +**/node_modules/ +**/testdata/ +**/.git/ +**/.vscode/ +**/.vs/ +**/wwwroot/dist/ +*.md +.gitignore +.editorconfig \ No newline at end of file diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..7922053 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,128 @@ +# Docker Deployment for HaWeb + +## Quick Start + +### 1. Create the external volume +```bash +docker volume create hamann_data +``` + +### 2. Set webhook secret (optional) +```bash +export WEBHOOK_SECRET="your-secret-here" +``` + +### 3. Build and run +```bash +docker-compose up -d --build +``` + +### 4. View logs +```bash +docker-compose logs -f +``` + +## Configuration + +The application runs with the following defaults: +- **HTTP Port**: 5000 +- **HTTPS Port**: 5001 (self-signed cert) +- **Data Path**: `/app/data` (mounted to `hamann_data` volume) +- **Repository**: https://github.com/Theodor-Springmann-Stiftung/hamann-xml +- **Branch**: main + +### Environment Variables + +Override in `docker-compose.yml` or set before running: + +- `DOTNET_ENVIRONMENT`: `Production`, `Staging`, or `Development` +- `FileStoragePath`: Base path for data storage (default: `/app/data`) +- `RepositoryBranch`: Git branch to track +- `RepositoryURL`: Git repository URL +- `WebhookSecret`: GitHub webhook secret for signature validation + +## Data Structure + +Inside the `hamann_data` volume: +``` +/app/data/ + ├── GIT/ # Git repository with XML sources + └── HAMANN/ # Compiled Hamann.xml files +``` + +## Webhook Setup + +Configure GitHub webhook to POST to: +``` +http://your-server:5000/api/webhook/git +``` + +Or with reverse proxy: +``` +https://your-domain.com/api/webhook/git +``` + +Set Content-Type to `application/json` and add your webhook secret. + +## Production Deployment + +### With Reverse Proxy (Recommended) + +Add to your nginx/traefik config: +```nginx +location / { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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; +} +``` + +### Update docker-compose.yml + +Remove port exposure if using reverse proxy: +```yaml +services: + web: + build: . + volumes: + - hamann_data:/app/data + # ports: # Comment out for reverse proxy + # - "5000:5000" + environment: + - ASPNETCORE_URLS=http://+:5000 +``` + +## Troubleshooting + +### View application logs +```bash +docker-compose logs -f web +``` + +### Access container shell +```bash +docker-compose exec web /bin/bash +``` + +### Check data volume +```bash +docker volume inspect hamann_data +``` + +### Rebuild from scratch +```bash +docker-compose down +docker-compose up -d --build --force-recreate +``` + +### Manual Git operations +```bash +docker-compose exec web /bin/bash +cd /app/data/GIT +git status +``` \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2e5c5bf..e539315 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,51 @@ -# PREREQUISITES -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build -RUN apt update -RUN apt install openssh-server nodejs npm -y - -# CLONE & SETUP -COPY . . -RUN mkdir /data/ -RUN mkdir /data/hamann/ -RUN mkdir /data/xml/ -RUN git clone https://github.com/Theodor-Springmann-Stiftung/hamann-xml.git /data/xml/ - -# COMPILE & PUBLISH -WORKDIR HaWeb/ -RUN dotnet restore +# Build frontend assets +FROM node:18 AS frontend +WORKDIR /app/HaWeb +COPY HaWeb/package*.json ./ RUN npm install -RUN npm run css_build -RUN dotnet publish --no-restore -o /app +COPY HaWeb/ ./ +RUN npm run build -# RUN +# Build .NET application +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /app -RUN DOTNET_ENVIRONMENT=Docker dotnet HaWeb.dll + +# Copy project files and restore dependencies +COPY HaDocumentV6/HaDocumentV6.csproj ./HaDocumentV6/ +COPY HaXMLReaderV6/HaXMLReaderV6.csproj ./HaXMLReaderV6/ +COPY HaWeb/HaWeb.csproj ./HaWeb/ +RUN dotnet restore HaWeb/HaWeb.csproj + +# Copy all source files +COPY HaDocumentV6/ ./HaDocumentV6/ +COPY HaXMLReaderV6/ ./HaXMLReaderV6/ +COPY HaWeb/ ./HaWeb/ + +# Copy built frontend assets (overwrites the source wwwroot/dist) +COPY --from=frontend /app/HaWeb/wwwroot/dist/ ./HaWeb/wwwroot/dist/ + +# Build application +WORKDIR /app/HaWeb +RUN dotnet publish -c Release -o /app/publish + +# Runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app + +# Install git for LibGit2Sharp +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app/publish . + +# Create data directory +RUN mkdir -p /app/data + +# Expose ports for HTTP, HTTPS EXPOSE 5000 +EXPOSE 5001 + +ENV ASPNETCORE_URLS="http://+:5000;https://+:5001" +ENV DOTNET_ENVIRONMENT="Production" +ENV FileStoragePath="/app/data" + +CMD ["dotnet", "HaWeb.dll"] \ No newline at end of file diff --git a/HaWeb/CLAUDE.md b/HaWeb/CLAUDE.md index a9d2987..a7f0a16 100644 --- a/HaWeb/CLAUDE.md +++ b/HaWeb/CLAUDE.md @@ -32,7 +32,17 @@ npm run build # Build CSS/JS first (required!) dotnet build HaWeb.csproj # Build the web application ``` -### Production Deployment (Linux) +### Production Deployment + +**Docker (Recommended)**: +```bash +# From repository root +docker volume create hamann_data +docker-compose up -d --build +``` +See `DOCKER.md` for detailed Docker deployment instructions. + +**Manual (Linux)**: ```bash npm run build dotnet publish --runtime linux-x64 -c Release diff --git a/HaWeb/FileHelpers/GitService.cs b/HaWeb/FileHelpers/GitService.cs index 16d35bc..90fe88e 100644 --- a/HaWeb/FileHelpers/GitService.cs +++ b/HaWeb/FileHelpers/GitService.cs @@ -121,10 +121,35 @@ public class GitService : IGitService { // Checkout the specified branch if it's not the default using var repo = new Repository(_repositoryPath); + + // Log diagnostic information + _logger?.LogInformation("HEAD: {Head}, IsDetached: {IsDetached}, Tip: {Tip}", + repo.Head?.FriendlyName ?? "null", + repo.Head?.IsRemote.ToString() ?? "null", + repo.Head?.Tip?.Sha.Substring(0, 7) ?? "null"); + + _logger?.LogInformation("Available branches: {Branches}", + string.Join(", ", repo.Branches.Select(b => $"{b.FriendlyName} (Remote: {b.IsRemote})"))); + var branch = repo.Branches[_branch] ?? repo.Branches[$"origin/{_branch}"]; + + if (branch == null) { + _logger?.LogWarning("Branch {Branch} not found. Attempting to create local tracking branch from origin/{Branch}", _branch, _branch); + var remoteBranch = repo.Branches[$"origin/{_branch}"]; + if (remoteBranch != null) { + branch = repo.CreateBranch(_branch, remoteBranch.Tip); + repo.Branches.Update(branch, b => b.TrackedBranch = remoteBranch.CanonicalName); + _logger?.LogInformation("Created local tracking branch {Branch}", _branch); + } + } + if (branch != null && branch.FriendlyName != repo.Head.FriendlyName) { Commands.Checkout(repo, branch); _logger?.LogInformation("Checked out branch {Branch}", _branch); + } else if (branch != null) { + _logger?.LogInformation("Already on branch {Branch}", _branch); + } else { + _logger?.LogError("Could not find or create branch {Branch}", _branch); } } else { diff --git a/HaWeb/FileHelpers/XMLFileProvider.cs b/HaWeb/FileHelpers/XMLFileProvider.cs index be77a0a..e0b776a 100644 --- a/HaWeb/FileHelpers/XMLFileProvider.cs +++ b/HaWeb/FileHelpers/XMLFileProvider.cs @@ -52,8 +52,8 @@ public class XMLFileProvider : IXMLFileProvider { // Create File Lists; Here and in xmlservice, which does preliminary checking Scan(); if (_WorkingTreeFiles != null && _WorkingTreeFiles.Any()) { - var state = xmlservice.Collect(_WorkingTreeFiles, xmlservice.GetRootDefs()); - xmlservice.SetState(state); + var initialState = xmlservice.Collect(_WorkingTreeFiles, xmlservice.GetRootDefs()); + xmlservice.SetState(initialState); } _HamannFiles = _ScanHamannFiles(); @@ -64,29 +64,27 @@ public class XMLFileProvider : IXMLFileProvider { if (_Lib.GetLibrary() != null) return; } - // -> NO: Try to create a new file - var created = _XMLService.TryCreate(_XMLService.GetState()); - if (created != null) { - var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); - if (file != null) { - _Lib.SetLibrary(file, created.Document, null); - if (_Lib.GetLibrary() != null) return; + // -> NO: Try to create a new file (only if we have a valid git state and XML files) + var currentState = _XMLService.GetState(); + if (currentState != null && _GitState != null) { + var created = _XMLService.TryCreate(currentState); + if (created != null) { + var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); + if (file != null) { + _Lib.SetLibrary(file, created.Document, null); + if (_Lib.GetLibrary() != null) return; + } } } // It failed, so use the last best File: - else if (_HamannFiles != null && _HamannFiles.Any()) { + if (_HamannFiles != null && _HamannFiles.Any()) { _Lib.SetLibrary(_HamannFiles.First(), null, null); if (_Lib.GetLibrary() != null) return; } - // -> There is none? Use Fallback: - else { - var options = new HaWeb.Settings.HaDocumentOptions(); - if (_Lib.SetLibrary(null, null, null) == null) { - throw new Exception("Die Fallback Hamann.xml unter " + options.HamannXMLFilePath + " kann nicht geparst werden."); - } - } + // No valid data available + throw new Exception("Keine gültige Hamann.xml Datei gefunden. Repository konnte nicht geklont oder geparst werden."); } public void ParseConfiguration(IConfiguration config) { @@ -111,29 +109,27 @@ public class XMLFileProvider : IXMLFileProvider { if (_Lib.GetLibrary() != null) return; } - // -> NO: Try to create a new file - var created = _XMLService.TryCreate(_XMLService.GetState()); - if (created != null) { - var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); - if (file != null) { - _Lib.SetLibrary(file, created.Document, null); - if (_Lib.GetLibrary() != null) return; + // -> NO: Try to create a new file (only if we have a valid git state and XML files) + var configState = _XMLService.GetState(); + if (configState != null && _GitState != null) { + var created = _XMLService.TryCreate(configState); + if (created != null) { + var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); + if (file != null) { + _Lib.SetLibrary(file, created.Document, null); + if (_Lib.GetLibrary() != null) return; + } } } // It failed, so use the last best File: - else if (_HamannFiles != null && _HamannFiles.Any()) { + if (_HamannFiles != null && _HamannFiles.Any()) { _Lib.SetLibrary(_HamannFiles.First(), null, null); if (_Lib.GetLibrary() != null) return; } - // -> There is none? Use Fallback: - else { - var options = new HaWeb.Settings.HaDocumentOptions(); - if (_Lib.SetLibrary(null, null, null) == null) { - throw new Exception("Die Fallback Hamann.xml unter " + options.HamannXMLFilePath + " kann nicht geparst werden."); - } - } + // No valid data available + throw new Exception("Keine gültige Hamann.xml Datei gefunden. Repository konnte nicht geklont oder geparst werden."); } // Getters and Setters @@ -180,15 +176,18 @@ public class XMLFileProvider : IXMLFileProvider { } } - // Try to create a new file - var created = _XMLService.TryCreate(_XMLService.GetState()); - if (created != null) { - var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); - if (file != null) { - _Lib.SetLibrary(file, created.Document, null); - if (_Lib.GetLibrary() != null) { - OnNewData(); - return; + // Try to create a new file (only if we have a valid git state and XML files) + var reloadState = _XMLService.GetState(); + if (reloadState != null && _GitState != null) { + var created = _XMLService.TryCreate(reloadState); + if (created != null) { + var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); + if (file != null) { + _Lib.SetLibrary(file, created.Document, null); + if (_Lib.GetLibrary() != null) { + OnNewData(); + return; + } } } } @@ -202,12 +201,8 @@ public class XMLFileProvider : IXMLFileProvider { } } - // Use Fallback: - var options = new HaWeb.Settings.HaDocumentOptions(); - if (_Lib.SetLibrary(null, null, null) == null) { - throw new Exception("Die Fallback Hamann.xml unter " + options.HamannXMLFilePath + " kann nicht geparst werden."); - } - OnNewData(); + // No valid data available + throw new Exception("Keine gültige Hamann.xml Datei gefunden. Repository konnte nicht geklont oder geparst werden."); } public IFileInfo? SaveHamannFile(XElement element, string basefilepath, ModelStateDictionary? ModelState) { diff --git a/HaWeb/Program.cs b/HaWeb/Program.cs index 9a10c31..044688b 100644 --- a/HaWeb/Program.cs +++ b/HaWeb/Program.cs @@ -56,9 +56,6 @@ app.UseMiddleware(); // Production Options if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - app.UseHttpsRedirection(); app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); } @@ -66,11 +63,17 @@ app.UseAuthorization(); var cacheMaxAgeOneWeek = (60 * 60 * 24 * 7).ToString(); app.UseStaticFiles(new StaticFileOptions { - // Set ETag: OnPrepareResponse = ctx => { ctx.Context.Response.Headers.Add("Cache-Control", "public, max-age=" + cacheMaxAgeOneWeek); - }, - ServeUnknownFileTypes = true, + + // Ensure correct MIME types + var path = ctx.File.PhysicalPath; + if (path?.EndsWith(".css") == true) { + ctx.Context.Response.ContentType = "text/css"; + } else if (path?.EndsWith(".js") == true) { + ctx.Context.Response.ContentType = "application/javascript"; + } + } }); app.MapControllers(); app.Run(); diff --git a/HaWeb/appsettings.Development.json b/HaWeb/appsettings.Development.json index a2ddc33..0ba3164 100644 --- a/HaWeb/appsettings.Development.json +++ b/HaWeb/appsettings.Development.json @@ -15,7 +15,7 @@ "AllowedHosts": "*", "FileStoragePath": "/home/simon/source/hamann-ausgabe-core/HaWeb/testdata/", "RepositoryBranch": "Main", - "RepositoryURL": "https://github.com/Theodor-Springmann-Stiftung/hamann-xml", + "RepositoryURL": "https://github.com/Theodor-Springmann-Stiftung/hamann-xml.git", "WebhookSecret": "secret", "FileSizeLimit": 52428800, "AvailableStartYear": 1700, diff --git a/HaWeb/appsettings.Staging.json b/HaWeb/appsettings.Staging.json index 6025cb9..58df1c9 100644 --- a/HaWeb/appsettings.Staging.json +++ b/HaWeb/appsettings.Staging.json @@ -13,7 +13,7 @@ }, "AllowedWebSocketConnections": "*", "AllowedHosts": "*", - "FileStoragePath": "/var/www/vhosts/development.hamann-ausgabe.de/httpdocs/Storage/", + "FileStoragePath": "/app/data", "RepositoryBranch": "main", "RepositoryURL": "https://github.com/Theodor-Springmann-Stiftung/hamann-xml", "WebhookSecret": "", diff --git a/HaWeb/appsettings.json b/HaWeb/appsettings.json index 8667073..87b1141 100644 --- a/HaWeb/appsettings.json +++ b/HaWeb/appsettings.json @@ -13,8 +13,8 @@ }, "AllowedWebSocketConnections": "*", "AllowedHosts": "*", - "FileStoragePath": "/var/www/vhosts/hamann-ausgabe.de/httpdocs/Storage/", - "RepositoryBranch": "main", - "RepositoryURL": "", + "FileStoragePath": "/app/data", + "RepositoryBranch": "Release", + "RepositoryURL": "https://github.com/Theodor-Springmann-Stiftung/hamann-xml.git", "WebhookSecret": "" } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0616d30 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +name: hamann-ausgabe +services: + web: + build: . + volumes: + - hamann_data:/app/data + ports: + - "5000:5000" + environment: + - ASPNETCORE_URLS=http://+:5000 + - DOTNET_ENVIRONMENT=Production + - FileStoragePath=/app/data + - RepositoryBranch=Release + - RepositoryURL=https://github.com/Theodor-Springmann-Stiftung/hamann-xml + - WebhookSecret=${WEBHOOK_SECRET:-} + restart: unless-stopped + +volumes: + hamann_data: + external: true \ No newline at end of file