mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-09-04 13:46:34 +08:00
chore(self hosting): run selfhosted backend in prod mode (@fehmer) (#6326)
Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
7d7118f744
commit
f80dde4c89
14 changed files with 116 additions and 75 deletions
2
.github/workflows/publish-docker-images.yml
vendored
2
.github/workflows/publish-docker-images.yml
vendored
|
@ -48,6 +48,8 @@ jobs:
|
|||
push: true
|
||||
tags: ${{ env.BE_REPO }}:latest,${{ steps.bemeta.outputs.tags }}
|
||||
labels: ${{ steps.bemeta.outputs.labels }}
|
||||
build-args: |
|
||||
server_version: {{version}}
|
||||
|
||||
- name: Backend publish description
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
const hasAnticheatImplemented = process.env["BYPASS_ANTICHEAT"] === "true";
|
||||
|
||||
import {
|
||||
CompletedEvent,
|
||||
KeyStats,
|
||||
} from "@monkeytype/contracts/schemas/results";
|
||||
import Logger from "../utils/logger";
|
||||
|
||||
export function implemented(): boolean {
|
||||
return false;
|
||||
if (hasAnticheatImplemented) {
|
||||
Logger.warning("BYPASS_ANTICHEAT is enabled! Running without anti-cheat.");
|
||||
}
|
||||
return hasAnticheatImplemented;
|
||||
}
|
||||
|
||||
export function validateResult(
|
||||
|
@ -13,6 +19,7 @@ export function validateResult(
|
|||
_uaStringifiedObject: string,
|
||||
_lbOptOut: boolean
|
||||
): boolean {
|
||||
Logger.warning("No anticheat module found, result will not be validated.");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -22,5 +29,6 @@ export function validateKeys(
|
|||
_keyDurationStats: KeyStats,
|
||||
_uid: string
|
||||
): boolean {
|
||||
Logger.warning("No anticheat module found, key data will not be validated.");
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -96,6 +96,7 @@ export const BASE_CONFIGURATION: Configuration = {
|
|||
xpRewardBrackets: [],
|
||||
},
|
||||
leaderboards: {
|
||||
minTimeTyping: 2 * 60 * 60,
|
||||
weeklyXp: {
|
||||
enabled: false,
|
||||
expirationTimeInDays: 0, // This should atleast be 15
|
||||
|
@ -548,6 +549,12 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema<Configuration> = {
|
|||
type: "object",
|
||||
label: "Leaderboards",
|
||||
fields: {
|
||||
minTimeTyping: {
|
||||
type: "number",
|
||||
label: "Minimum typing time the user needs to get on a leaderboard",
|
||||
hint: "Typing time in seconds. Change is only applied after restarting the server.",
|
||||
min: 0,
|
||||
},
|
||||
weeklyXp: {
|
||||
type: "object",
|
||||
label: "Weekly XP",
|
||||
|
|
|
@ -3,13 +3,16 @@ import Logger from "../utils/logger";
|
|||
import { performance } from "perf_hooks";
|
||||
import { setLeaderboard } from "../utils/prometheus";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { getCachedConfiguration } from "../init/configuration";
|
||||
import {
|
||||
getCachedConfiguration,
|
||||
getLiveConfiguration,
|
||||
} from "../init/configuration";
|
||||
|
||||
import { addLog } from "./logs";
|
||||
import { Collection, ObjectId } from "mongodb";
|
||||
import { LeaderboardEntry } from "@monkeytype/contracts/schemas/leaderboards";
|
||||
import { omit } from "lodash";
|
||||
import { DBUser } from "./user";
|
||||
import { DBUser, getUsersCollection } from "./user";
|
||||
import MonkeyError from "../utils/error";
|
||||
|
||||
export type DBLeaderboardEntry = LeaderboardEntry & {
|
||||
|
@ -269,7 +272,11 @@ export async function update(
|
|||
};
|
||||
}
|
||||
|
||||
async function createIndex(key: string): Promise<void> {
|
||||
async function createIndex(
|
||||
key: string,
|
||||
minTimeTyping: number,
|
||||
dropIfMismatch = true
|
||||
): Promise<void> {
|
||||
const index = {
|
||||
[`${key}.wpm`]: -1,
|
||||
[`${key}.acc`]: -1,
|
||||
|
@ -293,16 +300,41 @@ async function createIndex(key: string): Promise<void> {
|
|||
$gt: 0,
|
||||
},
|
||||
timeTyping: {
|
||||
$gt: isDevEnvironment() ? 0 : 7200,
|
||||
$gt: minTimeTyping,
|
||||
},
|
||||
},
|
||||
};
|
||||
await db.collection("users").createIndex(index, partial);
|
||||
try {
|
||||
await getUsersCollection().createIndex(index, partial);
|
||||
} catch (e) {
|
||||
if (!dropIfMismatch) throw e;
|
||||
if (
|
||||
(e as Error).message.startsWith(
|
||||
"An existing index has the same name as the requested index"
|
||||
)
|
||||
) {
|
||||
Logger.warning(`Index ${key} not matching, dropping and recreating...`);
|
||||
|
||||
const existingIndex = (await getUsersCollection().listIndexes().toArray())
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
.map((it) => it.name as string)
|
||||
.find((it) => it.startsWith(key));
|
||||
|
||||
if (existingIndex !== undefined && existingIndex !== null) {
|
||||
await getUsersCollection().dropIndex(existingIndex);
|
||||
return createIndex(key, minTimeTyping, false);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createIndicies(): Promise<void> {
|
||||
await createIndex("lbPersonalBests.time.15.english");
|
||||
await createIndex("lbPersonalBests.time.60.english");
|
||||
const minTimeTyping = (await getLiveConfiguration()).leaderboards
|
||||
.minTimeTyping;
|
||||
await createIndex("lbPersonalBests.time.15.english", minTimeTyping);
|
||||
await createIndex("lbPersonalBests.time.60.english", minTimeTyping);
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
Logger.info("Updating leaderboards in dev mode...");
|
||||
|
|
|
@ -6,10 +6,21 @@ import { identity } from "../utils/misc";
|
|||
import { BASE_CONFIGURATION } from "../constants/base-configuration";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { addLog } from "../dal/logs";
|
||||
import { PartialConfiguration } from "@monkeytype/contracts/configuration";
|
||||
import {
|
||||
PartialConfiguration,
|
||||
PartialConfigurationSchema,
|
||||
} from "@monkeytype/contracts/configuration";
|
||||
import { getErrorMessage } from "../utils/error";
|
||||
import { join } from "path";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
import { z } from "zod";
|
||||
|
||||
const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes
|
||||
const SERVER_CONFIG_FILE_PATH = join(
|
||||
__dirname,
|
||||
"../backend-configuration.json"
|
||||
);
|
||||
|
||||
function mergeConfigurations(
|
||||
baseConfiguration: Configuration,
|
||||
|
@ -138,3 +149,20 @@ export async function patchConfiguration(
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function updateFromConfigurationFile(): Promise<void> {
|
||||
if (existsSync(SERVER_CONFIG_FILE_PATH)) {
|
||||
Logger.info(
|
||||
`Reading server configuration from file ${SERVER_CONFIG_FILE_PATH}`
|
||||
);
|
||||
const json = readFileSync(SERVER_CONFIG_FILE_PATH, "utf-8");
|
||||
const data = parseJsonWithSchema(
|
||||
json,
|
||||
z.object({
|
||||
configuration: PartialConfigurationSchema,
|
||||
})
|
||||
);
|
||||
|
||||
await patchConfiguration(data.configuration);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,9 +44,12 @@ export async function init(): Promise<void> {
|
|||
Logger.warning(
|
||||
"No email client configuration provided. Running without email."
|
||||
);
|
||||
return;
|
||||
} else if (process.env["BYPASS_EMAILCLIENT"] === "true") {
|
||||
Logger.warning("BYPASS_EMAILCLIENT is enabled! Running without email.");
|
||||
} else {
|
||||
throw new Error("No email client configuration provided");
|
||||
}
|
||||
throw new Error("No email client configuration provided");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import "dotenv/config";
|
||||
import * as db from "./init/db";
|
||||
import jobs from "./jobs";
|
||||
import { getLiveConfiguration } from "./init/configuration";
|
||||
import {
|
||||
getLiveConfiguration,
|
||||
updateFromConfigurationFile,
|
||||
} from "./init/configuration";
|
||||
import app from "./app";
|
||||
import { Server } from "http";
|
||||
import { version } from "./version";
|
||||
|
@ -30,6 +33,7 @@ async function bootServer(port: number): Promise<Server> {
|
|||
Logger.info("Fetching live configuration...");
|
||||
await getLiveConfiguration();
|
||||
Logger.success("Live configuration fetched");
|
||||
await updateFromConfigurationFile();
|
||||
|
||||
Logger.info("Initializing email client...");
|
||||
await EmailClient.init();
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
},
|
||||
"dailyLeaderboards": {
|
||||
"enabled": false
|
||||
},
|
||||
"leaderboards":{
|
||||
"minTimeTyping": 0
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,9 +20,8 @@ RUN pnpm deploy --filter backend --prod /prod/backend
|
|||
## target image
|
||||
FROM node:20.16.0-alpine3.19
|
||||
|
||||
##install wget, used by the applyConfig script
|
||||
RUN apk update --no-cache && \
|
||||
apk add --no-cache wget
|
||||
## get server_version from build-arg, default to UNKNOWN
|
||||
ARG server_version=UNKNOWN
|
||||
|
||||
# COPY to target
|
||||
COPY --from=builder /prod/backend/node_modules /app/backend/node_modules
|
||||
|
@ -37,10 +36,14 @@ WORKDIR /app/backend/dist
|
|||
## logs
|
||||
RUN mkdir -p /app/backend/dist/logs
|
||||
|
||||
COPY ["docker/backend/entry-point.sh", "docker/backend/applyConfig.sh", "./"]
|
||||
COPY ["docker/backend/entry-point.sh", "./"]
|
||||
|
||||
#run in dev mode (no anticheat)
|
||||
ENV MODE=dev
|
||||
RUN echo "${server_version}" > /app/backend/dist/server.version
|
||||
|
||||
#run in prod mode, but don't require anti-cheat or email client
|
||||
ENV MODE=prod
|
||||
ENV BYPASS_ANTICHEAT=true
|
||||
ENV BYPASS_EMAILCLIENT=true
|
||||
|
||||
EXPOSE 5005
|
||||
USER node
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/sh
|
||||
if [ -f backend-configuration.json ]; then
|
||||
echo "waiting for backend..."
|
||||
|
||||
timeout 30 sh -c 'until nc -z $0 $1; do sleep 1; done' localhost 5005
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed to apply config"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "apply server config"
|
||||
|
||||
wget -qO- --method=PATCH \
|
||||
--body-data="`cat backend-configuration.json`" \
|
||||
--header='Content-Type:application/json' \
|
||||
http://localhost:5005/configuration
|
||||
|
||||
echo "server config applied"
|
||||
else
|
||||
echo "skip backend configuration"
|
||||
fi
|
|
@ -1,3 +1,2 @@
|
|||
#!/bin/sh
|
||||
./applyConfig.sh &
|
||||
node server.js
|
|
@ -162,39 +162,7 @@ Contains your firebase config, only needed if you want to allow users to signup.
|
|||
|
||||
### backend-configuration.json
|
||||
|
||||
Configuration of the backend.
|
||||
|
||||
If you don't want to update this file manually you can
|
||||
|
||||
- open the backend url in your browser, e.g. `http://localhost:5005/configure/`
|
||||
- adjust the settings and click `Save Changes`
|
||||
- click `Export Configuration`
|
||||
- save the file as `backend-configuration.json`, overwriting the existing one.
|
||||
|
||||
Example output from `http://localhost:5005/configuration`:
|
||||
```json
|
||||
{
|
||||
"message": "Configuration retrieved",
|
||||
"data":
|
||||
{
|
||||
"maintenance": false,
|
||||
"results": {},
|
||||
....
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example content from `backend-configuration.json`:
|
||||
```
|
||||
{
|
||||
"maintenance": false,
|
||||
"results": {},
|
||||
....
|
||||
}
|
||||
```
|
||||
|
||||
If you have `curl` and `jq` installed you can also run `curl -wO- http://localhost:5005/configuration | jq ".data" > backend-configuration.json` to update the configuration file.
|
||||
|
||||
Configuration of the backend. Check the [default configuration](https://github.com/monkeytypegame/monkeytype/blob/master/backend/src/constants/base-configuration.ts#L8) for possible values.
|
||||
|
||||
> [!NOTE]
|
||||
> The configuration is applied on container startup only. You have to restart the container for your changes to become active.
|
||||
|
|
|
@ -13,7 +13,7 @@ export const limits = {
|
|||
},
|
||||
|
||||
adminLimit: {
|
||||
window: 5000, //5 seconds
|
||||
window: 5000,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
|
|
|
@ -111,6 +111,12 @@ export const ConfigurationSchema = z.object({
|
|||
xpRewardBrackets: z.array(RewardBracketSchema),
|
||||
}),
|
||||
leaderboards: z.object({
|
||||
minTimeTyping: z
|
||||
.number()
|
||||
.min(0)
|
||||
.describe(
|
||||
"Minimum typing time (in seconds) the user needs to get on a leaderboard"
|
||||
),
|
||||
weeklyXp: z.object({
|
||||
enabled: z.boolean(),
|
||||
expirationTimeInDays: z.number().nonnegative(),
|
||||
|
|
Loading…
Add table
Reference in a new issue