Skip to content

Commit

Permalink
Add production build for proxy server
Browse files Browse the repository at this point in the history
  • Loading branch information
ai committed Apr 28, 2024
1 parent c94e5fd commit 0f89fd7
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 14 deletions.
1 change: 1 addition & 0 deletions nano-staged.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
".tool-versions": "tsx ./scripts/check-versions.ts",
"package.json": "tsx ./scripts/check-versions.ts",
"*/package.json": "tsx ./scripts/check-versions.ts",
"*/Dockerfile": "tsx ./scripts/check-versions.ts",
"*.test.ts": "tsx ./scripts/check-focused-tests.ts",
"core/messages/*/en.ts": "tsx ./scripts/check-messages.ts"
}
9 changes: 6 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions proxy/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Put only dist/ and nginx.conf files to Docker image to make deploy faster

*
!dist/
1 change: 1 addition & 0 deletions proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
coverage/
dist/
9 changes: 9 additions & 0 deletions proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:22.0.0-alpine

ENV NODE_ENV production
WORKDIR /var/www
COPY --chown=node:node . /var/www

COPY ./dist/ /var/www/

CMD "node" "--enable-source-maps" "index.mjs"
16 changes: 16 additions & 0 deletions proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,30 @@ _See the [full architecture guide](../README.md) first._

- `cd proxy && pnpm test`: run all proxy tests.
- `cd proxy && pnpm start`: run proxy server.
- `cd proxy && pnpm build`: prepare single JS file of the proxy server.
- `cd proxy && pnpm production`: start production build of the proxy server.

## Abuse Protection

- Proxy allows only GET requests and HTTP/HTTPS protocols.
- Proxy do not allow requests to in-cloud IP addresses like `127.0.0.1`.
- Proxy removes cookie headers.
- Proxy set user’s IP in `X-Forwarded-For` header.
- Proxy has timeout and response size limit.

## Test Strategy

To test proxy we emulate the real HTTP servers (end-to-end testing).

## Deploy

For deploy we:

1. Use `esbuild` to compile TS to JS and bundle all dependencies to a single JS file. Bundling allows us to put only necessary dependencies into the server.
2. Build Docker image with Node.js.
3. Run this image on Google Cloud Run.

We have 2 proxy servers:

- `proxy.slowreader.app` works only for production clients.
- `dev-proxy.slowreader.app` works with staging and PR previews.
12 changes: 8 additions & 4 deletions proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
"scripts": {
"start": "tsx watch index.ts",
"test": "FORCE_COLOR=1 pnpm run /^test:/",
"build": "esbuild index.ts --bundle --platform=node --sourcemap --format=esm --outfile=dist/index.mjs",
"production": "node --run build && ./scripts/run-image.sh",
"test:coverage": "c8 pnpm bnt",
"clean:coverage": "rm -rf coverage"
"clean:coverage": "rm -rf coverage",
"clean:build": "rm -rf build"
},
"dependencies": {
"martian-cidr": "2.0.3",
"tsx": "4.7.3"
"esbuild": "0.20.2",
"martian-cidr": "2.0.3"
},
"devDependencies": {
"better-node-test": "0.5.1",
"c8": "9.1.0"
"c8": "9.1.0",
"tsx": "4.7.3"
}
}
4 changes: 2 additions & 2 deletions proxy/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import isMartianIP from 'martian-cidr'
import type http from 'node:http'
import type { Server } from 'node:http'
import { createServer } from 'node:http'
import { isIP } from 'node:net'
import { styleText } from 'node:util'
Expand All @@ -19,7 +19,7 @@ export function createProxyServer(config: {
allowsFrom: RegExp[]
maxSize: number
timeout: number
}): http.Server {
}): Server {
return createServer(async (req, res) => {
let sent = false

Expand Down
22 changes: 22 additions & 0 deletions proxy/scripts/run-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

ERROR='\033[0;31m'
NC='\033[0m' # No Color

command_exists() {
command -v "$1" >/dev/null 2>&1
}

build_and_run() {
IMAGE_ID=$($1 build . | tail -1)
$1 run --rm -p 5284 -e PORT=5284 -it $IMAGE_ID
}

if command_exists podman; then
build_and_run podman
elif command_exists docker; then
build_and_run docker
else
echo -e "${ERROR}Install Podman or Docker${NC}"
exit 1
fi
25 changes: 22 additions & 3 deletions scripts/check-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// - All dependencies has "1.2.3" requirement and not "^1.2.3" used by npm.
// It prevents unexpected updates on lock file issues.
// - Node.js and pnpm versions in .tool-versions and package.json are the same.
// - All Dockerfile use the same Node.js version that is in .tool-versions.

import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
Expand All @@ -21,6 +22,7 @@ function error(msg: string): void {
let pkg = read('package.json')
let toolVersions = read('.tool-versions')

let nodeFull = toolVersions.match(/nodejs (\d+\.\d+\.\d+)/)![1]
let nodeMinor = toolVersions.match(/nodejs (\d+\.\d+)\./)![1]
let pnpmMajor = toolVersions.match(/pnpm (\d+)\./)![1]

Expand All @@ -42,10 +44,27 @@ function checkDependencies(file: string, content: string): void {
}
}

function checkDockerfile(file: string, content: string): void {
let match = content.match(/node:(\d+\.\d+\.\d+)/)
if (match && match[1] !== nodeFull) {
error(
`Different Node.js version in ${file}: ${styleText('yellow', match[1]!)}`
)
}
}

let projects = readdirSync(ROOT)

checkDependencies('package.json', pkg)
for (let child of readdirSync(ROOT)) {
if (existsSync(join(ROOT, child, 'package.json'))) {
let relative = join(child, 'package.json')
for (let project of projects) {
if (existsSync(join(ROOT, project, 'package.json'))) {
let relative = join(project, 'package.json')
checkDependencies(relative, read(relative))
}
}
for (let project of projects) {
if (existsSync(join(ROOT, project, 'Dockerfile'))) {
let relative = join(project, 'Dockerfile')
checkDockerfile(relative, read(relative))
}
}
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build:visual": "NODE_ENV=test storybook build",
"build:web": "vite build && tsx ./scripts/clean-css.ts",
"format:stylelint": "stylelint --fix **/*.{css,svelte}",
"clean:dist": "rm -rf dist"
"clean:build": "rm -rf dist"
},
"dependencies": {
"@csstools/postcss-oklab-function": "3.0.14",
Expand Down
2 changes: 1 addition & 1 deletion web/scripts/run-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ command_exists() {
build_and_run() {
IMAGE_ID=$($1 build . | tail -1)
echo -e "${OK}Web server is running on http://localhost:8080${NC}"
$1 run --rm -p 8080:80 -e PORT=80 -it $IMAGE_ID
$1 run --rm -p 8080 -e PORT=8080 -it $IMAGE_ID
}

if command_exists podman; then
Expand Down

0 comments on commit 0f89fd7

Please sign in to comment.