diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 587ef2c4..31a0f3ed 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -26,8 +26,8 @@ jobs:
outputs:
turnierplan_version: ${{ steps.extract-version.outputs.turnierplan_version }}
- container-image:
- name: 'Build container image'
+ container-image-amd64:
+ name: 'Build container image (amd64)'
runs-on: ubuntu-24.04
permissions:
contents: read
@@ -51,8 +51,8 @@ jobs:
- name: 'Build and push container image'
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
- context: ./src
- file: ./src/Turnierplan.App/Dockerfile
+ context: .
+ file: ./docker/turnierplan-amd64/Dockerfile
push: true
# Note: Always push "latest" even when building from a non-main branch. This is because currently, no patches are released for a non-latest minor release.
tags: 'ghcr.io/turnierplan-net/turnierplan:latest,ghcr.io/turnierplan-net/turnierplan:${{ needs.extract-version.outputs.turnierplan_version }}'
diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml
index 4411e655..e0b7ba61 100644
--- a/.github/workflows/validate.yaml
+++ b/.github/workflows/validate.yaml
@@ -72,8 +72,9 @@ jobs:
if: ${{ env.RUN_SONARQUBE_ANALYSIS == 'true' }}
run: 'dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"'
working-directory: './src'
- e2e:
- name: 'E2E Tests'
+
+ e2e-amd64:
+ name: 'E2E Tests (amd64)'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v2
@@ -87,7 +88,7 @@ jobs:
run: 'npm ci'
working-directory: './src/Turnierplan.App/Client/'
- name: 'Prepare e2e test environment'
- run: 'npm run e2e:prepare'
+ run: 'npm run e2e:prepare:amd64'
working-directory: './src/Turnierplan.App/Client/'
- name: 'Run e2e tests'
run: 'npm run e2e:run'
diff --git a/README.md b/README.md
index 8fa5a9ee..4ce20359 100644
--- a/README.md
+++ b/README.md
@@ -163,7 +163,7 @@ If you have an Entra ID app registration with the necessary permissions on the s
## Documentation
-The developer documentation of **turnierplan.NET** is located in the `docs` directory of this repository.
+The developer documentation of **turnierplan.NET** is located in the `docs` directory of this repository. Information about the container images and how to build them can be found in the [README.md in the docker folder](./docker/README.md).
## Development
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 00000000..078489c2
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,16 @@
+# turnierplan.NET · Docker Images
+
+Currently, there exists one Dockerfile with the `dotnet/aspnet:10.0-alpine` base image for the amd64 architecture. To build the container image, run the following command from the *repository root*:
+
+```shell
+docker build -t turnierplan:dev -f docker/turnierplan-amd64/Dockerfile .
+```
+
+The resulting container image can be run as described in the [main readme](../README.md). The following minimal example uses an in-memory data store and does not mount any volumes:
+
+```shell
+docker run -p 80:8080 -e Turnierplan__ApplicationUrl="http://localhost" -e Database__InMemory="true" turnierplan:dev
+```
+
+> [!WARNING]
+> Manually built container images should not be used in production! This is because the relevant GitHub workflow performs other important steps before actually building the images.
diff --git a/src/Turnierplan.App/Dockerfile b/docker/turnierplan-amd64/Dockerfile
similarity index 54%
rename from src/Turnierplan.App/Dockerfile
rename to docker/turnierplan-amd64/Dockerfile
index 6115e3fe..0177b991 100644
--- a/src/Turnierplan.App/Dockerfile
+++ b/docker/turnierplan-amd64/Dockerfile
@@ -1,13 +1,12 @@
-FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+FROM mcr.microsoft.com/dotnet/aspnet:10.0.1-alpine3.23-amd64 AS base
-# Where are the files stored in the container? This folder should be mapped as a volume
+# Where are the files stored inside the container? This folder should be mapped as a volume
ARG DATA_DIRECTORY=/var/turnierplan
-FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+FROM mcr.microsoft.com/dotnet/sdk:10.0.101-noble-amd64 AS build
# Copy the source files and build the solution.
-WORKDIR /src
-COPY . .
+COPY src /src/
WORKDIR "/src/Turnierplan.App"
RUN dotnet build "./Turnierplan.App.csproj" -c Release -o /app/build
@@ -21,16 +20,11 @@ RUN dotnet publish "./Turnierplan.App.csproj" -c Release --runtime linux-x64 -o
FROM base AS final
-# Since .NET 8, the official container image no longer includes the 'Kerberos' package. Since Npgsql 10.0, an
-# error is shown upon startup if that package is not installed. Because of this, we install the package manually.
-# The official documentation states you have to install the 'libkrb5-3' package:
-# - https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/8.0/krb5-libs-package
-# However, this seems to have no effect. By digging in the 'dotnet-docker' repository, you can find the Dockerfile
-# used for the .NET 6 images which states the 'libgssapi-krb5-2' package which is installed below.
-# - source: https://github.com/dotnet/dotnet-docker/blob/0a4258cc250885be5162d01f178bff5c856291f4/src/runtime-deps/6.0/jammy/amd64/Dockerfile
-RUN apt-get update \
- && apt-get -y install libgssapi-krb5-2 \
- && rm -rf /var/lib/apt/lists/*
+# Install some packages:
+# - 'gcompat' to run QuestPDF dependencies on Alpine Linux
+# - 'krb5-libs' to enable Npgsql GSS session encryption
+# - 'tzdata' contains relevant time zone information
+RUN apk add gcompat krb5-libs tzdata
# Prepare the directory used by the turnierplan.NET application
RUN mkdir "$DATA_DIRECTORY" && chown "$APP_UID" "$DATA_DIRECTORY"
@@ -39,7 +33,6 @@ RUN mkdir "$DATA_DIRECTORY" && chown "$APP_UID" "$DATA_DIRECTORY"
USER $APP_UID
WORKDIR /app
EXPOSE 8080
-EXPOSE 8081
# Copy the publish output i.e. the application binaries and the static web assets including the client application
COPY --from=publish /app/publish .
diff --git a/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml b/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml
index b306079b..8cbdf49e 100644
--- a/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml
+++ b/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml
@@ -1,10 +1,10 @@
# This docker compose file is used for running the E2E tests during the CI workflow
services:
- turnierplan.e2e.application:
+ turnierplan.e2e.application.amd64:
build:
- dockerfile: 'Turnierplan.App/Dockerfile'
- context: '../../../../'
+ dockerfile: 'docker/turnierplan-amd64/Dockerfile'
+ context: '../../../../../'
ports:
- "45001:8080"
environment:
diff --git a/src/Turnierplan.App/Client/package.json b/src/Turnierplan.App/Client/package.json
index f22d77ee..c5bb28f7 100644
--- a/src/Turnierplan.App/Client/package.json
+++ b/src/Turnierplan.App/Client/package.json
@@ -21,7 +21,7 @@
"test:ci": "ng test --watch=false --source-map=true --code-coverage --browsers ChromeHeadless",
"e2e:swap-environment": "replace-in-file \"environment.prod.ts\" \"environment.e2e.ts\" angular.json",
"e2e:swap-environment-back": "replace-in-file \"environment.e2e.ts\" \"environment.prod.ts\" angular.json",
- "e2e:prepare": "npm run e2e:swap-environment && docker compose -f cypress/docker/docker-compose.yaml up --build -d && npm run e2e:swap-environment-back",
+ "e2e:prepare:amd64": "npm run e2e:swap-environment && docker compose -f cypress/docker/docker-compose.yaml up --build -d turnierplan.e2e.application.amd64 && npm run e2e:swap-environment-back",
"e2e:destroy": "docker compose -f cypress/docker/docker-compose.yaml down",
"e2e:open": "npx cypress open",
"e2e:run": "npx cypress run"
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts
index c8b44897..ffed0b04 100644
--- a/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts
+++ b/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts
@@ -188,6 +188,7 @@ export class DocumentManagerComponent {
const document = this.documents.find((x) => x.id === id);
if (document) {
document.name = name;
+ this.sortDocuments();
}
this.currentlyUpdatingName = undefined;
},
@@ -197,6 +198,16 @@ export class DocumentManagerComponent {
});
}
+ private sortDocuments(): void {
+ this.documents = [...this.documents].sort((a, b) => {
+ const nameComparison = a.name.localeCompare(b.name);
+ if (nameComparison !== 0) {
+ return nameComparison;
+ }
+ return new Date(a.lastModifiedAt).getTime() - new Date(b.lastModifiedAt).getTime();
+ });
+ }
+
protected deleteDocument(id: string): void {
if (id === this.currentlyViewedDocumentId) {
this.displayPdfViewer = false;
diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts
index b74b4f72..135f4d39 100644
--- a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts
+++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts
@@ -255,7 +255,7 @@ export class ViewTournamentComponent implements OnInit, OnDestroy {
this.turnierplanApi.invoke(getDocuments, { tournamentId: this.tournament.id }).subscribe({
next: (documents) => {
this.documents = documents ?? [];
- this.documents.sort((a, b) => a.id.localeCompare(b.id));
+ this.sortDocuments();
this.isLoadingDocuments = false;
},
error: (error) => {
@@ -888,11 +888,24 @@ export class ViewTournamentComponent implements OnInit, OnDestroy {
return this.turnierplanApi.invoke(getDocuments, { tournamentId: this.tournament.id }).pipe(
tap((result) => {
this.documents = result;
- this.documents.sort((a, b) => a.id.localeCompare(b.id));
+ this.sortDocuments();
})
);
}
+ private sortDocuments(): void {
+ if (!this.documents) {
+ return;
+ }
+ this.documents = [...this.documents].sort((a, b) => {
+ const nameComparison = a.name.localeCompare(b.name);
+ if (nameComparison !== 0) {
+ return nameComparison;
+ }
+ return new Date(a.lastModifiedAt).getTime() - new Date(b.lastModifiedAt).getTime();
+ });
+ }
+
private setTournament(tournament: TournamentDto | undefined): void {
this.tournament = tournament;
this.titleService.setTitleFrom(tournament);
diff --git a/src/Turnierplan.App/Turnierplan.App.csproj b/src/Turnierplan.App/Turnierplan.App.csproj
index b1897606..751f1ef2 100644
--- a/src/Turnierplan.App/Turnierplan.App.csproj
+++ b/src/Turnierplan.App/Turnierplan.App.csproj
@@ -33,12 +33,6 @@
-
-
- .dockerignore
-
-
-