docker & prod building 🎉

This commit is contained in:
Grant 2024-03-11 20:59:47 -06:00
parent 613b75edb6
commit 004e4926c4
19 changed files with 303 additions and 261 deletions

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# Canvas
## Running via Docker Compose
1. Run `npm run build:all`
2. Run `npm run build:docker`
3. Run `docker compose run --rm canvas npx prisma migrate deploy`
4. (optional) Load default palette colors
Run `docker compose run --rm canvas npm run -w packages/server prisma:seed:palette`
5. Run `docker compose up -d`

34
docker-compose.yml Normal file
View File

@ -0,0 +1,34 @@
# this docker-compose does not include a build for the Canvas image
# generate the image via a build script
name: canvas
services:
canvas:
image: sc07/canvas
ports:
- "3000:3000"
environment:
- SESSION_SECRET=CHANGE ME TO RANDOM VALUE
- REDIS_HOST=redis://redis
- DATABASE_URL=postgres://postgres@postgres/canvas
depends_on:
- redis
- postgres
redis:
restart: always
image: redis:7-alpine
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
volumes:
- ./data/redis:/data
postgres:
restart: always
image: postgres:14-alpine
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
- 'POSTGRES_HOST_AUTH_METHOD=trust'

273
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,13 @@
"scripts": { "scripts": {
"dev:client": "npm run dev -w packages/client", "dev:client": "npm run dev -w packages/client",
"dev:server": "npm run dev -w packages/server", "dev:server": "npm run dev -w packages/server",
"prisma:studio": "npm run prisma:studio -w packages/server" "prisma:studio": "npm run prisma:studio -w packages/server",
"build:all": "./packages/build/build-all.sh",
"build:docker": "./packages/build/docker-build.sh",
"build:lib": "npm run build -w packages/lib",
"build:client": "npm run build -w packages/client",
"build:admin": "npm run build -w packages/admin",
"build:server": "npm run build -w packages/server"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

2
packages/admin/.env Normal file
View File

@ -0,0 +1,2 @@
# expose APP_ROOT to client, for routing
VITE_APP_ROOT=$APP_ROOT

View File

@ -8,7 +8,8 @@ import { Root } from "./Root.tsx";
import { HomePage } from "./pages/Home/page.tsx"; import { HomePage } from "./pages/Home/page.tsx";
import { AccountsPage } from "./pages/Accounts/Accounts/page.tsx"; import { AccountsPage } from "./pages/Accounts/Accounts/page.tsx";
const router = createBrowserRouter([ const router = createBrowserRouter(
[
{ {
path: "/", path: "/",
element: <Root />, element: <Root />,
@ -23,7 +24,11 @@ const router = createBrowserRouter([
}, },
], ],
}, },
]); ],
{
basename: import.meta.env.VITE_APP_ROOT,
}
);
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>

View File

@ -5,8 +5,10 @@ import react from "@vitejs/plugin-react";
export default defineConfig({ export default defineConfig({
root: "src", root: "src",
envDir: "..", envDir: "..",
base: process.env.APP_ROOT,
build: { build: {
outDir: "../dist", outDir: "../dist",
emptyOutDir: true,
}, },
plugins: [ plugins: [
react({ react({

18
packages/build/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# this file needs to be copied to /build
FROM node:20-alpine
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
COPY --chown=node:node . /home/node/app/
USER node
RUN npm install --omit=dev
RUN npx prisma generate
ENV PORT 3000
ENV NODE_ENV production
ENV SERVE_CLIENT /home/node/app/packages/client
ENV SERVE_ADMIN /home/node/app/packages/admin
EXPOSE 3000
CMD [ "npm", "-w", "packages/server", "start" ]

68
packages/build/build-all.sh Executable file
View File

@ -0,0 +1,68 @@
#!/bin/bash
# builds client, admin & server into one folder
# - client is mounted at /
# - admin is mounted at /admin
# ensure we are in packages/build
MY_DIR="$(cd "$(dirname "$0")"; pwd)"
OUT_DIR="$(cd "$MY_DIR/../../build"; pwd)"
cd $MY_DIR
# empty out directory
rm -rf $OUT_DIR/*
mkdir -p $OUT_DIR/packages/lib
mkdir -p $OUT_DIR/packages/client
mkdir -p $OUT_DIR/packages/admin
mkdir -p $OUT_DIR/packages/server
mkdir -p $OUT_DIR/prisma
cp $MY_DIR/../../package.json $MY_DIR/../../package-lock.json $OUT_DIR/
LIB_DIR="$MY_DIR/../../packages/lib"
CLIENT_DIR="$MY_DIR/../../packages/client"
ADMIN_DIR="$MY_DIR/../../packages/admin"
SERVER_DIR="$MY_DIR/../../packages/server"
PRISMA_DIR="$SERVER_DIR/prisma"
cp -r $PRISMA_DIR/schema.prisma $PRISMA_DIR/migrations $OUT_DIR/prisma/
# --- Shared Library ---
echo "Building lib..."
cd "$MY_DIR/../.." && npm run-script build:lib
cd $LIB_DIR
mv dist $OUT_DIR/packages/lib
cp package.json $OUT_DIR/packages/lib/
# janky? fix to keep imports in dev
sed -i -e 's/"main": ".*"/"main": ".\/dist\/index.js"/' $OUT_DIR/packages/lib/package.json
# --- Main Client ---
echo "Building client..."
cd "$MY_DIR/../.." && npm run-script build:client
cd $CLIENT_DIR
mv dist/* $OUT_DIR/packages/client
rm -r dist # this dir is empty, delete it to prevent confusion
# --- Admin Client ---
echo "Building admin..."
cd "$MY_DIR/../../" && APP_ROOT=/admin npm run-script build:admin
cd $ADMIN_DIR
mv dist/* $OUT_DIR/packages/admin
rm -r dist # this dir is empty, delete it to prevent confusion
# --- Server ---
echo "Building server..."
cd "$MY_DIR/../../" && npm run-script build:server
cd $SERVER_DIR
mv dist $OUT_DIR/packages/server
cp package.json tool.sh $OUT_DIR/packages/server
# rm -r dist # this dir is empty, delete it to prevent confusion

10
packages/build/docker-build.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
MY_DIR="$(cd "$(dirname "$0")"; pwd)"
OUT_DIR="$(cd "$MY_DIR/../../build"; pwd)"
cd $MY_DIR
cp Dockerfile $OUT_DIR/
cd $OUT_DIR
docker build . -t sc07/canvas

View File

@ -6,6 +6,7 @@ export default defineConfig({
envDir: "..", envDir: "..",
build: { build: {
outDir: "../dist", outDir: "../dist",
emptyOutDir: true,
}, },
plugins: [ plugins: [
react({ react({

View File

@ -2,6 +2,9 @@
"name": "@sc07-canvas/lib", "name": "@sc07-canvas/lib",
"version": "1.0.0", "version": "1.0.0",
"main": "./src/index.ts", "main": "./src/index.ts",
"scripts": {
"build": "tsc"
},
"dependencies": { "dependencies": {
"eventemitter3": "^5.0.1" "eventemitter3": "^5.0.1"
} }

View File

@ -1,3 +1,4 @@
import * as net from "./net"; import * as net from "./net";
import { CanvasLib } from "./canvas";
export { net }; export { net, CanvasLib };

View File

@ -0,0 +1,10 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"inlineSourceMap": true,
"jsx": "react",
"declaration": true,
},
}

View File

@ -1,11 +1,14 @@
{ {
"name": "@sc07-canvas/server", "name": "@sc07-canvas/server",
"version": "1.0.0", "version": "1.0.0",
"main": "./build/index.js",
"scripts": { "scripts": {
"dev": "DOTENV_CONFIG_PATH=.env.local nodemon -r dotenv/config src/index.ts", "dev": "DOTENV_CONFIG_PATH=.env.local nodemon -r dotenv/config src/index.ts",
"start": "node dist/index.js",
"build": "tsc",
"lint": "eslint .", "lint": "eslint .",
"prisma:studio": "prisma studio" "prisma:studio": "prisma studio",
"prisma:migrate": "prisma migrate deploy",
"prisma:seed:palette": "./tool.sh seed_palette"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -21,7 +24,6 @@
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^3.0.1", "prettier": "^3.0.1",
"prisma": "^5.3.1", "prisma": "^5.3.1",
"prisma-dbml-generator": "^0.12.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.6" "typescript": "^5.1.6"
}, },
@ -31,6 +33,7 @@
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"prisma-dbml-generator": "^0.12.0",
"redis": "^4.6.12", "redis": "^4.6.12",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"winston": "^3.11.0" "winston": "^3.11.0"

View File

@ -1,4 +1,5 @@
import http from "node:http"; import http from "node:http";
import path from "node:path";
import express, { type Express } from "express"; import express, { type Express } from "express";
import expressSession from "express-session"; import expressSession from "express-session";
import RedisStore from "connect-redis"; import RedisStore from "connect-redis";
@ -28,6 +29,49 @@ export class ExpressServer {
this.app = express(); this.app = express();
this.httpServer = http.createServer(this.app); this.httpServer = http.createServer(this.app);
if (process.env.SERVE_CLIENT) {
// client is needing to serve
Logger.info(
"Serving client UI at / using root " +
path.join(__dirname, process.env.SERVE_CLIENT)
);
this.app.use(express.static(process.env.SERVE_CLIENT));
} else {
this.app.get("/", (req, res) => {
res.status(404).contentType("html").send(`
<html>
<head>
<title>Canvas Server</title>
</head>
<body>
<h1>Canvas Server</h1>
<p>This instance is not serving the client</p>
<i>This instance might not be configured correctly</i>
</body>
</html>
`);
});
}
if (process.env.SERVE_ADMIN) {
// client is needing to serve
Logger.info(
"Serving admin UI at /admin using root " +
path.join(__dirname, process.env.SERVE_ADMIN)
);
const assetsDir = path.join(__dirname, process.env.SERVE_ADMIN, "assets");
const indexFile = path.join(
__dirname,
process.env.SERVE_ADMIN,
"index.html"
);
this.app.use("/admin/assets", express.static(assetsDir));
this.app.use("/admin/*", (req, res) => {
res.sendFile(indexFile);
});
}
this.app.use(session); this.app.use(session);
this.app.use("/api", APIRoutes); this.app.use("/api", APIRoutes);

View File

@ -71,12 +71,20 @@ export class User {
return Date.now() - this._updatedAt >= 1000 * 60; return Date.now() - this._updatedAt >= 1000 * 60;
} }
static async fromAuthSession(auth: AuthSession): Promise<User> { static async fromAuthSession(auth: AuthSession): Promise<User | undefined> {
try {
const user = await this.fromSub( const user = await this.fromSub(
auth.user.username + "@" + auth.service.instance.hostname auth.user.username + "@" + auth.service.instance.hostname
); );
user.authSession = auth; user.authSession = auth;
return user; return user;
} catch (e) {
if (e instanceof UserNotFound) {
return undefined;
} else {
throw e;
}
}
} }
static async fromSub(sub: string): Promise<User> { static async fromSub(sub: string): Promise<User> {

View File

@ -31,6 +31,15 @@ declare global {
* Specifically setting CORS origin is required because of use of credentials (cookies) * Specifically setting CORS origin is required because of use of credentials (cookies)
*/ */
CLIENT_ORIGIN?: string; CLIENT_ORIGIN?: string;
/**
* If set, use this relative path to serve the client at the root
*/
SERVE_CLIENT?: string;
/**
* If set, use this relative path to serve the admin UI at /admin
*/
SERVE_ADMIN?: string;
} }
} }
} }

View File

@ -2,6 +2,7 @@
"extends": "@tsconfig/recommended/tsconfig.json", "extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"rootDir": "src", "rootDir": "src",
"outDir": "build" "outDir": "dist",
} "inlineSourceMap": true,
},
} }