diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c2e1aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +**/.git +**/.gitignore +**/.vs +**/.vscode +**/bin +**/obj +**/TestResults +**/node_modules +Dockerfile +README.md diff --git a/.forgejo/workflows/publish-container.yml b/.forgejo/workflows/publish-container.yml new file mode 100644 index 0000000..467bdb0 --- /dev/null +++ b/.forgejo/workflows/publish-container.yml @@ -0,0 +1,123 @@ +name: Publish Container + +on: + push: + branches: + - master + workflow_dispatch: + +env: + DOTNET_VERSION: 10.0.x + REGISTRY: ${{ vars.FORGEJO_REGISTRY }} + IMAGE_NAMESPACE: ${{ vars.IMAGE_NAMESPACE }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + +jobs: + publish: + runs-on: docker + env: + # Use explicit override when provided, otherwise use the known-good host + DOCKER_HOST: ${{ vars.DOCKER_HOST != '' && vars.DOCKER_HOST || 'tcp://172.17.0.1:2375' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Validate workflow variables + run: | + set -eu + if [ -z "${REGISTRY}" ]; then echo "vars.FORGEJO_REGISTRY is required"; exit 1; fi + if [ -z "${IMAGE_NAMESPACE}" ]; then echo "vars.IMAGE_NAMESPACE is required"; exit 1; fi + if [ -z "${IMAGE_NAME}" ]; then echo "vars.IMAGE_NAME is required"; exit 1; fi + + - name: Validate registry secrets + run: | + set -eu + if [ -z "${{ secrets.FORGEJO_REGISTRY_USERNAME }}" ]; then echo "secrets.FORGEJO_REGISTRY_USERNAME is required"; exit 1; fi + if [ -z "${{ secrets.FORGEJO_REGISTRY_TOKEN }}" ]; then echo "secrets.FORGEJO_REGISTRY_TOKEN is required"; exit 1; fi + + - name: Ensure Docker CLI exists + run: | + set -eu + if command -v docker >/dev/null 2>&1; then + docker --version + exit 0 + fi + + ARCH="$(uname -m)" + case "${ARCH}" in + x86_64) DOCKER_ARCH="x86_64" ;; + aarch64|arm64) DOCKER_ARCH="aarch64" ;; + *) echo "Unsupported architecture for Docker CLI bootstrap: ${ARCH}"; exit 1 ;; + esac + + DOCKER_CLI_VERSION="27.5.1" + curl -fsSL "https://download.docker.com/linux/static/stable/${DOCKER_ARCH}/docker-${DOCKER_CLI_VERSION}.tgz" -o docker.tgz + tar -xzf docker.tgz + mkdir -p "${HOME}/.local/bin" + mv docker/docker "${HOME}/.local/bin/docker" + chmod +x "${HOME}/.local/bin/docker" + echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" + "${HOME}/.local/bin/docker" --version + + - name: Ensure Docker Buildx exists + run: | + set -eu + if docker buildx version >/dev/null 2>&1; then + docker buildx version + exit 0 + fi + + ARCH="$(uname -m)" + case "${ARCH}" in + x86_64) BUILDX_ARCH="amd64" ;; + aarch64|arm64) BUILDX_ARCH="arm64" ;; + *) echo "Unsupported architecture for Docker Buildx bootstrap: ${ARCH}"; exit 1 ;; + esac + + BUILDX_VERSION="v0.21.1" + mkdir -p "${HOME}/.docker/cli-plugins" + curl -fsSL "https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-${BUILDX_ARCH}" -o "${HOME}/.docker/cli-plugins/docker-buildx" + chmod +x "${HOME}/.docker/cli-plugins/docker-buildx" + docker buildx version + + # Simplified: we trust DOCKER_HOST (default to tcp://172.17.0.1:2375). If you + # need a different endpoint, set the Forgejo variable `DOCKER_HOST`. + + - name: Check Docker daemon connectivity + run: | + set -eu + echo "Using DOCKER_HOST=${DOCKER_HOST}" + docker version + docker info >/dev/null + + - name: Create Buildx builder + run: | + set -eu + docker buildx rm forgejo-builder >/dev/null 2>&1 || true + docker buildx create --name forgejo-builder --driver docker-container --use + docker buildx inspect --bootstrap + + - name: Restore and publish app + run: dotnet publish src/MaddoScientisto.Web/MaddoScientisto.Web.csproj -c Release -o ./artifacts/publish + + - name: Login to Forgejo registry + run: | + echo "${{ secrets.FORGEJO_REGISTRY_TOKEN }}" | docker login "${REGISTRY}" -u "${{ secrets.FORGEJO_REGISTRY_USERNAME }}" --password-stdin + + - name: Build and push image + run: | + set -eu + IMAGE_REF="${REGISTRY}/${IMAGE_NAMESPACE}/${IMAGE_NAME}" + SHORT_SHA="$(echo "${GITHUB_SHA}" | cut -c1-12)" + docker buildx build \ + --builder forgejo-builder \ + --tag "${IMAGE_REF}:sha-${SHORT_SHA}" \ + --tag "${IMAGE_REF}:latest" \ + --push \ + . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..39045ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY ["WorkTracker.csproj", "./"] +RUN dotnet restore "WorkTracker.csproj" + +COPY . . +RUN dotnet publish "WorkTracker.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends wget \ + && rm -rf /var/lib/apt/lists/* + +ENV ASPNETCORE_URLS=http://+:8080 + +COPY --from=build /app/publish . + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:8080/ >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["dotnet", "WorkTracker.dll"] diff --git a/Program.cs b/Program.cs index 6c6acf7..f466448 100644 --- a/Program.cs +++ b/Program.cs @@ -89,7 +89,11 @@ else app.UseHsts(); } -app.UseHttpsRedirection(); +var useHttpsRedirection = app.Configuration.GetValue("UseHttpsRedirection", !app.Environment.IsDevelopment()); +if (useHttpsRedirection) +{ + app.UseHttpsRedirection(); +} app.UseRequestLocalization(localizationOptions); diff --git a/appsettings.json b/appsettings.json index 6f5de7d..45c2bee 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "DataSource=Data\\app.db;Cache=Shared" + "DefaultConnection": "Data Source=Data/app.db;Cache=Shared" }, "MongoDb": { "ConnectionString": "mongodb://localhost:27017", diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8aa3080 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + worktracker: + build: + context: . + dockerfile: Dockerfile + image: ${IMAGE_REGISTRY:-worktracker}:${IMAGE_TAG:-latest} + environment: + ASPNETCORE_ENVIRONMENT: Production + UseHttpsRedirection: "false" + ConnectionStrings__DefaultConnection: Data Source=/app/Data/app.db;Cache=Shared + MongoDb__ConnectionString: mongodb://mongo:27017 + ports: + - "8002:8080" + depends_on: + - mongo + volumes: + - worktracker_data:/app/Data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ >/dev/null 2>&1 || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + + mongo: + image: mongo:7 + restart: unless-stopped + volumes: + - mongo_data:/data/db + +volumes: + worktracker_data: + mongo_data: diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..2f1009c --- /dev/null +++ b/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(?:js|mjs|css|ico|gif|jpe?g|png|svg|webp|woff2?)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800, immutable"; + try_files $uri =404; + } +}