Containerize Bifrost deployment
This commit is contained in:
parent
43a5d1297b
commit
f1823c2a59
7 changed files with 186 additions and 11 deletions
22
.dockerignore
Normal file
22
.dockerignore
Normal 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
4
.env.local.example
Normal 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
54
.github/workflows/publish-container.yml
vendored
Normal 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
29
Dockerfile
Normal 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"]
|
||||||
|
|
@ -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
|
- health and readiness endpoints plus startup config validation
|
||||||
- GitHub App webhook ingestion for issue, pull request, and review sync
|
- GitHub App webhook ingestion for issue, pull request, and review sync
|
||||||
- local bridge tooling for agent-owned GitHub draft PRs and Discord posts
|
- 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`
|
- xUnit integration tests under `tests/Bifrost.Web.Tests`
|
||||||
- matching deploy artifacts and runbooks in `E:\Projects\gamecult-ops`
|
- 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`
|
- public app hostname: `bifrost.gamecult.org`
|
||||||
- infrastructure context: `Yggdrasil`
|
- 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
|
## 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
|
- default connection string points at local PostgreSQL
|
||||||
- build with `dotnet build Bifrost.slnx`
|
- 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
|
- 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
|
## Read First
|
||||||
|
|
||||||
|
|
|
||||||
50
compose.local.yaml
Normal file
50
compose.local.yaml
Normal 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:
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.HttpLogging;
|
using Microsoft.AspNetCore.HttpLogging;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
@ -67,23 +68,34 @@ builder.Services.AddScoped<ApplicationBootstrapper>();
|
||||||
builder.Services.AddSingleton<StartupConfigurationValidator>();
|
builder.Services.AddSingleton<StartupConfigurationValidator>();
|
||||||
builder.Services.AddScoped<IAuthorizationHandler, ActiveMemberAuthorizationHandler>();
|
builder.Services.AddScoped<IAuthorizationHandler, ActiveMemberAuthorizationHandler>();
|
||||||
|
|
||||||
builder.Services
|
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)
|
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
.AddCookie(options =>
|
.AddCookie(options =>
|
||||||
{
|
{
|
||||||
options.LoginPath = "/auth/sign-in";
|
options.LoginPath = "/auth/sign-in";
|
||||||
options.AccessDeniedPath = "/Membership/Status";
|
options.AccessDeniedPath = "/Membership/Status";
|
||||||
options.SlidingExpiration = true;
|
options.SlidingExpiration = true;
|
||||||
})
|
});
|
||||||
.AddOAuth(GitHubAuthenticationDefaults.AuthenticationScheme, options =>
|
|
||||||
{
|
|
||||||
var gitHubOptions = builder.Configuration
|
|
||||||
.GetSection(GitHubOAuthOptions.SectionName)
|
|
||||||
.Get<GitHubOAuthOptions>() ?? new GitHubOAuthOptions();
|
|
||||||
|
|
||||||
options.ClientId = gitHubOptions.ClientId;
|
if (configuredGitHubOAuthOptions.IsConfigured)
|
||||||
options.ClientSecret = gitHubOptions.ClientSecret;
|
{
|
||||||
options.CallbackPath = gitHubOptions.CallbackPath;
|
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.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
|
||||||
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
|
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
|
||||||
options.UserInformationEndpoint = "https://api.github.com/user";
|
options.UserInformationEndpoint = "https://api.github.com/user";
|
||||||
|
|
@ -118,6 +130,7 @@ builder.Services
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
builder.Services.AddAuthorization(options =>
|
builder.Services.AddAuthorization(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue