Containerize Bifrost deployment

This commit is contained in:
Meta Vens 2026-05-20 19:07:45 +01:00
commit f1823c2a59
7 changed files with 186 additions and 11 deletions

22
.dockerignore Normal file
View file

@ -0,0 +1,22 @@
.git
.github
.agents
.bifrost
.voidbot
bin
obj
**/bin
**/obj
**/.vs
**/.vscode
**/.idea
**/TestResults
**/*.user
**/*.suo
.env
.env.*
!.env.local.example
README.md
docs
plugins
tools

4
.env.local.example Normal file
View file

@ -0,0 +1,4 @@
# Optional local overrides for Docker Compose.
# The default compose.local.yaml is self-contained for local smoke testing.
COMPOSE_PROJECT_NAME=bifrost-local

54
.github/workflows/publish-container.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Publish Container
on:
push:
branches:
- main
tags:
- "v*"
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: gamecult/bifrost
jobs:
publish:
name: Build and publish Bifrost image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-
type=ref,event=branch
type=ref,event=tag
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

29
Dockerfile Normal file
View file

@ -0,0 +1,29 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY src/Bifrost.Web/Bifrost.Web.csproj src/Bifrost.Web/
RUN dotnet restore src/Bifrost.Web/Bifrost.Web.csproj
COPY . .
RUN dotnet publish src/Bifrost.Web/Bifrost.Web.csproj \
--configuration Release \
--output /app/publish \
/p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV ASPNETCORE_URLS=http://0.0.0.0:5080 \
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
DOTNET_RUNNING_IN_CONTAINER=true
EXPOSE 5080
COPY --from=build /app/publish .
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS http://127.0.0.1:5080/healthz || exit 1
ENTRYPOINT ["dotnet", "Bifrost.Web.dll"]

View file

@ -15,6 +15,7 @@ This repository now includes a viable alpha foundation slice of the ASP.NET Core
- health and readiness endpoints plus startup config validation
- GitHub App webhook ingestion for issue, pull request, and review sync
- local bridge tooling for agent-owned GitHub draft PRs and Discord posts
- Docker image and local Compose stack for containerized smoke testing
- xUnit integration tests under `tests/Bifrost.Web.Tests`
- matching deploy artifacts and runbooks in `E:\Projects\gamecult-ops`
@ -41,7 +42,7 @@ It is closer to deployable now, but it is still not a finished internet-facing m
- public app hostname: `bifrost.gamecult.org`
- infrastructure context: `Yggdrasil`
- deployment model: nginx + systemd + localhost Postgres, following the existing GameCult ops pattern
- deployment model: GHCR image + Docker Compose behind nginx, with the older systemd path kept as rollback until the container path is boring
## Next Build Target
@ -62,6 +63,8 @@ For the full staged roadmap, including contribution points, revenue share, patro
- default connection string points at local PostgreSQL
- build with `dotnet build Bifrost.slnx`
- test with `DOTNET_ROLL_FORWARD=Major dotnet test Bifrost.slnx` if the machine only has the .NET 10 runtime installed
- container smoke test with `docker compose -f compose.local.yaml up --build`
- container health checks live at `http://127.0.0.1:5080/healthz` and `http://127.0.0.1:5080/readyz`
## Read First

50
compose.local.yaml Normal file
View file

@ -0,0 +1,50 @@
name: bifrost-local
services:
postgres:
image: postgres:16
container_name: bifrost-postgres
environment:
POSTGRES_DB: bifrost_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "127.0.0.1:55432:5432"
volumes:
- bifrost-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d bifrost_dev"]
interval: 10s
timeout: 5s
retries: 5
web:
build:
context: .
dockerfile: Dockerfile
image: ghcr.io/gamecult/bifrost:local
container_name: bifrost-web
depends_on:
postgres:
condition: service_healthy
environment:
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://0.0.0.0:5080
ConnectionStrings__Bifrost: Host=postgres;Database=bifrost_dev;Username=postgres;Password=postgres
Bootstrap__ApplyMigrationsOnStartup: "true"
GitHubOAuth__ClientId: local-dev-client
GitHubOAuth__ClientSecret: local-dev-secret
GitHubApp__EnableWebhookSync: "false"
Host__PublicBaseUrl: http://localhost:5080
Host__ExpectedHost: localhost
Host__RequireStrictHostValidation: "false"
DataProtection__KeysPath: /var/lib/bifrost/data-protection-keys
AllowedHosts: localhost;127.0.0.1
ports:
- "127.0.0.1:5080:5080"
volumes:
- bifrost-data-protection:/var/lib/bifrost/data-protection-keys
volumes:
bifrost-postgres:
bifrost-data-protection:

View file

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
@ -67,23 +68,34 @@ builder.Services.AddScoped<ApplicationBootstrapper>();
builder.Services.AddSingleton<StartupConfigurationValidator>();
builder.Services.AddScoped<IAuthorizationHandler, ActiveMemberAuthorizationHandler>();
var dataProtectionKeysPath = builder.Configuration["DataProtection:KeysPath"];
if (!string.IsNullOrWhiteSpace(dataProtectionKeysPath))
{
builder.Services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeysPath));
}
var configuredGitHubOAuthOptions = builder.Configuration
.GetSection(GitHubOAuthOptions.SectionName)
.Get<GitHubOAuthOptions>() ?? new GitHubOAuthOptions();
var authenticationBuilder = builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/auth/sign-in";
options.AccessDeniedPath = "/Membership/Status";
options.SlidingExpiration = true;
})
.AddOAuth(GitHubAuthenticationDefaults.AuthenticationScheme, options =>
{
var gitHubOptions = builder.Configuration
.GetSection(GitHubOAuthOptions.SectionName)
.Get<GitHubOAuthOptions>() ?? new GitHubOAuthOptions();
});
options.ClientId = gitHubOptions.ClientId;
options.ClientSecret = gitHubOptions.ClientSecret;
options.CallbackPath = gitHubOptions.CallbackPath;
if (configuredGitHubOAuthOptions.IsConfigured)
{
authenticationBuilder.AddOAuth(GitHubAuthenticationDefaults.AuthenticationScheme, options =>
{
options.ClientId = configuredGitHubOAuthOptions.ClientId;
options.ClientSecret = configuredGitHubOAuthOptions.ClientSecret;
options.CallbackPath = configuredGitHubOAuthOptions.CallbackPath;
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user";
@ -118,6 +130,7 @@ builder.Services
}
};
});
}
builder.Services.AddAuthorization(options =>
{