Skip to content

Commit 17f3060

Browse files
luthesJade3375
authored andcommitted
feat: Adds a Docker Build for the Web Client (#697)
* feat: add runtime env injection script and server package Signed-off-by: Steven Luther <steven@lutherlabs.com> * fix: remove revolt env vars Signed-off-by: Steven Luther <steven@lutherlabs.com> * ci: add GitHub Actions workflow for Docker image build and push Signed-off-by: Steven Luther <steven@lutherlabs.com> * improvement: rewrite to use native node packages Signed-off-by: Steven Luther <steven@lutherlabs.com> * improvement: bump Docker base image from Node 22 to Node 24 LTS Signed-off-by: Steven Luther <steven@lutherlabs.com> --------- Signed-off-by: Steven Luther <steven@lutherlabs.com> Signed-off-by: Jade3375 <floodlockgames9@gmail.com>
1 parent fd77405 commit 17f3060

File tree

5 files changed

+214
-0
lines changed

5 files changed

+214
-0
lines changed

.dockerignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
node_modules
2+
.git
3+
*.md
4+
!README.md
5+
packages/client/dist
6+
packages/client/dist_injected
7+
packages/client/playwright-report
8+
packages/client/test-results
9+
.mise
10+
.vscode
11+
.github
12+
docs

.github/workflows/docker-build.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Docker Build & Push
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- main
8+
tags:
9+
- "v*"
10+
pull_request:
11+
branches:
12+
- master
13+
- main
14+
15+
env:
16+
REGISTRY: ghcr.io
17+
IMAGE_NAME: ${{ github.repository }}
18+
19+
jobs:
20+
build:
21+
name: Build Docker Image
22+
runs-on: ubuntu-latest
23+
24+
permissions:
25+
contents: read
26+
packages: write
27+
28+
steps:
29+
- name: Checkout with submodules
30+
uses: actions/checkout@v4
31+
with:
32+
submodules: recursive
33+
34+
- name: Set up Docker Buildx
35+
uses: docker/setup-buildx-action@v3
36+
37+
- name: Log in to GHCR
38+
if: github.event_name != 'pull_request'
39+
uses: docker/login-action@v3
40+
with:
41+
registry: ${{ env.REGISTRY }}
42+
username: ${{ github.actor }}
43+
password: ${{ secrets.GITHUB_TOKEN }}
44+
45+
- name: Extract metadata
46+
id: meta
47+
uses: docker/metadata-action@v5
48+
with:
49+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
50+
tags: |
51+
type=ref,event=branch
52+
type=semver,pattern={{version}}
53+
type=semver,pattern={{major}}.{{minor}}
54+
type=sha,prefix=
55+
56+
- name: Build and push
57+
uses: docker/build-push-action@v6
58+
with:
59+
context: .
60+
push: ${{ github.event_name != 'pull_request' }}
61+
tags: ${{ steps.meta.outputs.tags }}
62+
labels: ${{ steps.meta.outputs.labels }}
63+
cache-from: type=gha
64+
cache-to: type=gha,mode=max

Dockerfile

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# ============================================
2+
# Stage 1: Build the web client
3+
# ============================================
4+
FROM node:24-alpine AS builder
5+
6+
RUN apk add --no-cache git python3 make g++
7+
8+
# Install pnpm
9+
RUN corepack enable && corepack prepare pnpm@10.28.1 --activate
10+
11+
WORKDIR /build
12+
13+
# Copy workspace config files for dependency resolution
14+
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./
15+
16+
# Copy all package.json files for workspace packages
17+
COPY packages/stoat.js/package.json packages/stoat.js/
18+
COPY packages/solid-livekit-components/package.json packages/solid-livekit-components/
19+
COPY packages/js-lingui-solid/packages/babel-plugin-lingui-macro/package.json packages/js-lingui-solid/packages/babel-plugin-lingui-macro/
20+
COPY packages/js-lingui-solid/packages/babel-plugin-extract-messages/package.json packages/js-lingui-solid/packages/babel-plugin-extract-messages/
21+
COPY packages/client/package.json packages/client/
22+
23+
# Copy panda config needed by client's "prepare" lifecycle script (panda codegen)
24+
COPY packages/client/panda.config.ts packages/client/
25+
26+
# Install dependencies
27+
RUN pnpm install --frozen-lockfile
28+
29+
# Submodules:
30+
# In CI: actions/checkout@v4 with submodules: recursive handles this automatically.
31+
# Locally: run `git submodule update --init --recursive` before `docker build`.
32+
COPY packages/ packages/
33+
34+
# Build sub-dependencies (stoat.js, livekit-components, lingui plugins, panda css etc)
35+
RUN pnpm --filter stoat.js build && \
36+
pnpm --filter solid-livekit-components build && \
37+
pnpm --filter @lingui-solid/babel-plugin-lingui-macro build && \
38+
pnpm --filter @lingui-solid/babel-plugin-extract-messages build && \
39+
pnpm --filter client exec lingui compile --typescript && \
40+
pnpm --filter client exec node scripts/copyAssets.mjs && \
41+
pnpm --filter client exec panda codegen
42+
43+
# Build the client with placeholder env vars for runtime injection
44+
# these are replaced by inject.js at container run startup
45+
ENV VITE_API_URL=__VITE_API_URL__
46+
ENV VITE_WS_URL=__VITE_WS_URL__
47+
ENV VITE_MEDIA_URL=__VITE_MEDIA_URL__
48+
ENV VITE_PROXY_URL=__VITE_PROXY_URL__
49+
ENV VITE_HCAPTCHA_SITEKEY=__VITE_HCAPTCHA_SITEKEY__
50+
ENV BASE_PATH=/
51+
52+
RUN pnpm --filter client exec vite build
53+
54+
# ============================================
55+
# Stage 2: Minimal runtime image
56+
# ============================================
57+
FROM node:24-alpine
58+
59+
WORKDIR /app
60+
61+
# Copy the server package and install dependencies
62+
COPY docker/package.json docker/inject.js ./
63+
RUN npm install --omit=dev
64+
65+
# Copy built static assets stage 1
66+
COPY --from=builder /build/packages/client/dist ./dist
67+
68+
EXPOSE 5000
69+
70+
# Runtime env vars (overridden by Helm chart / docker run)
71+
ENV VITE_API_URL=""
72+
ENV VITE_WS_URL=""
73+
ENV VITE_MEDIA_URL=""
74+
ENV VITE_PROXY_URL=""
75+
ENV VITE_HCAPTCHA_SITEKEY=""
76+
ENV REVOLT_PUBLIC_URL=""
77+
78+
CMD ["npm", "start"]

docker/inject.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const { cpSync, rmSync, readFileSync, writeFileSync, readdirSync } = require("node:fs");
2+
const { join } = require("node:path");
3+
4+
const BUILD_DIR = "dist";
5+
const OUT_DIR = "dist_injected";
6+
7+
// Map of placeholder to env var name
8+
// At build time, Vite replaces import.meta.env.VITE_X with the literal string value.
9+
// We build with placeholder values like "__VITE_API_URL__" so they appear in the output.
10+
const REPLACEMENTS = {
11+
__VITE_API_URL__: process.env.VITE_API_URL || "",
12+
__VITE_WS_URL__: process.env.VITE_WS_URL || "",
13+
__VITE_MEDIA_URL__: process.env.VITE_MEDIA_URL || "",
14+
__VITE_PROXY_URL__: process.env.VITE_PROXY_URL || "",
15+
__VITE_HCAPTCHA_SITEKEY__: process.env.VITE_HCAPTCHA_SITEKEY || "",
16+
};
17+
18+
console.log("Preparing injected build...");
19+
20+
rmSync(OUT_DIR, { recursive: true, force: true });
21+
cpSync(BUILD_DIR, OUT_DIR, { recursive: true });
22+
23+
console.log("Injecting environment variables...");
24+
const files = readdirSync(OUT_DIR, { recursive: true });
25+
26+
for (const file of files) {
27+
const path = join(OUT_DIR, file);
28+
if (!path.endsWith(".js") && !path.endsWith(".html")) continue;
29+
30+
let data = readFileSync(path, "utf-8");
31+
let modified = false;
32+
33+
for (const [placeholder, value] of Object.entries(REPLACEMENTS)) {
34+
if (data.includes(placeholder)) {
35+
data = data.replaceAll(placeholder, value);
36+
modified = true;
37+
}
38+
}
39+
40+
if (modified) {
41+
console.log("Injected:", path);
42+
writeFileSync(path, data);
43+
}
44+
}
45+
46+
console.log("Injection complete.");

docker/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "stoat-web-server",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"start": "node inject.js && sirv dist_injected --port 5000 --cors --single --host"
7+
},
8+
"dependencies": {
9+
"sirv-cli": "^3.0.1"
10+
},
11+
"engines": {
12+
"node": ">=18"
13+
}
14+
}

0 commit comments

Comments
 (0)