SIAG ES Digital Exhibit 2026
A React + Vite Project for building kiosk-style digital exhibits: a PWA-capable single-page app with schema-driven configuration (via .env and URL params), an idle-triggered screensaver, offline-cacheable media, and analytics tracking. ()
Tech Stack
- React 19 + Vite
- react-router (
HashRouterby default,BrowserRouteravailable) - vite-plugin-pwa / Workbox for offline support
- SCSS, @ui-marcom design system
- Playwright for tests
- ESLint + Prettier + Stylelint
- JSDoc + clean-jsdoc-theme for generated code documentation
Getting Started
npm install
cp .env-sample .env # then adjust values, see "Configuration" below
npm run devThe app uses HashRouter, so URLs look like http://localhost:5173/#/. This matters for URL search params too - see the config section below.
If you switch to BrowserRouter (src/BrowserRouter.jsx, see App.jsx), VITE_BASE_URL is used both as Vite's base and as BrowserRouter's basename - it must be a path (e.g. / or /some-subpath/), not a full URL with protocol/host.
Available Scripts
| Script | Description |
|---|---|
npm run dev | Start the Vite dev server |
npm run build | Production build |
npm run preview | Preview a production build locally |
npm run lint | Run ESLint + Stylelint |
npm run lint:fix | Same, with autofix |
npm run test | Run Playwright tests |
npm run test:ui | Run Playwright tests in UI mode |
npm run docs | Generate the JSDoc site into docs/ (see below) |
npm run docs:serve | Serve the generated docs/ site on port 8080 |
npm run sbom | Generate a CycloneDX SBOM (sbom.xml) |
npm run validate:content | Validate public/data/*.json (schema + references + assets, see "Content") |
Configuration
Config values come from two sources - .env variables and URL search params - and are combined into a single, typed object via one hook: useConfig() (src/services/config/config.jsx).
Adding a new config value does not require writing new hook logic. Add one entry to CONFIG_SCHEMA in src/services/config/config-schema.js:
{
key: "screensaverTimeout", // property name on the returned config object
source: "param", // "env" (import.meta.env) or "param" (URL search param)
name: "screensaverTimeout", // the actual env var name (e.g. "VITE_...") or param name
type: "number", // "boolean" | "number" | "string"
default: 180,
min: 0, // optional, "number" type only
description: "Seconds of inactivity before the screensaver starts.",
}Values are validated/coerced generically by parseConfigValue() (src/utils/validationFunctions.js) - invalid or missing values fall back to default and log a warning.
The current schema (CONFIG_SCHEMA in config-schema.js) includes screensaverTimeout, screensaverFadeDuration, defaultMode (selects which screensaver video plays, see "Screensaver" below), contentFile, the globe* entries tuning InteractiveGlobe, and the stage*/stageDevice entries picking Stage's reference resolution (see "Scaling (Stage)" below).
Because the app uses HashRouter, URL params must come after the #:
http://localhost:5173/#/?screensaverTimeout=5&screensaverFadeDuration=300A param placed before the # is invisible to useConfig() and silently falls back to the default. Navigation preserves the params: slide navigation, the app selection and the menu's "Apps" button all carry the current query string along, so a kiosk keeps its configuration for its entire uptime.
Presentation Modes & Navigation
The app hosts 3 presentation modes/sales apps, defined once in src/services/presentationModes.js (PRESENTATION_MODES, { key, label, path, orientation, contentFile|contentFiles }):
value-partnership→/value-partnership(landscape; bespoke interactive sub-app with its own nested routes - globe, excellence, partnerships - not aSlidePagedeck like the two below)value-advisory-services→/value-advisory-services(landscape)rad-enablement→/rad-enablement(portrait-only)
orientation ("landscape" | "portrait") is each app's fixed, content-authored physical orientation - confirmed with the content team, not something a device negotiates at runtime. resolveModeForPath() (same file) maps a route to its mode - the one place this lookup happens, reused by Stage (see "Scaling (Stage)" below) and GeneralPageTracking so they can't disagree about which mode a path belongs to. / (app selection) matches no mode and defaults to landscape.
/ always renders the app selection screen (src/pages/AppSelection/AppSelection.jsx): one tile per mode, and the target of the idle session reset (see "Screensaver"). The 3 app routes are also always reachable directly by URL. Routing lives in src/Routes.jsx.
The two content-driven apps share one page skeleton, SlidePage (src/components/SlidePage/SlidePage.jsx): it resolves the :viewId route param against the loaded content, renders the view's elements via ComponentRenderer, and mounts SlideNavigation. Each page (ValueAdvisoryServices.jsx, RadEnablement.jsx) only supplies its constants, CSS prefix and an optional header. SlidePage also handles the failure states: a visible message while content can't be fetched, and a redirect to the start view for unknown view ids.
SlideNavigation (src/components/SlideNavigation/SlideNavigation.jsx) is the fixed bottom bar with three modes: the default prev/next arrows plus menu button; an expanded menu bar (Home / Screensaver / Apps / Slides / close); and the slides overlay (src/components/SlidesOverlay/SlidesOverlay.jsx) - a full-screen grid of slide thumbnails grouped by story, with a story toggle switch, for jumping directly to any slide. Thumbnails come from the optional thumbnail field of a view in the content JSON (/assets/thumbs/...); views without one get a text placeholder tile.
Page/app switches use the native View Transitions API directly via navigateWithTransition (src/utils/viewTransition.js, document.startViewTransition + flushSync) for a light cross-fade (tuned in src/styles/index.scss). react-router's own viewTransition navigate option is not used here - it only works with the Data Router API (createHashRouter/RouterProvider), and this app uses the plain declarative <HashRouter>/<Routes> instead.
Screensaver
ScreensaverProvider (src/services/screensaver.jsx) combines idle detection (useIdle, src/hooks/idle.jsx) with the screensaverTimeout/screensaverFadeDuration config values and exposes isIdle plus a manual trigger (showNow, wired to the menu's "Screensaver" button) via useScreensaver().
Waking from idle also resets the kiosk session: while the screensaver is still fading out, the app navigates back to the app selection (/, config params preserved), so the next visitor starts fresh instead of inheriting the previous visitor's route, open menus and selections. The reset deliberately happens on wake-up rather than at idle start - the screensaver video is defined by the current route's content file, and navigating away at idle start would unload it mid-show.
The actual overlay is src/components/Screensaver/Screensaver.jsx - stays mounted at all times so the CSS opacity transition plays both when fading in and out (the video is paused and rewound while not idle, so it doesn't decode 24/7 behind the app). Its video depends on defaultMode: looked up from the current route's content file's screensavers object (see below), keyed by that mode's own key. With the default defaultMode "all" no entry matches, so no video is shown.
ScreensaverProvider and ContentProvider need router context (useConfig() uses useSearchParams), so they're mounted inside <HashRouter> in src/HashRouter.jsx, not in App.jsx.
Scaling (Stage)
The exhibit runs on a small, fixed set of physical devices - a 4K kiosk panel and a 13" iPad - each shown at a real distance/size very different from a normal responsive website, and each needing to render every app (landscape and, for Rad Enablement, portrait) pixel-crisp, never blurry. Stage (src/components/Stage/Stage.jsx) is the single mechanism that makes that work: it renders the whole app inside a fixed-size reference box and scales that box down (transform: scale(), contain-style - never up) to fit whatever real screen it lands on. Because it's a whole-app transform, no component ever needs its own device/breakpoint logic to look right on a different screen - it just needs to be sized in em/rem like anything else, and Stage takes care of the rest. This replaced an earlier per-page calc(1rem / X) + html-root-font-size scheme that fought against @ui-marcom/shared's own independent viewport-width breakpoints and caused cross-component regressions (fixing one component's size would silently break another's) - if you're touching scaling code, that history is worth knowing before reaching for another root-font-size trick.
Two independent axes pick the reference box, both read via useConfig():
- Device (
stageDeviceconfig value,"4k"default or"ipad") - which physical kiosk this is. A 4K panel and an iPad need their own separate landscape width/height pairs (stageRefWidth/stageRefHeightfor"4k",stageIpadRefWidth/stageIpadRefHeightfor"ipad") rather than a shared one, because the iPad is a 2x Retina display - its CSS point resolution is roughly half its physical pixel count, nothing like a 4K panel's 1:1 mapping. On a real kiosk, this is set once in that device's fixed launch URL, e.g..../#/?stageDevice=ipad. - Orientation - from the current route's
PRESENTATION_MODESentry (see "Presentation Modes" above). Portrait routes use the active device's own pair swapped, not a separately-guessed number - "the same panel, physically rotated" only holds true within one device, so a 4K-panel-in-portrait and an iPad-in-portrait get different reference boxes even though both are "portrait".
What this means when you're building or editing a slide:
- Size everything in
em/rem, neverpx,vw, orvh.vw/vhresolve against the real browser viewport and ignore Stage's transform entirely, so they silently size against the wrong box the moment the scale factor isn't 1 - use%against a sized ancestor instead. - The iPad's reference canvas is proportionally narrower/shorter than the 4K one (different aspect ratio, not just a smaller version of the same shape), so content tuned only by eye against the 4K canvas can still overflow on the iPad even after Stage's own scaling.
.rad-pagecarries an iPad-only content-density correction ([data-stage-device="ipad"] .rad-page { font-size: calc(1em / ...) },RadEnablement.scss) for exactly this - a new page that turns out to run on both devices may need the same pattern. - For any element whose size must never exceed the space it's actually in (rather than just being smaller than average) - a ring, a card, anything that would visibly clip if the density correction alone isn't enough - don't hand-tune another fixed
emguess. Cap it with CSS Container Queries instead:container-type: sizeon the sizing ancestor, thenmin(<original em size>, <N>cqmin/cqh>)on the thing that must fit.SelectWidget.scss/SelectWidget.jsx(the ring/button picker) is the worked example for a fixed-shape element with JS-computed positions kept in sync with a CSS-capped size (measure the actual rendered box viauseElementSize,src/hooks/elementSize.jsx, rather than assuming the originalemvalue always won);InfoGridWidget.scssis the worked example for a whole content block (textbox + grid) that needs to shrink as one proportional unit to fit whatever vertical space is actually left below the headline, deriving each capped value'scqhnumber as (that value's original em number ÷ the content's total natural em height) × 100 so every contributor shrinks in the same proportion. - Test new/changed slides at both
?stageDevice=4k(default) and?stageDevice=ipad, in the route's real orientation - Chrome DevTools' device toolbar with a matching viewport size is enough, no physical device needed for a first pass. Match the viewport to the actual reference resolution when testing anything built withcqh/cqw/cqmin(2160x3840 portrait for4k, 1024x1366 portrait foripad, see the values above): under atransform: scale()ancestor (which is whatStagealways is), Chrome resolves container query units against the container's real, on-screen size, not its pre-transform reference-box size - the same asem/remonly when Stage's scale factor is exactly 1 (i.e. the browser window's actual size matches the reference resolution). A smaller/mismatched test window makesemandcq*values drift apart in a way that isn't representative of the real device and won't reproduce there. - Known gap: the iPad density correction fixes most Rad Enablement content, but only
SelectWidgetandInfoGridWidgethave the container-query cap described above so far. A content-heavy slide (long headline + logo row + stat card + quote, say) can still overflow at the bottom on the iPad even though the identical slide fits comfortably on the 4K device - if you hit that, that slide's own elements need the same container-query treatment, not another tweak to the single correction factor.
Content
ContentProvider (src/services/content.jsx) fetches a JSON content file and exposes it via useContent(), so exhibit content can be edited without touching code. Which file it fetches depends on the current route: each entry in PRESENTATION_MODES (src/services/presentationModes.js) names the contentFile its route loads, so every presentation app has its own content file instead of one shared one (rad-enablement -> /data/rad-content.json, value-advisory-services -> /data/vas-content.json; value-partnership has none yet). Already-fetched files are cached in memory, so switching between apps doesn't refetch. The contentFile config value (URL param, no default) overrides the route-based choice, e.g. to test one file against any route.
Fetch failures are surfaced instead of silently blanking the kiosk: useContent() exposes an error, the pages render a visible message, and the provider keeps retrying every 10 s until it succeeds. Render errors anywhere below the routes are caught by an ErrorBoundary (src/components/ErrorBoundary.jsx) that shows a message and reloads the app after 15 s.
Content file structure
A content file has four top-level keys: header (persistent page header), screensavers, stories and views. A view (one slide) renders an array of typed content blocks; a story declares its slide order explicitly:
{
"stories": {
"cancer": {
"name": "Cancer Screening",
"header": { "icon": "/assets/images/..." },
"slides": ["cancer-1", "cancer-2", "cancer-3"]
}
},
"views": {
"cancer-2": {
"story": "cancer",
"thumbnail": "/assets/thumbs/...",
"elements": [
{ "type": "headline", "text": "What are your challenges?" },
{ "type": "buttonList", "buttons": [{ "id": "cancer-2-1", "label": "..." }] }
]
},
"cancer-2-1": {
"story": "cancer",
"parent": "cancer-2",
"elements": [ ... ]
}
}
}- Blocks render in array order; a block can repeat.
typeselects the element (see "Adding a new element" below); the optionalregionfield ("storyTitle" | "headline" | "content") overrides the element's default layout region. Headlines take an optionallevel(2 = slide headline, 3 = subheadline). - Navigation is derived, never authored: prev/next on root slides follow the story's
slidesarray, a sub view (parentfield) goes back to its parent and forward to the slide after it, and the first/last slide connect to the story hub. Standalone views outside stories ("rad-intro") chain vianextView. scripts/migrate-content.mjsconverts files from the legacy format (elements object, id-prefix conventions,nextViewchains) to this one.
Validation: npm run validate:content checks every content file against a JSON schema (skeleton in schemas/content.schema.json, per-element block schemas contributed by src/components/elements/*/schema.js) and verifies all view references (slides, story, parent, button/option targets) - both fail the run. Once public/assets is synced, it additionally warns (doesn't fail) about referenced asset files that don't exist yet - develop is a preview environment, so content still being produced in Drive must not block a deploy; a missing video/image just degrades visibly at runtime instead (stuck "Loading video...", broken image). CI runs the full check on every branch pipeline, and the develop image build re-runs the asset check right after the Google Drive sync.
Adding a new element
Each element folder under src/components/elements/ owns its whole contract, aggregated in elements/registry.js:
schema.js- blocktype, default layoutregionand the JSON-schemaproperties/requiredof the block (pure data, also used by the content validation)<Name>.jsx+<Name>.scss- the componentindex.js- the registry definition: schema +component+adapter(maps a raw content block to the component's props)
Register the definition in ELEMENT_REGISTRY (registry.js) and its schema in ELEMENT_SCHEMAS (schemas.js - node-importable, so it can't live in the registry), and the renderer, layout and validation all pick the new element up.
Each content file also carries its own screensavers entry (see above), keyed by that mode's own PRESENTATION_MODES key:
{
"screensavers": {
"rad-enablement": "/assets/videos/ra_screensaver.mp4"
}
}Asset paths in content files are authored root-absolute (/assets/...) and resolved against Vite's base once at load time (src/utils/resolveAssetPath.js), so subpath deployments (e.g. the test server) work without touching the JSONs.
Assets & Offline
Media assets live in Google Drive and are mirrored into public/assets (gitignored): locally via the Docker-based sync service in sync/ (one-shot or continuous watch mode), and on the dev server via the same image running as the sync container in compose.develop.yml (see "Deployment" below). Either way, public/assets/the mounted assets/ directory is never baked into a Docker image (.dockerignore) - the develop deployment serves it from a volume so media can change without a rebuild; production images (main) ship with no bundled media at all today.
Runtime images are served as right-sized WebP. scripts/optimize-images.mjs (run with node scripts/optimize-images.mjs, uses sharp) converts new PNG slide thumbnails (to 270px WebP) and the large title backgrounds (max 2560px WebP), and moves the PNG masters to asset-src/ (gitignored, not served - keep them, public/assets is gitignored too, so the masters exist nowhere else). Content JSON references must point at the .webp files.
Offline support: the service worker (src/service-worker/service-worker.js, configured in vite.config.js via injectManifest.globPatterns) precaches the bundle, fonts and content JSONs at install time, so a kiosk keeps working through network loss for everything except media it hasn't fetched yet. Since assets aren't present at build time on develop, none of them end up in that precache manifest either way - instead, a StaleWhileRevalidate runtime route (matching by file extension, with ETag/Last-Modified conditional requests) fetches and caches each asset the first time it's requested, then keeps it fresh against the sync container's live copy. Note the size cap (maximumFileSizeToCacheInBytes) is raised to cover the ~9.4 MB screensaver videos, relevant wherever assets are precached (e.g. a production build with bundled media).
Kiosk hardening: pinch/double-tap zoom, text selection, long-press callouts, image dragging and overscroll are disabled globally (index.html viewport meta + src/styles/index.scss); the context menu is suppressed in production (App.jsx).
External Tabs / Windows
TabProvider/useTabs (src/services/tabs.jsx) opens external links in a separate tab/window, keyed by URL - opening the same URL twice focuses the existing tab instead of spawning a new one, and closeTab closes it again. Useful for links that should leave the kiosk view without navigating the app itself away from its route. (Currently has no consumer in the UI - kept for upcoming features, same as the hold-to-reload helper src/components/Reload.jsx.)
Tracking
src/services/analytics/analytics.js initializes a singleton AnalyticsSDK (vendored in AnalyticsSDK.js from the shared messe-tracking client) that implicitly captures clicks/touches/keyups and route changes (via the History API), plus an explicit useAnalytics() API (trackEvent, trackPageView, trackHotspot, trackSlideOrStory) for UI states that don't trigger navigation. Events are queued in IndexedDB and batch-POSTed to an AWS backend. Configure via .env:
VITE_ANALYTICS_EXPONAT_ID- exponat/booth id sent with every event; leave empty to disable tracking entirelyVITE_ANALYTICS_API_KEY- AWS API key (x-api-key) for the analytics backend; leave empty to disable tracking entirelyVITE_ANALYTICS_ENDPOINT- full HTTPS endpoint URL the SDK POSTs event batches to
GeneralPageTracking (src/services/tracking/GeneralPageTracking.jsx) keeps document.title in sync with the current route (e.g. Rad Enablement - cancer-2); page-view tracking itself no longer needs an explicit call there, since the SDK's History API listener picks up every route change automatically.
Cacheable Video
<CacheableVideo> (src/components/CacheableVideo.jsx) is a drop-in replacement for <video src="...">. A plain <video> tag triggers HTTP range requests, which the service worker's Cache API can't store - this component fetches the file as a full response instead and hands the video element a local blob: URL, so it becomes cacheable/offline-capable.
Documentation (JSDoc)
Code is documented with standard JSDoc comments. Generate a browsable site:
npm run docs
npm run docs:serve # Pagefind (search) needs to be served over HTTP, not opened as a fileOutput goes to docs/ (gitignored, regenerate as needed). Add /** ... */ doc comments above exported functions/hooks to have them picked up - a plain /* ... */ comment is treated as file-level prose and ignored by the generator.
Always-on docs site: every push to develop also builds and deploys the current docs as a static site to https://<project-name>-docs.dev.clck.de (this project: https://digital-exhibit-2026-docs.dev.clck.de) - a standing bookmark that's always up to date with develop, independent of the app deploy. Pipeline: build_docs_develop (runs npm run docs, uploads docs/ as an artifact) -> build_docker_docs_develop (packages it via Dockerfile.docs, a minimal nginx image, into <registry>:docs-dev-<sha> / <registry>:docs-develop) -> deploy_develop starts it as the docs service in compose.develop.yml, routed by Traefik under its own subdomain (DOCS_HOST, override via the DOCS_HOST_OVERRIDE CI/CD variable) so it keeps working even if the app container is redeploying.
Testing
npm run test # headless
npm run test:ui # interactive UI modePlaywright (playwright.config.js) starts the Vite dev server itself (webServer, reused if one is already running) and runs against Chromium, Firefox and WebKit. tests/smoke.spec.js covers the app selection, a content-driven app's intro/navigation flow, story/sub-view branching and the unknown-view-id redirect - deliberately small, since content-authoring mistakes are caught much earlier by npm run validate:content (see "Content" above).
Linting
npm run lint # eslint + stylelint
npm run lint:fix # same, with autofixDeployment
Deployment to Develop Environment (dev.clck.de)
Every push to the develop branch deploys a full Docker-based preview to https://<project-name>.dev.clck.de (this project: https://digital-exhibit-2026.dev.clck.de).
How it works:
- Content validation -
sync_assets_developruns a one-shot rclone sync that mirrors the Google Drive export folder intopublic/assets(same mechanism as the local sync service), then runsvalidate-contentagainst it. This is for CI validation only:public/assetsis excluded from the image (.dockerignore) and never baked in, on develop or elsewhere - see step 3. - Docker build & push - the app image is built like the production one (
Dockerfile), and a smallsyncimage (sync/Dockerfile.sync) is built alongside it. Both are pushed to the GitLab registry (dev-<short-sha>/developandsync-dev-<short-sha>/sync-develop). - Deploy - the pipeline copies
compose.develop.ymlplus a generated.env(includingGDRIVE_TOKEN/GDRIVE_FOLDER_NAME) to the dev server via SSH and runsdocker compose up -d. That starts thesynccontainer alongsideapp: it continuously mirrors Google Drive into a host directory (./assets, every 10 minutes by default), whichappmounts read-only per subfolder (assets/images,assets/thumbs,assets/videos,assets/icons- not the wholeassets/dir, since Vite's own hashed JS/CSS/font chunks also live directly underdist/assets/). The server runs a Traefik reverse proxy with a wildcard DNS entry*.dev.clck.de: each project gets its own subdomain and an automatic Let's Encrypt certificate - no server configuration needed per project.
Updating content without a code change:
New or changed media in the Drive folder shows up on its own, no pipeline run needed - the sync container picks it up on its next interval (default 10 min, override via the SYNC_INTERVAL_SECONDS CI/CD variable). Content JSON changes still need a commit/push, since those are bundled into the image (see "Assets & Offline" above for how the service worker picks up both without a client-side rebuild).
Required CI/CD variables (Settings → CI/CD → Variables):
| Variable | Type | Description |
|---|---|---|
APPLICATION_DEV_SERVER | Variable | Hostname/IP of the dev server |
APPLICATION_DEV_USER | Variable | SSH user (optional, defaults to ubuntu) |
APPLICATION_DEV_SSH_PRIVATE_KEY | Variable | Private SSH key for the deploy user (unmasked - multiline; raw, base64 and File type all work) |
DOT_ENV_DEVELOP_FILE | File | Content of the .env used for the dev build |
DEV_HOST_OVERRIDE | Variable | Optional: full hostname if it should differ from <project-name>.dev.clck.de |
DOCS_HOST_OVERRIDE | Variable | Optional: full hostname for the docs site if it should differ from <project-name>-docs.dev.clck.de |
GDRIVE_TOKEN | Variable | rclone OAuth token - the full JSON object ({"access_token":...}), without surrounding quotes |
GDRIVE_FOLDER_NAME | Variable | Source folder path in Google Drive |
GDRIVE_TEAM_DRIVE_ID | Variable | Optional: Shared Drive ID |
Additionally a Deploy token named exactly gitlab-deploy-token with read_registry scope must exist (Settings → Repository → Deploy tokens) - the dev server uses it to pull images from the registry.
On the server (one-time setup): Docker + Compose, the external network proxy (docker network create proxy) and the Traefik stack. Each project lives in ~/apps/<project-name>/ (created by the pipeline); Traefik picks new containers up automatically via Docker labels defined in compose.develop.yml.
Deployment to Test Environment
Development builds are automatically deployed to a test environment upon merging a Merge Request into the test branch.
Each successful deployment creates a unique release accessible via a dedicated URL.
Accessing Your Development Build:
You can find your build at: https://test.click-solutions.com/clck/project-browser/ or https://test.click-solutions.com/<gitlab-group>/<project-name>/<pipeline-id>_<short-commit-sha>
<gitlab-group>/<project-name>: This path is derived from your project's path in GitLab.<pipeline-id>_<short-commit-sha>: This forms the unique release name, combining the pipeline ID and the short commit hash.- The pipeline itself prints out the url of your deployment, too
Customizing the Deployment Path:
You can override the default <gitlab-group>/<project-name> path by setting the CI/CD variable DEPLOY_PATH_OVERRIDE in your project's settings (e.g., siag/hpresenter_supertest). The URL will then use this custom path.
Handling Environment Variables:
If your application uses environment variables (e.g., via a .env file), define them as a CI/CD variable named DOT_ENV_TEST_FILE. This variable must be configured as a 'File' type, and its content should be the content of your .env file. The variable VITE_BASE_URL will be overwritten by the pipeline.
Release Management:
- The release name is automatically generated as
release_<pipeline-id>_<short-commit-sha>. - The test environment retains the last three successful builds.
- You can manually trigger a pipeline for any commit to redeploy a specific version.
Ready-to-use Docker images
Development builds automatically build a Docker image pushed to the GitLab registry when merged to main.
- The image name is automatically generated as
gitlab.clacks.de:5050/<group>/<repo-name>:<COMMIT_SHORT_SHA>. - The release image name is automatically generated as
gitlab.clacks.de:5050/<group>/<repo-name>:latest, e.g.gitlab.clacks.de:5050/siag/react-boilerplate-siag-exhibitions:latest - You can manually trigger a pipeline for any commit to redeploy a specific version.
Using images on-site:
- Define environment variables for local spin-up in
.env - Check
compose.ymland change the image in theappservice section - Login via
docker login gitlab.clacks.de:5050 -u gitlab-ext(find the "Access token" field in Keeper by searching gitlab-ext) - Run
docker-compose up -d appto start the app service, served via nginx on the configured port