Skip to content

Commit 77e3850

Browse files
authored
feat: enable standalone UI in docker-compose with configurable deployment mode (#2056)
* feat: enable standalone UI in docker-compose with configurable deployment mode Signed-off-by: Eder Ignatowicz <[email protected]> * fix(ui): address PR review - separate standalone Dockerfile Address PR review feedback from @lucferbux: - Create dedicated Dockerfile.standalone with envtest support to avoid bloating production image with K8s test binaries (~150MB) - Keep main Dockerfile clean for kubeflow/federated deployments - Update docker-compose.yaml to use ui-standalone image - Add explicit --mock-mr-client=false and --mock-mr-catalog-client=false flags for better readability - Update Makefile and GitHub workflow to use new Dockerfile - Document Dockerfile structure in README Signed-off-by: Eder Ignatowicz <[email protected]> --------- Signed-off-by: Eder Ignatowicz <[email protected]>
1 parent e979e4e commit 77e3850

File tree

9 files changed

+160
-10
lines changed

9 files changed

+160
-10
lines changed

.github/workflows/build-and-push-ui-images-standalone.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ jobs:
7171
uses: docker/build-push-action@v6
7272
with:
7373
context: ./clients/ui
74+
file: ./clients/ui/Dockerfile.standalone
7475
push: true
7576
tags: ${{ steps.meta.outputs.tags }}
7677
labels: ${{ steps.meta.outputs.labels }}

clients/ui/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ COPY ${UI_SOURCE_CODE} /usr/src/app
2222
# Install the dependencies and build
2323
RUN npm cache clean --force
2424
RUN npm ci --omit=optional
25-
RUN npm run build:prod
25+
RUN DEPLOYMENT_MODE=${DEPLOYMENT_MODE} STYLE_THEME=${STYLE_THEME} npm run build:prod
2626

2727
# BFF build stage
2828
FROM ${GOLANG_BASE_IMAGE} AS bff-builder

clients/ui/Dockerfile.standalone

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Source code for the repos
2+
ARG UI_SOURCE_CODE=./frontend
3+
ARG BFF_SOURCE_CODE=./bff
4+
5+
# Set the base images for the build stages
6+
ARG NODE_BASE_IMAGE=node:22
7+
ARG GOLANG_BASE_IMAGE=golang:1.24.6
8+
ARG DISTROLESS_BASE_IMAGE=gcr.io/distroless/static:nonroot
9+
10+
# UI build stage
11+
FROM ${NODE_BASE_IMAGE} AS ui-builder
12+
13+
ARG UI_SOURCE_CODE
14+
ARG DEPLOYMENT_MODE
15+
ARG STYLE_THEME
16+
17+
WORKDIR /usr/src/app
18+
19+
# Copy the source code to the container
20+
COPY ${UI_SOURCE_CODE} /usr/src/app
21+
22+
# Install the dependencies and build
23+
RUN npm cache clean --force
24+
RUN npm ci --omit=optional
25+
RUN DEPLOYMENT_MODE=${DEPLOYMENT_MODE} STYLE_THEME=${STYLE_THEME} npm run build:prod
26+
27+
# BFF build stage
28+
FROM ${GOLANG_BASE_IMAGE} AS bff-builder
29+
30+
ARG BFF_SOURCE_CODE
31+
32+
ARG TARGETOS
33+
ARG TARGETARCH
34+
35+
WORKDIR /usr/src/app
36+
37+
# Copy the Go Modules manifests
38+
COPY ${BFF_SOURCE_CODE}/go.mod ${BFF_SOURCE_CODE}/go.sum ./
39+
40+
# Download dependencies
41+
RUN go mod download
42+
43+
# Copy the go source files
44+
COPY ${BFF_SOURCE_CODE}/cmd/ cmd/
45+
COPY ${BFF_SOURCE_CODE}/internal/ internal/
46+
47+
# Build the Go application
48+
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o bff ./cmd
49+
50+
# Install setup-envtest and download K8s binaries for mock mode
51+
RUN go install sigs.k8s.io/controller-runtime/tools/[email protected]
52+
RUN setup-envtest use 1.29.0 --bin-dir /envtest-bin -p path
53+
54+
# Final stage
55+
# Use distroless as minimal base image to package the application binary
56+
FROM ${DISTROLESS_BASE_IMAGE}
57+
WORKDIR /
58+
COPY --from=bff-builder /usr/src/app/bff ./
59+
COPY --from=ui-builder /usr/src/app/dist ./static/
60+
# Copy envtest binaries for mock K8s mode
61+
COPY --from=bff-builder /envtest-bin /envtest-bin
62+
ENV ENVTEST_ASSETS_DIR=/envtest-bin
63+
USER 65532:65532
64+
65+
# Expose port 8080
66+
EXPOSE 8080
67+
68+
ENTRYPOINT ["/bff"]

clients/ui/Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ docker-build:
8080

8181
.PHONY: docker-build-standalone
8282
docker-build-standalone:
83-
$(CONTAINER_TOOL) build --build-arg DEPLOYMENT_MODE=standalone --build-arg STYLE_THEME=mui-theme -t ${IMG_UI_STANDALONE} .
83+
$(CONTAINER_TOOL) build -f Dockerfile.standalone --build-arg DEPLOYMENT_MODE=standalone --build-arg STYLE_THEME=mui-theme -t ${IMG_UI_STANDALONE} .
8484

8585
.PHONY: docker-build-federated
8686
docker-build-federated:
@@ -92,7 +92,7 @@ docker-buildx:
9292

9393
.PHONY: docker-buildx-standalone
9494
docker-buildx-standalone:
95-
docker buildx build --build-arg DEPLOYMENT_MODE=standalone --build-arg STYLE_THEME=mui-theme --platform ${PLATFORM} -t ${IMG_UI_STANDALONE} --push .
95+
docker buildx build -f Dockerfile.standalone --build-arg DEPLOYMENT_MODE=standalone --build-arg STYLE_THEME=mui-theme --platform ${PLATFORM} -t ${IMG_UI_STANDALONE} --push .
9696

9797
.PHONY: docker-buildx-federated
9898
docker-buildx-federated:

clients/ui/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ The following Makefile targets are used to build and push the Docker images the
119119
* Command: `make docker-push-standalone`
120120
* This command uses the `CONTAINER_TOOL` and `IMG_UI_STANDALONE` environment variables to push the image.
121121

122+
### Dockerfile Structure
123+
124+
The UI has two Dockerfiles:
125+
126+
* **`Dockerfile`**: Production image for Kubeflow and federated deployments. Minimal image without test dependencies.
127+
* **`Dockerfile.standalone`**: Standalone mode image. Used for local development and docker compose.
128+
129+
The standalone image includes K8s test binaries to support the `--mock-k8s-client=true` flag at runtime.
130+
122131
## Deployments
123132

124133
For more information on how to deploy the Model Registry UI, please refer to the [Model registry UI] documentation.

clients/ui/bff/internal/integrations/kubernetes/k8mocks/base_testenv.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,27 @@ type TestEnvInput struct {
4949
}
5050

5151
func SetupEnvTest(input TestEnvInput) (*envtest.Environment, kubernetes.Interface, error) {
52-
projectRoot, err := getProjectRoot()
53-
if err != nil {
54-
input.Logger.Error("failed to find project root", slog.String("error", err.Error()))
55-
input.Cancel()
56-
os.Exit(1)
52+
var binaryAssetsDir string
53+
54+
// Check for explicit envtest assets directory (used in Docker)
55+
if envDir := os.Getenv("ENVTEST_ASSETS_DIR"); envDir != "" {
56+
// Construct full path with OS/ARCH suffix
57+
binaryAssetsDir = filepath.Join(envDir, "k8s",
58+
fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH))
59+
} else {
60+
// Fall back to project root detection (local development)
61+
projectRoot, err := getProjectRoot()
62+
if err != nil {
63+
input.Logger.Error("failed to find project root", slog.String("error", err.Error()))
64+
input.Cancel()
65+
os.Exit(1)
66+
}
67+
binaryAssetsDir = filepath.Join(projectRoot, "bin", "k8s",
68+
fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH))
5769
}
5870

5971
testEnv := &envtest.Environment{
60-
BinaryAssetsDirectory: filepath.Join(projectRoot, "bin", "k8s", fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
72+
BinaryAssetsDirectory: binaryAssetsDir,
6173
}
6274

6375
cfg, err := testEnv.Start()

clients/ui/bff/internal/integrations/kubernetes/k8mocks/mock_factory.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,15 @@ func (f *MockedStaticClientFactory) GetClient(_ context.Context) (k8s.Kubernetes
8080
}
8181

8282
func (f *MockedStaticClientFactory) ExtractRequestIdentity(httpHeader http.Header) (*k8s.RequestIdentity, error) {
83-
return f.realFactoryWithoutClient.ExtractRequestIdentity(httpHeader)
83+
identity, err := f.realFactoryWithoutClient.ExtractRequestIdentity(httpHeader)
84+
if err != nil {
85+
// In mock mode, use default test user when header is missing
86+
return &k8s.RequestIdentity{
87+
UserID: DefaultTestUsers[0].UserName,
88+
Groups: DefaultTestUsers[0].Groups,
89+
}, nil
90+
}
91+
return identity, nil
8492
}
8593
func (f *MockedStaticClientFactory) ValidateRequestIdentity(identity *k8s.RequestIdentity) error {
8694
return f.realFactoryWithoutClient.ValidateRequestIdentity(identity)

docker-compose-local.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,40 @@
33
# - a MySQL database,
44
# - a model-registry server built from source connected to the MySQL database.
55
# - a model-catalog server built from source.
6+
# - a model-registry-ui built from source (standalone mode)
67
# or
78
# - a PostgreSQL database,
89
# - a model-registry server built from source connected to the PostgreSQL database.
910
# - a model-catalog server built from source.
11+
# - a model-registry-ui built from source (standalone mode)
1012
version: '3.8'
1113
services:
14+
model-registry-ui:
15+
build:
16+
context: ./clients/ui
17+
dockerfile: Dockerfile.standalone
18+
args:
19+
DEPLOYMENT_MODE: standalone
20+
STYLE_THEME: mui-theme
21+
command:
22+
- --port=9000
23+
- --mock-k8s-client=true
24+
- --mock-mr-client=false
25+
- --mock-mr-catalog-client=false
26+
- --dev-mode=true
27+
- --deployment-mode=standalone
28+
- --log-level=DEBUG
29+
container_name: model-registry-ui
30+
ports:
31+
- "9000:9000"
32+
depends_on:
33+
- model-registry
34+
- model-catalog
35+
profiles:
36+
- postgres
37+
extra_hosts:
38+
- "localhost:host-gateway"
39+
1240
model-catalog:
1341
build:
1442
context: .

docker-compose.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,36 @@
33
# - a MySQL database,
44
# - a model-registry server connected to the MySQL database.
55
# - a model-catalog server
6+
# - a model-registry-ui (standalone mode)
67
# or
78
# - a PostgreSQL database,
89
# - a model-registry server connected to the PostgreSQL database.
910
# - a model-catalog server
11+
# - a model-registry-ui (standalone mode)
1012
version: '3.8'
1113
services:
14+
model-registry-ui:
15+
image: ghcr.io/kubeflow/model-registry/ui-standalone:latest
16+
pull_policy: always
17+
command:
18+
- --port=9000
19+
- --mock-k8s-client=true
20+
- --mock-mr-client=false
21+
- --mock-mr-catalog-client=false
22+
- --dev-mode=true
23+
- --deployment-mode=standalone
24+
- --log-level=DEBUG
25+
container_name: model-registry-ui
26+
ports:
27+
- "9000:9000"
28+
depends_on:
29+
- model-registry
30+
- model-catalog
31+
profiles:
32+
- postgres
33+
extra_hosts:
34+
- "localhost:host-gateway"
35+
1236
model-catalog:
1337
image: ghcr.io/kubeflow/model-registry/server:latest
1438
pull_policy: always

0 commit comments

Comments
 (0)