Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,4 @@ jobs:
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
65 changes: 65 additions & 0 deletions .github/workflows/containers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Create containers

on:
# run every night
schedule:
- cron: "0 22 * * *"

# schedule manually
workflow_dispatch:
inputs:
# On workflow dispatch, `branch` is selected by default
# You can access it in `github.ref_name`

tag_name:
description: "Tag name for the container"
required: true
default: "nightly"

container_repository_branch:
description: "Branch of the container repository"
required: true
default: "main"

jobs:
build-and-push-containers:
name: Build and push container images to Quay
if: github.event_name != 'schedule' || github.repository_owner == 'geo-engine'

runs-on: ubuntu-24.04

permissions:
contents: read

env:
TAG_NAME: nightly
BACKEND_CONTAINER_NAME: biois-backend
FRONTEND_CONTAINER_NAME: biois-frontend

steps:
- name: Modify TAG_NAME if on `tag_name` is set on `workflow_dispatch`
if: github.event.inputs.tag_name != ''
run: |
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV

- name: Checkout code
uses: actions/checkout@v6

- name: Login to quay.io
run: podman login -u="geoengine+bot" -p="${{secrets.QUAY_IO_TOKEN}}" quay.io

- name: Build containers
run: |
just build-backend-container
just build-frontend-container

- name: Push image to quay.io
run: |
podman push ${{env.BACKEND_CONTAINER_NAME}}:${{env.TAG_NAME}} quay.io/geoengine/${{env.BACKEND_CONTAINER_NAME}}:${{env.TAG_NAME}}
podman push ${{env.FRONTEND_CONTAINER_NAME}}:${{env.TAG_NAME}} quay.io/geoengine/${{env.FRONTEND_CONTAINER_NAME}}:${{env.TAG_NAME}}

- name: Push nightly with date
if: env.TAG_NAME == 'nightly'
run: |
podman push ${{env.BACKEND_CONTAINER_NAME}}:${{env.TAG_NAME}} quay.io/geoengine/${{env.BACKEND_CONTAINER_NAME}}:${{env.TAG_NAME}}-$(date +'%Y-%m-%d')
podman push ${{env.FRONTEND_CONTAINER_NAME}}:${{env.TAG_NAME}} quay.io/geoengine/${{env.FRONTEND_CONTAINER_NAME}}:${{env.TAG_NAME}}-$(date +'%Y-%m-%d')
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ The API client is a TypeScript library generated from the OpenAPI specification

### [Frontend](frontend/README.md)

_TODO: Add description of frontend component._
The frontend is an Angular application that provides a user interface for interacting with the BioIS service.
11 changes: 11 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
target
.git
.gitignore
node_modules
dist
*.log
*.env
/.venv
# Keep Cargo.lock in context for reproducible builds
# Cargo.lock
**/target
39 changes: 39 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Backend multi-stage build:
# 1. build with Rust
# 2. copy release binary into distroless final image

FROM quay.io/geoengine/devcontainer:latest AS builder

WORKDIR /usr/src/biois

# Cache dependencies: copy manifest first
COPY \
Cargo.toml \
Cargo.lock \
rust-toolchain.toml \
./
# create dummy src to allow caching of cargo registry build step
RUN mkdir src && printf "fn main() { println!(\"cargo build cache\"); }\n" > src/main.rs
RUN cargo build --release
RUN rm -rf src

# Copy full source and build the release binary
COPY conf ./conf
COPY migrations ./migrations
COPY src ./src
COPY build.rs ./build.rs
RUN cargo build --release --bin biois

# Strip the binary to reduce size if possible
RUN strip target/release/biois || true

# Final image: distroless for minimal attack surface and size
FROM gcr.io/distroless/cc-debian13:nonroot

# Copy ca-certificates and the built binary
COPY --from=builder /usr/src/biois/target/release/biois /usr/local/bin/biois
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

USER nonroot
EXPOSE 4040
ENTRYPOINT ["/usr/local/bin/biois"]
12 changes: 12 additions & 0 deletions frontend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules
dist
.git
.gitignore
npm-debug.log
yarn-error.log
.cache
/.idea
/.vscode
coverage
docs
*.local
34 changes: 34 additions & 0 deletions frontend/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
# Disable Caddy admin api
admin off
}

:80

# Enable Compression
encode zstd gzip

# Proxy API requests to the backend
handle /api* {
uri strip_prefix /api

reverse_proxy biois-backend:4040
}

# Match static assets
@static {
file
path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.ico *.woff *.woff2
}

# Set Cache-Control to 1 day (86400 seconds)
header @static Cache-Control "public, max-age=86400"

# ALWAYS keep index.html fresh
header /index.html Cache-Control "no-cache, no-store, must-revalidate"

handle {
root * /usr/share/caddy
try_files {path} /index.html
file_server
}
38 changes: 38 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Frontend multi-stage build:
# 1. build with devcontainer
# 2. serve static with Caddy

FROM quay.io/geoengine/devcontainer:latest AS builder

# 1.1 Install dependencies

WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci --silent

WORKDIR /app/api-client/typescript
COPY api-client/typescript .
RUN npm ci --silent

# 1.2 Copy source and build
WORKDIR /app/frontend
COPY frontend/public ./public
COPY frontend/src ./src
COPY \
frontend/_theme-colors.scss \
frontend/angular.json \
frontend/tsconfig*.json \
./
RUN npm run build --silent

# 2. Final image: Caddy to serve static files and reverse-proxy API
FROM caddy:2-alpine AS runner

# Copy built site into caddy's web root
COPY --from=builder /app/frontend/dist/BioIS/browser /usr/share/caddy

# Copy Caddyfile for SPA fallback and API proxy
COPY frontend/Caddyfile /etc/caddy/Caddyfile

EXPOSE 80
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
3 changes: 3 additions & 0 deletions frontend/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
},
"configurations": {
"production": {
"buildTarget": "BioIS:build:production"
Expand Down
9 changes: 9 additions & 0 deletions frontend/proxy.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"/api": {
"target": "http://localhost:4040",
"secure": false,
"changeOrigin": true,
"logLevel": "info",
"pathRewrite": { "^/api": "" }
}
}
4 changes: 2 additions & 2 deletions frontend/src/app/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class UserService {
accessToken: 'Bearer ' + this.user()?.id /* TODO: handle missing/expired token */,
};
const config = createConfiguration({
baseServer: new ServerConfiguration('http://localhost:4040', {}),
baseServer: new ServerConfiguration('/api', {}),
// authMethods: authMethods as AuthMethodsConfiguration,
authMethods: {
default: {
Expand Down Expand Up @@ -105,7 +105,7 @@ function oidcRedirectUri(): string {

function configuration(/*options: { authMethods?: OAuth2Configuration } = {}*/): Configuration {
return createConfiguration({
baseServer: new ServerConfiguration('http://localhost:4040', {}),
baseServer: new ServerConfiguration('/api', {}),
// ...options,
});
}
Expand Down
57 changes: 57 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ build-frontend:
npm ci
npm run build

# Build the frontend container. Usage: `just build-frontend-container`.
[group('build')]
[group('container')]
build-frontend-container:
@-clear
podman build \
-f frontend/Dockerfile \
-t biois-frontend:latest \
.

# Build the frontend container. Usage: `just build-frontend-container`.
[group('build')]
[group('container')]
build-backend-container:
@-clear
podman build \
-f backend/Dockerfile \
-t biois-backend:latest \
backend

### LINT ###

[group('api-client')]
Expand Down Expand Up @@ -204,6 +224,43 @@ run-frontend:
@-clear
npm run ng serve

# Run the backend container in dev mode. Usage: `just run-backend-container`.
[group('container')]
[group('run')]
run-backend-container: build-backend-container
podman run --rm --replace \
--name biois-backend-dev \
--network host \
-p 4040:4040 \
-v $(pwd)/backend/Settings.toml:/usr/local/bin/Settings.toml \
biois-backend:latest

# Run the frontend container in dev mode. Usage: `just run-frontend-container`.
[group('container')]
[group('run')]
run-frontend-container: build-frontend-container
podman run --rm --replace \
--name biois-frontend-dev \
-p 4200:80 \
biois-frontend:latest

# Run the container as a pod in dev mode. Usage: `just run-pod`.
[group('container')]
[group('run')]
run-pod: build-backend-container build-frontend-container
cat k8s/dev-config.yaml k8s/pod.yaml | \
podman play kube \
--network=pasta:-T,3030:3030 `# Map local Geo Engine at port 3030 into pod` \
--replace -

# Stop the pod in dev mode. Usage: `just down-pod`.
[group('container')]
[group('run')]
down-pod:
cat k8s/dev-config.yaml k8s/pod.yaml | \
podman play kube \
--down -

### MISC ###

# Generate the OpenAPI spec and write it to `openapi.json`.
Expand Down
59 changes: 59 additions & 0 deletions k8s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Containers

This directory contains Kubernetes manifests for running the BioIS backend and frontend in containers.
The manifests are designed for development and testing purposes, using a single Pod to host both services and a PVC for PostgreSQL data persistence.

## Running with Podman

Build images with Podman (from repository root):

```bash
just build-backend-container
just build-frontend-container
```

You can run them individually with `podman run`:

```bash
just run-frontend-container
just run-backend-container
```

## Running with Podman and Kubernetes manifests

Run locally using `podman play kube` with the provided manifest. The manifest now contains a single Pod and a PVC; Podman does not support Service objects.

Before applying, create a named Podman volume and use its mountpoint as the hostPath backing store for Postgres (no PersistentVolume resource is required):

```bash
# optionally, create a named podman volume
podman volume create biois-postgres

# build containers and run pods
just run-pod
```

Inspect running containers and ports:

```bash
podman ps
podman port <container-id-or-name>
```

Stop the deployed resources:

```bash
just down-pod
```

## Podman: Persistent volumes & Secrets

- The development `ConfigMap` and `Secret` are in `k8s/dev-config.yaml`.
It contains the `biois-config` ConfigMap and `biois-postgres-secret` Secret used by the Pod.
To override credentials, update that file or create a different Secret and apply it before the Pod.

Example: create an overriding secret and then apply manifests

```bash
kubectl create secret generic biois-postgres-secret --from-literal=POSTGRES_USER=youruser --from-literal=POSTGRES_PASSWORD=yourpass --from-literal=POSTGRES_DB=yourdb
```
Loading
Loading