impr: add selfhosting using docker only (fehmer) (#5170)

* impr: add selfhosting using docker only

* add recaptcha config and docs

* add documentation on the backend-configuration.json file, remove ---redacted--- from example config

---------

Co-authored-by: Jack <jack@monkeytype.com>
This commit is contained in:
Christian Fehmer 2024-03-12 12:02:13 +01:00 committed by GitHub
parent b31dba6b7a
commit 9432602727
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 582 additions and 5 deletions

View file

@ -1,3 +1,23 @@
node_modules
frontend/node_modules
backend/node_modules
backend/node_modules
# Firebase
.firebase/
.firebaserc
serviceAccountKey*.json
frontend/src/ts/constants/firebase-config.ts
frontend/src/ts/constants/firebase-config-live.ts
#frontend
frontend/.env
#cloudflare
.cloudflareKey.txt
.cloudflareKey_copy.txt
#backend
backend/src/credentials/*.json
backend/.env
backend/build

View file

@ -1 +1,2 @@
backend/build
backend/build
docker

View file

@ -0,0 +1,76 @@
name: Publish Docker image
on:
release:
types: [published]
workflow_dispatch:
jobs:
push_to_registry:
env:
BE_REPO: monkeytype/monkeytype-backend
FE_REPO: monkeytype/monkeytype-frontend
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Backend extract metadata (tags, labels)
id: bemeta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81
with:
images: ${{ env.BE_REPO }}
tags: |
type=semver,pattern={{version}}
- name: Backend build and push Docker image
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
file: ./docker/backend/Dockerfile
push: true
tags: ${{ env.BE_REPO }}:latest,${{ steps.bemeta.outputs.tags }}
labels: ${{ steps.bemeta.outputs.labels }}
- name: Backend publish description
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
repository: ${{ env.BE_REPO }}
short-description: Backend server for monkeytype.com
readme-filepath: ./SELF_HOSTING.md
- name: Frontend extract metadata (tags, labels)
id: femeta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81
with:
images: ${{ env.FE_REPO }}
tags: |
type=semver,pattern={{version}}
- name: Frontend build and push Docker image
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
file: ./docker/frontend/Dockerfile
push: true
tags: ${{ env.FE_REPO }}:latest,${{ steps.femeta.outputs.tags }}
labels: ${{ steps.femeta.outputs.labels }}
- name: Frontend publish description
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
repository: ${{ env.FE_REPO }}
short-description: Frontend server for monkeytype.com
readme-filepath: ./SELF_HOSTING.md

1
.gitignore vendored
View file

@ -82,6 +82,7 @@ node_modules_bak/
.firebaserc
.firebaserc_copy
serviceAccountKey*.json
!docker/serviceAccountKey-example.json
frontend/src/ts/constants/firebase-config.ts
frontend/src/ts/constants/firebase-config-live.ts

View file

@ -3,6 +3,7 @@
"esbenp.prettier-vscode",
"Orta.vscode-jest",
"vitest.explorer",
"ryanluker.vscode-coverage-gutters"
"ryanluker.vscode-coverage-gutters",
"huntertran.auto-markdown-toc"
]
}

192
SELF_HOSTING.md Normal file
View file

@ -0,0 +1,192 @@
# Monkeytype Self Hosting
<!-- TOC ignore:true -->
## Table of contents
<!-- TOC -->
- [Monkeytype Self Hosting](#monkeytype-self-hosting)
- [Table of contents](#table-of-contents)
- [Prerequisites](#prerequisites)
- [Quickstart](#quickstart)
- [Account System](#account-system)
- [Setup Firebase](#setup-firebase)
- [Update backend configuration](#update-backend-configuration)
- [Setup Recaptcha](#setup-recaptcha)
- [Enable daily leaderboards](#enable-daily-leaderboards)
- [Configuration files](#configuration-files)
- [env file](#env-file)
- [serviceAccountKey.json](#serviceaccountkeyjson)
- [backend-configuration.json](#backend-configurationjson)
<!-- /TOC -->
## Prerequisites
- you need `docker` and `docker-compose-plugin` installed. Follow the [docker documentation](https://docs.docker.com/compose/install/) on how to do this.
## Quickstart
- create a new directory, e.g. `monkeytype` and open it.
- download the [docker-compose.yml](https://github.com/monkeytypegame/monkeytype/tree/master/docker/docker-compose.yml)
- create an `.env` file, you can copy the content from the [example.env](https://github.com/monkeytypegame/monkeytype/tree/master/docker/example.env).
- create an `serviceAccountKey.json` file. you can copy the content from the [serviceAccountKey-example.json](https://github.com/monkeytypegame/monkeytype/tree/master/docker/serviceAccountKey-example.json).
- download the [backend-configuration.json](https://github.com/monkeytypegame/monkeytype/tree/master/docker/backend-configuration.json)
- run `docker compose up -d`
- After the command exits successfully you can access [http://localhost:8080](http://localhost:8080)
## Account System
User signup/login is disabled by default. To allow users to signup you'll need to setup a Firebase project.
### Setup Firebase
- create a [Firebase](https://firebase.google.com/) account
- create a [new Firebase project](https://console.firebase.google.com/u/0/).
- name "monkeytype"
- uncheck "enable google analytics"
- enable authentication
- open the [firebase console](https://console.firebase.google.com/) and open your project
- go to `Authentication > Sign-in method`
- enable `Email/Password` and save
- generate service account
- open the project settings by clicking the `⚙` icon on the sidebar and `Project settings`
- go to `Service accounts`
- click `Generate new private key`. This will download a `.json` file.
- store the `.json` file as `serviceAccountKey.json`
- update the `.env` file
- open the [firebase console](https://console.firebase.google.com/) and open your project
- open the project settings by clicking the `⚙` icon on the sidebar and `Project settings`
- If there is no app in your project create a new web-app `</>`
- nickname `monkeytype`
- uncheck `set up firebase hosting`
- click `Register app`
- select your app and select `Config` for `SDK setup and configuration`
- it will display sth like this:
```
const firebaseConfig = {
apiKey: "AAAAAAAA",
authDomain: "monkeytype-00000.firebaseapp.com",
projectId: "monkeytype-00000",
storageBucket: "monkeytype-00000.appspot.com",
messagingSenderId: "90000000000",
appId: "1:90000000000:web:000000000000"
};
```
- update the `.env` file with the values above:
```
FIREBASE_APIKEY="AAAAAAAA"
FIREBASE_AUTHDOMAIN="monkeytype-00000.firebaseapp.com"
FIREBASE_PROJECTID="monkeytype-00000"
FIREBASE_STORAGEBUCKET="monkeytype-00000.appspot.com"
FIREBASE_MESSAGINGSENDERID="90000000000"
FIREBASE_APPID="1:90000000000:web:000000000000"
```
### Update backend configuration
- update the `backend-configuration.json` file and add/modify
```json
{
"users": {
"signUp": true,
"profiles": {
"enabled": true
}
}
}
```
### Setup Recaptcha
- [create](https://www.google.com/recaptcha/admin/create) a new recaptcha token
- label: monkeytype
- type: v2
- domain: the domain of the frontend
- update the `.env` file with the site key from the previous step
```
RECAPTCHA_SITE_KEY="your site key"
RECAPTCHA_SECRET="your secret key"
```
## Enable daily leaderboards
To enable daily leaderboards update the `backend-configuration.json` file and add/modify
```json
{
"dailyLeaderboards": {
"enabled": true,
"maxResults": 250,
"leaderboardExpirationTimeInDays": 1,
"validModeRules": [
{
"language": "english",
"mode": "time",
"mode2": "15"
},
{
"language": "english",
"mode": "time",
"mode2": "60"
}
]
}
}
```
- language is one of the supported language
- mode can be `time` or `words`
- mode2 can be `15`,`30`,`60` or `120` if you picked `mode=time` or `10`,`25`,`50` or `100` if you picked `mode=words`.
## Configuration files
### env file
All settings are described in the [example.env](https://github.com/monkeytypegame/monkeytype/tree/master/docker/example.env) file.
### serviceAccountKey.json
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`
- open the configuration in your browser, e.g. `http://localhost:5005/configuration`
- copy everything from `data` into the `backend-configuration.json` file.
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 the `curl` and `jq` installed you can also run `curl -wO- http://localhost:5005/configuration | jq ".data" > backend-configuration.json` to update the configuration file.
_Note:_ The configuration is applied on container startup only. You have to restart the container for your changes to become active.

View file

@ -21,4 +21,4 @@ services:
volumes:
mongo-data:
redis-data:
redis-data:

View file

@ -0,0 +1,16 @@
{
"configuration": {
"results": {
"savingEnabled": true
},
"users": {
"signUp": false,
"profiles": {
"enabled": false
}
},
"dailyLeaderboards": {
"enabled": false
}
}
}

44
docker/backend/Dockerfile Normal file
View file

@ -0,0 +1,44 @@
FROM node:hydrogen-alpine as builder
WORKDIR /app
#copy
COPY .eslintignore .eslintignore
COPY .eslintrc.json .eslintrc.json
COPY package.json package.json
COPY package-lock.json package-lock.json
COPY shared-types shared-types
COPY backend backend
#build
RUN npm ci
RUN cd backend && npm ci
RUN cd backend && npm run build
# target
FROM node:hydrogen-alpine
RUN apk add wget
WORKDIR /
COPY backend/redis-scripts /redis-scripts
WORKDIR /app
COPY backend/package.json package.json
COPY backend/package-lock.json package-lock.json
COPY docker/backend/entry-point.sh entry-point.sh
COPY docker/backend/applyConfig.sh applyConfig.sh
COPY --from=builder /app/backend/build .
#install deps (no dev-dependencies)
RUN npm ci --omit=dev
## logs
RUN mkdir logs
#run in env mode (no anticheat)
ENV MODE=dev
EXPOSE 5005
USER node
CMD [ "/bin/sh", "./entry-point.sh" ]

22
docker/backend/applyConfig.sh Executable file
View file

@ -0,0 +1,22 @@
#!/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

View file

@ -0,0 +1,3 @@
#!bin/sh
./applyConfig.sh &
node server.js

95
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,95 @@
version: "3.8"
services:
monkeytype-frontend:
container_name: monkeytype-frontend
image: monkeytype/monkeytype-frontend:latest
ports:
- "${HTTP_PORT:-8080}:80"
restart: on-failure
depends_on:
monkeytype-backend:
condition: service_healthy
env_file:
- path: ./.env
required: true
environment:
- FIREBASE_APIKEY=${FIREBASE_APIKEY:-""}
- FIREBASE_AUTHDOMAIN=${FIREBASE_AUTHDOMAIN:-""}
- FIREBASE_PROJECTID=${FIREBASE_PROJECTID:-""}
- FIREBASE_STORAGEBUCKET=${FIREBASE_STORAGEBUCKET:-""}
- FIREBASE_MESSAGINGSENDERID=${FIREBASE_MESSAGINGSENDERID:-""}
- FIREBASE_APPID=${FIREBASE_APPID:-""}
- MONKEYTYPE_BACKENDURL=${MONKEYTYPE_BACKENDURL}
- RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY:-""}
monkeytype-backend:
container_name: monkeytype-backend
image: monkeytype/monkeytype-backend:latest
ports:
- "${BACKEND_PORT:-5005}:5005"
restart: on-failure
environment:
- DB_NAME=monkeytype
- DB_URI=mongodb://monkeytype-mongodb:27017
- REDIS_URI=redis://monkeytype-redis:6379
- FRONTEND_URL=${MONKEYTYPE_FRONTENDURL}
- RECAPTCHA_SECRET=${RECAPTCHA_SECRET:-""}
volumes:
- type: bind
source: ./serviceAccountKey.json
target: /src/credentials/serviceAccountKey.json
read_only: true
- type: bind
source: ./backend-configuration.json
target: /app/backend-configuration.json
read_only: true
depends_on:
monkeytype-redis:
condition: service_healthy
monkeytype-mongodb:
condition: service_healthy
healthcheck:
test: ["CMD", "nc", "-z", "-v", "localhost", "5005"]
interval: 5s
timeout: 10s
retries: 15
start_period: 5s
monkeytype-redis:
container_name: monkeytype-redis
image: redis:6.2.6
#uncomment if you want to expose the redis server
#ports:
# - "${REDIS_PORT:-6379}:6379"
restart: on-failure
volumes:
- redis-data:/data
healthcheck:
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
interval: 5s
timeout: 10s
retries: 15
start_period: 10s
monkeytype-mongodb:
container_name: monkeytype-mongodb
image: mongo:5.0.8
restart: on-failure
volumes:
- mongo-data:/data/db
#uncomment if you want to expose the mongodb server
#ports:
# - "${MONGO_PORT:-27017}:27017"
healthcheck:
test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
interval: 5s
timeout: 10s
retries: 15
start_period: 10s
volumes:
mongo-data:
name: monkeytype_mongo_data
redis-data:
name: monkeytype_redis_data

32
docker/example.env Normal file
View file

@ -0,0 +1,32 @@
#url of the frontend, this must be accessible by your clients browser
MONKEYTYPE_FRONTENDURL="http://myserver:8080"
#url of the backend server, this must be accessible by your clients browser
MONKEYTYPE_BACKENDURL="http://myserver:5005"
# below config is only needed, if you need user accounts
# firebase config
FIREBASE_APIKEY=""
FIREBASE_AUTHDOMAIN=""
FIREBASE_PROJECTID=""
FIREBASE_STORAGEBUCKET=""
FIREBASE_MESSAGINGSENDERID=""
FIREBASE_APPID=""
# google recapture
RECAPTCHA_SITE_KEY=""
RECAPTCHA_SECRET=""
# use alternative ports
# port of the frontend http server
# HTTP_PORT=8080
# port of the backend api server
# BACKEND_PORT=5005
# port of the redis server, not exposed by default
# REDIS_PORT=6379
# port of the mongodb server, not exposed by default
# MONGO_PORT:27017

View file

@ -0,0 +1,30 @@
FROM node:hydrogen-alpine as builder
WORKDIR /app
#ENV
ENV BACKEND_URL="###MONKEYTYPE_BACKENDURL###"
ENV RECAPTCHA_SITE_KEY="###RECAPTCHA_SITE_KEY###"
#COPY
COPY .eslintrc.json .eslintrc.json
COPY package.json package.json
COPY package-lock.json package-lock.json
COPY shared-types shared-types
COPY frontend frontend
COPY docker/frontend/firebase-config-live.ts frontend/src/ts/constants/firebase-config.ts
COPY docker/frontend/firebase-config-live.ts frontend/src/ts/constants/firebase-config-live.ts
#BUILD
RUN npm ci
RUN cd frontend && npm ci
RUN cd frontend && npx vite build
# COPY to target
FROM nginx:mainline-alpine
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
COPY docker/frontend/updateConfig.sh /docker-entrypoint.d/updateConfig.sh
RUN chmod +x /docker-entrypoint.d/updateConfig.sh
# entry
#CMD ["./entryPoint.sh"]

View file

@ -0,0 +1,13 @@
// To find your config, go to https://console.firebase.google.com/ and select your project
// Go to (top left) Settings > Project Settings > General
// scroll down to Your apps > Web Apps (if it doesnt exist, create one) > SDK setup and configuration > select npm
// your config should be visible there
export const firebaseConfig = {
apiKey: "###FIREBASE_APIKEY###",
authDomain: "###FIREBASE_AUTHDOMAIN###",
projectId: "###FIREBASE_PROJECTID###",
storageBucket: "###FIREBASE_STORAGEBUCKET###",
messagingSenderId: "###FIREBASE_MESSAGINGSENDERID###",
appId: "###FIREBASE_APPID###",
};

16
docker/frontend/updateConfig.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/sh
cd /usr/share/nginx/html
echo "repace firebase config"
sed -i "s/###FIREBASE_APIKEY###/${FIREBASE_APIKEY}/g" js/firebase.*.js
sed -i "s/###FIREBASE_AUTHDOMAIN###/${FIREBASE_AUTHDOMAIN}/g" js/firebase.*.js
sed -i "s/###FIREBASE_PROJECTID###/${FIREBASE_PROJECTID}/g" js/firebase.*.js
sed -i "s/###FIREBASE_STORAGEBUCKET###/${FIREBASE_STORAGEBUCKET}/g" js/firebase.*.js
sed -i "s/###FIREBASE_MESSAGINGSENDERID###/${FIREBASE_MESSAGINGSENDERID}/g" js/firebase.*.js
sed -i "s/###FIREBASE_APPID###/${FIREBASE_APPID}/g" js/firebase.*.js
echo "use backend url ${MONKEYTYPE_BACKENDURL}"
sed -i "s/###MONKEYTYPE_BACKENDURL###/${MONKEYTYPE_BACKENDURL//\//\\/}/g" js/*.js
echo "use recapture ${RECAPTCHA_SITE_KEY}"
sed -i "s/###RECAPTCHA_SITE_KEY###/${RECAPTCHA_SITE_KEY//\//\\/}/g" js/*.js

View file

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "",
"private_key_id": "",
"private_key": "-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----\n",
"client_email": "",
"client_id": "",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "",
"universe_domain": "googleapis.com"
}

View file

@ -192,7 +192,9 @@ const BUILD_CONFIG = {
]),
],
define: {
BACKEND_URL: JSON.stringify("https://api.monkeytype.com"),
BACKEND_URL: JSON.stringify(
process.env.BACKEND_URL || "https://api.monkeytype.com"
),
IS_DEVELOPMENT: JSON.stringify(false),
CLIENT_VERSION: JSON.stringify(buildClientVersion()),
RECAPTCHA_SITE_KEY: JSON.stringify(process.env.RECAPTCHA_SITE_KEY),