Adds Render deploy (#183)

This commit is contained in:
Chris Anderson 2023-06-03 21:40:13 -05:00 committed by GitHub
parent 6684a8e135
commit 88fb7fa4f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 323 additions and 13 deletions

View file

@ -23,10 +23,19 @@
- 🔒 **Secure** SSO (SAML/OpenID) is provided out of the box, no extra bolts or add-ons.
- 📦 **Open Source** Easy to setup and get running in your own cloud.
## 🚀 Getting Started
## 🚀 Deployment
You can run Parcelvoy locally or in the cloud easily using Docker.
### Render
You can do a one-click deploy on Render using the button below:
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/parcelvoy/platform)
### Docker Compose
To get up and running quickly to try things out, copy our latest `docker-compose.yml` and `.env` file onto your machine and go!
```
mkdir parcelvoy && cd parcelvoy

View file

@ -44,7 +44,7 @@ exports.up = function(knex) {
.references('id')
.inTable('projects')
.onDelete('CASCADE')
table.string('anonymous_id').defaultTo(knex.raw('(UUID())'))
table.string('anonymous_id')
table.string('external_id', 255).notNullable()
table.string('email', 255).nullable()
table.string('phone', 64).nullable()

View file

@ -1,6 +1,6 @@
exports.up = async function(knex) {
await knex.schema.alterTable('journey_steps', function(table) {
table.uuid('uuid').defaultTo()
table.uuid('uuid')
table.integer('x')
.notNullable()
.defaultTo(0)

View file

@ -27,8 +27,8 @@ exports.up = async function(knex) {
})
const orgId = await knex('organizations').insert({ id: 1, username: 'main' })
await knex.raw('UPDATE projects SET organization_id = ? WHERE organization_id IS NULL', [orgId])
await knex.raw('UPDATE admins SET organization_id = ? WHERE organization_id IS NULL', [orgId])
await knex('projects').update({ organization_id: orgId }).whereNull('organization_id')
await knex('admins').update({ organization_id: orgId }).whereNull('organization_id')
}
exports.down = async function(knex) {

View file

@ -0,0 +1,11 @@
exports.up = async function(knex) {
await knex.schema.table('admins', function(table) {
table.string('last_name', 255).nullable().alter()
})
}
exports.down = async function(knex) {
await knex.schema.table('admins', function(table) {
table.string('last_name', 255).notNullable().alter()
})
}

View file

@ -42,6 +42,7 @@
"koa": "^2.13.4",
"koa-body": "5.0.0",
"koa-jwt": "^4.0.3",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"libphonenumber-js": "^1.10.24",
"mysql2": "^2.3.3",
@ -50,6 +51,7 @@
"nodemailer": "^6.7.6",
"nodemailer-mailgun-transport": "^2.1.5",
"openid-client": "^5.2.1",
"pg": "^8.11.0",
"pino": "^8.1.0",
"pino-pretty": "^8.1.0",
"sqs-consumer": "^7.0.0"
@ -4456,6 +4458,14 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/builtins": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
@ -9341,6 +9351,11 @@
"node": ">=6"
}
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -9449,11 +9464,94 @@
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"node_modules/pg": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.0.tgz",
"integrity": "sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.6.0",
"pg-pool": "^3.6.0",
"pg-protocol": "^1.6.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz",
"integrity": "sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz",
"integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pg/node_modules/pg-connection-string": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.0.tgz",
"integrity": "sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg=="
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -9609,6 +9707,41 @@
"node": ">=8"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -11517,6 +11650,14 @@
"node": ">=0.6.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -36,6 +36,7 @@
"koa": "^2.13.4",
"koa-body": "5.0.0",
"koa-jwt": "^4.0.3",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"libphonenumber-js": "^1.10.24",
"mysql2": "^2.3.3",
@ -44,6 +45,7 @@
"nodemailer": "^6.7.6",
"nodemailer-mailgun-transport": "^2.1.5",
"openid-client": "^5.2.1",
"pg": "^8.11.0",
"pino": "^8.1.0",
"pino-pretty": "^8.1.0",
"sqs-consumer": "^7.0.0"

View file

@ -46,7 +46,7 @@ export default class Api extends Koa {
.use(cors())
.use(serve('./public', {
hidden: true,
defer: true,
defer: !app.env.mono,
}))
controllers(this)

View file

@ -35,6 +35,11 @@ export default {
code: 1007,
statusCode: 403,
},
MissingCredentials: {
message: 'An email and password must be provided to login.',
code: 1008,
statusCode: 400,
},
InvalidCredentials: {
message: 'The email and password combination provided is invalid.',
code: 1008,

View file

@ -33,7 +33,7 @@ export default class BasicAuthProvider extends AuthProvider {
async validate(ctx: Context) {
const { email, password } = ctx.request.body
if (!email || !password) throw new RequestError(AuthError.InvalidCredentials)
if (!email || !password) throw new RequestError(AuthError.MissingCredentials)
// Check email and password match
if (email !== this.config.email || password !== this.config.password) {

View file

@ -1,4 +1,5 @@
import Router from '@koa/router'
import send from 'koa-send'
import ProjectController, { ProjectSubrouter, projectMiddleware } from '../projects/ProjectController'
import ClientController from '../client/ClientController'
import SegmentController from '../client/SegmentController'
@ -38,6 +39,20 @@ export default (api: import('../api').default) => {
api.use(root.routes())
.use(root.allowedMethods())
// If we are running in mono mode, we need to also serve the UI
if (api.app.env.mono) {
const ui = new Router()
ui.get('/(.*)', async (ctx, next) => {
try {
await send(ctx, './public/index.html')
} catch {
return next()
}
})
api.use(ui.routes())
.use(ui.allowedMethods())
}
}
/**

View file

@ -9,6 +9,7 @@ import { RedisConfig } from './redis'
export type Runner = 'api' | 'worker'
export interface Env {
runners: Runner[]
mono: boolean
db: DatabaseConfig
queue: QueueConfig
storage: StorageConfig
@ -42,8 +43,12 @@ type EnvType = 'production' | 'test'
export default (type?: EnvType): Env => {
dotenv.config({ path: `.env${type === 'test' ? '.test' : ''}` })
const port = parseInt(process.env.PORT ?? '3000')
const baseUrl = process.env.BASE_URL ?? `http://localhost:${port}`
return {
runners: (process.env.RUNNER ?? 'api,worker').split(',') as Runner[],
mono: (process.env.MONO ?? 'false') === 'true',
db: {
client: process.env.DB_CLIENT as 'mysql2' | 'postgres',
connection: {
@ -88,8 +93,8 @@ export default (type?: EnvType): Env => {
baseUrl: process.env.STORAGE_BASE_URL,
}),
}),
baseUrl: process.env.BASE_URL!,
port: parseInt(process.env.PORT!),
baseUrl,
port,
secret: process.env.APP_SECRET!,
auth: driver<AuthConfig>(process.env.AUTH_DRIVER, {
basic: () => ({
@ -112,9 +117,6 @@ export default (type?: EnvType): Env => {
clientSecret: process.env.AUTH_OPENID_CLIENT_SECRET,
redirectUri: process.env.AUTH_OPENID_REDIRECT_URI,
}),
logger: () => ({
tokenLife: defaultTokenLife,
}),
}),
error: driver<ErrorConfig>(process.env.ERROR_DRIVER, {
bugsnag: () => ({

View file

@ -4,6 +4,6 @@ declare global {
export const env = {
api: {
baseURL: window.API_BASE_URL ?? process.env.REACT_APP_API_BASE_URL,
baseURL: window.API_BASE_URL ?? process.env.REACT_APP_API_BASE_URL ?? '/api',
},
}

37
docker/Dockerfile.render Normal file
View file

@ -0,0 +1,37 @@
# --------------> The frontend compiler image
FROM node:18 AS frontend_compile
WORKDIR /usr/src/app/apps/ui
COPY ./tsconfig.base.json /usr/src/app
COPY ./apps/ui ./
RUN npm ci
RUN npm run build
# --------------> The backend compiler image
FROM node:18 AS backend_compile
WORKDIR /usr/src/app/apps/platform
COPY ./tsconfig.base.json /usr/src/app
COPY ./apps/platform ./
RUN npm ci
RUN npm run build
# --------------> The build image
FROM node:18 AS build
WORKDIR /usr/src/app
COPY --from=backend_compile /usr/src/app/apps/platform/package*.json ./
COPY --from=backend_compile /usr/src/app/apps/platform/build ./
COPY --from=backend_compile /usr/src/app/apps/platform/db ./db
COPY --from=backend_compile /usr/src/app/apps/platform/scripts ./scripts
COPY --from=backend_compile /usr/src/app/apps/platform/public ./public
COPY --from=frontend_compile /usr/src/app/apps/ui/build ./public
RUN npm ci --only=production
# --------------> The production image
FROM node:18-alpine
RUN apk add dumb-init
ENV NODE_ENV="production"
USER node
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app ./
EXPOSE 80
CMD ["dumb-init", "node", "index.js"]

88
render.yaml Normal file
View file

@ -0,0 +1,88 @@
services:
- type: web
name: parcelvoy
env: docker
repo: https://github.com/parcelvoy/platform.git
dockerfilePath: ./docker/Dockerfile.render
dockerContext: .
autoDeploy: false
healthCheckPath: /api/health
envVars:
- key: NODE_ENV
value: production
- key: BASE_URL
sync: false
- key: MONO
value: true
- key: APP_SECRET
generateValue: true
- key: DB_CLIENT
value: mysql2
- key: DB_HOST
fromService:
type: pserv
name: mysql
property: host
- key: DB_USERNAME
fromService:
type: pserv
name: mysql
envVarKey: MYSQL_USER
- key: DB_PASSWORD
fromService:
type: pserv
name: mysql
envVarKey: MYSQL_PASSWORD
- key: DB_PORT
fromService:
type: pserv
name: mysql
property: port
- key: DB_DATABASE
fromService:
type: pserv
name: mysql
envVarKey: MYSQL_DATABASE
- key: STORAGE_DRIVER
value: local
- key: QUEUE_DRIVER
value: redis
- key: REDIS_HOST
fromService:
type: redis
name: lightning
property: host
- key: REDIS_PORT
fromService:
type: redis
name: lightning
property: port
- key: AUTH_DRIVER
value: basic
- key: AUTH_BASIC_EMAIL
sync: false
- key: AUTH_BASIC_PASSWORD
sync: false
- type: redis
name: lightning
ipAllowList: []
maxmemoryPolicy: noeviction # optional (defaults to allkeys-lru)
- type: pserv
name: mysql
plan: standard
env: docker
repo: https://github.com/render-examples/mysql
autoDeploy: false
disk:
name: mysql
mountPath: /var/lib/mysql
sizeGB: 10
envVars:
- key: MYSQL_ROOT_PASSWORD
generateValue: true
- key: MYSQL_DATABASE
value: parcelvoy
- key: MYSQL_USER
value: parcelvoy
- key: MYSQL_PASSWORD
generateValue: true