Skip to content

Commit 146e88e

Browse files
committed
feat: functions in docker
0 parents  commit 146e88e

14 files changed

+295
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
data
2+
.env
3+
.nhost
4+
node_modules

Dockerfile

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
FROM node:14-alpine
2+
3+
# * path to the server files
4+
ARG SERVER_PATH /opt/server
5+
ENV SERVER_PATH=$SERVER_PATH
6+
7+
# * Required to access to the globally installed modules
8+
ENV NODE_PATH=/usr/local/lib/node_modules
9+
10+
# * Add path to the stored pnpm packages
11+
ENV PNPM_HOME=/root/.local/share/pnpm
12+
ENV PATH=$PATH:$PNPM_HOME
13+
14+
# * Directory where the Nhost project is located
15+
ENV NHOST_PROJECT_PATH=/opt/project
16+
17+
# * Default package manager
18+
ENV PACKAGE_MANAGER=pnpm
19+
20+
# * Install packages that are required for this docker image to run
21+
RUN npm install -g pnpm nodemon express glob @swc-node/register typescript @antfu/ni
22+
23+
# * The pnpm store should be mounted in the same volume as node_modules (requires hard links)
24+
# * See https://pnpm.io/6.x/npmrc#store-dir
25+
RUN pnpm config set store-dir $NHOST_PROJECT_PATH/node_modules/.pnpm-store
26+
27+
# * Copy server files
28+
COPY nodemon.json start.sh server.ts $SERVER_PATH/
29+
30+
# * Change working directory to the Nhost project directory
31+
WORKDIR $NHOST_PROJECT_PATH
32+
33+
CMD $SERVER_PATH/start.sh

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Nhost functions in Docker
2+
3+
## Features
4+
- Hot reload of package.json, lock files, and the functions directory
5+
- Accepts both Javascript and Typesript
6+
- Create a package.json file if missing
7+
- Customisable custom package manager (pnpm by default)
8+
- No need to add the Express dependency anymore
9+
10+
## Example
11+
12+
```sh
13+
cd example
14+
cp .env.example .env
15+
# Full Nhost
16+
docker-compose up
17+
18+
# Or only functions
19+
docker-compose up functions traefik
20+
21+
```

example/.env.example

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
POSTGRES_PASSWORD=secret-pg-password-never-use-this-value
2+
HASURA_GRAPHQL_ADMIN_SECRET=hello123
3+
HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256", "key":"5152fa850c02dc222631cca898ed1485821a70912a6e3649c49076912daa3b62182ba013315915d64f40cddfbb8b58eb5bd11ba225336a6af45bbae07ca873f3","issuer":"hasura-auth"}'
4+
STORAGE_ACCESS_KEY=storage-access-key-never-use-this-value
5+
STORAGE_SECRET_KEY=storage-secret-key-never-use-this-value

example/docker-compose.yaml

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
version: '3.6'
2+
services:
3+
traefik:
4+
image: "traefik:v2.5"
5+
container_name: "traefik"
6+
command:
7+
- "--api.insecure=true"
8+
- "--providers.docker=true"
9+
- "--providers.docker.exposedbydefault=false"
10+
- "--entrypoints.web.address=:1337"
11+
ports:
12+
- "1337:1337"
13+
- "9090:8080"
14+
volumes:
15+
- "/var/run/docker.sock:/var/run/docker.sock:ro"
16+
postgres:
17+
image: postgres
18+
restart: always
19+
volumes:
20+
- ./data/db:/var/lib/postgresql/data
21+
- ./initdb.d:/docker-entrypoint-initdb.d:ro
22+
environment:
23+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secretpgpassword}
24+
ports:
25+
- '5432:5432'
26+
graphql-engine:
27+
image: hasura/graphql-engine:v2.2.0
28+
depends_on:
29+
- 'postgres'
30+
restart: always
31+
expose:
32+
- 8080
33+
environment:
34+
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-secretpgpassword}@postgres:5432/postgres
35+
HASURA_GRAPHQL_JWT_SECRET: ${HASURA_GRAPHQL_JWT_SECRET}
36+
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
37+
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: public
38+
HASURA_GRAPHQL_LOG_LEVEL: debug
39+
HASURA_GRAPHQL_ENABLE_CONSOLE: 'true'
40+
labels:
41+
- "traefik.enable=true"
42+
- "traefik.http.routers.hasura.rule=Host(`localhost`) && PathPrefix(`/`)"
43+
- "traefik.http.routers.hasura.entrypoints=web"
44+
auth:
45+
image: nhost/hasura-auth:latest
46+
depends_on:
47+
- postgres
48+
- graphql-engine
49+
restart: always
50+
environment:
51+
AUTH_HOST: '0.0.0.0'
52+
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-secretpgpassword}@postgres:5432/postgres
53+
HASURA_GRAPHQL_GRAPHQL_URL: http://graphql-engine:8080/v1/graphql
54+
HASURA_GRAPHQL_JWT_SECRET: ${HASURA_GRAPHQL_JWT_SECRET}
55+
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
56+
AUTH_CLIENT_URL: ${AUTH_CLIENT_URL:-http://localhost:3000}
57+
AUTH_SMTP_HOST: mailhog
58+
AUTH_SMTP_PORT: 1025
59+
AUTH_SMTP_USER: user
60+
AUTH_SMTP_PASS: password
61+
AUTH_SMTP_SENDER: [email protected]
62+
expose:
63+
- 4000
64+
healthcheck:
65+
disable: true
66+
labels:
67+
- "traefik.enable=true"
68+
- "traefik.http.middlewares.strip-auth.stripprefix.prefixes=/v1/auth"
69+
- "traefik.http.routers.auth.rule=Host(`localhost`) && PathPrefix(`/v1/auth`)"
70+
- "traefik.http.routers.auth.middlewares=strip-auth@docker"
71+
- "traefik.http.routers.auth.entrypoints=web"
72+
storage:
73+
image: nhost/hasura-storage:0.2.1
74+
depends_on:
75+
- postgres
76+
- graphql-engine
77+
- minio
78+
restart: always
79+
expose:
80+
- 8000
81+
healthcheck:
82+
disable: true
83+
environment:
84+
PUBLIC_URL: http://localhost:${PROXY_PORT:-1337}
85+
HASURA_METADATA: 1
86+
HASURA_ENDPOINT: http://graphql-engine:8080/v1
87+
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
88+
S3_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
89+
S3_SECRET_KEY: ${STORAGE_SECRET_KEY}
90+
S3_ENDPOINT: http://minio:8484
91+
S3_BUCKET: nhost
92+
POSTGRES_MIGRATIONS: 1
93+
POSTGRES_MIGRATIONS_SOURCE: postgres://postgres:${POSTGRES_PASSWORD:-secretpgpassword}@postgres:5432/postgres?sslmode=disable
94+
labels:
95+
- "traefik.enable=true"
96+
- "traefik.http.routers.storage.rule=Host(`localhost`) && PathPrefix(`/v1/storage`)"
97+
- "traefik.http.routers.storage.entrypoints=web"
98+
# Rewrite the path so it matches with the new storage API path introduced in hasura-storage 0.2
99+
- "traefik.http.middlewares.strip-suffix.replacepathregex.regex=^/v1/storage/(.*)"
100+
- "traefik.http.middlewares.strip-suffix.replacepathregex.replacement=/v1/$$1"
101+
- "traefik.http.routers.storage.middlewares=strip-suffix@docker"
102+
command: serve
103+
functions:
104+
build: ..
105+
labels:
106+
- "traefik.enable=true"
107+
- "traefik.http.middlewares.strip-functions.stripprefix.prefixes=/v1/functions"
108+
- "traefik.http.routers.functions.rule=Host(`localhost`) && PathPrefix(`/v1/functions`)"
109+
- "traefik.http.routers.functions.middlewares=strip-functions@docker"
110+
- "traefik.http.routers.functions.entrypoints=web"
111+
restart: always
112+
expose:
113+
- 3000
114+
healthcheck:
115+
disable: true
116+
volumes:
117+
- .:/opt/project
118+
- functions_node_modules:/opt/project/node_modules
119+
- /opt/project/data/
120+
- /opt/project/initdb.d/
121+
- /opt/project/docker-functions/
122+
minio:
123+
image: minio/minio:RELEASE.2021-09-24T00-24-24Z
124+
entrypoint: sh
125+
command: -c 'mkdir -p /data/nhost && /opt/bin/minio server --address :8484 /data'
126+
environment:
127+
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY}
128+
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY}
129+
ports:
130+
- ${MINIO_PORT:-8484}:8484
131+
volumes:
132+
- ./data/minio:/data
133+
mailhog:
134+
image: mailhog/mailhog
135+
environment:
136+
SMTP_HOST: ${AUTH_SMTP_HOST:-mailhog}
137+
SMTP_PORT: ${AUTH_SMTP_PORT:-1025}
138+
SMTP_PASS: ${AUTH_SMTP_PASS:-password}
139+
SMTP_USER: ${AUTH_SMTP_USER:-user}
140+
SMTP_SECURE: "${AUTH_SMTP_SECURE:-false}"
141+
SMTP_SENDER: ${AUTH_SMTP_SENDER:[email protected]}
142+
ports:
143+
- ${AUTH_SMTP_PORT:-1025}:1025
144+
- 8025:8025
145+
volumes:
146+
- ./data/mailhog:/maildir
147+
volumes:
148+
functions_node_modules:

example/functions/hello.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default (req, res) => {
2+
res.status(200).send(`Hullo, ${req.query.name}!`)
3+
}

example/functions/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default (req, res) => {
2+
res.status(200).send(`This is the index function`)
3+
}

example/functions/sub/hello.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default (req, res) => {
2+
res.status(200).send(`Hello from a subdirectory, ${req.query.name}!`)
3+
}

example/functions/sub/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default (req, res) => {
2+
res.status(200).send(`Index of a sub-directory`)
3+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- auth schema
2+
CREATE SCHEMA IF NOT EXISTS auth;
3+
CREATE SCHEMA IF NOT EXISTS storage;
4+
-- https://github.com/hasura/graphql-engine/issues/3657
5+
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
6+
CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public;
7+
CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger LANGUAGE plpgsql AS $$
8+
declare _new record;
9+
begin _new := new;
10+
_new."updated_at" = now();
11+
return _new;
12+
end;
13+
$$;

example/package.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "project",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"dependencies": {},
6+
"devDependencies": {},
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"keywords": [],
11+
"author": "",
12+
"license": "ISC",
13+
"description": ""
14+
}

nodemon.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ignore": ["!(package*.json|pnpm-*.yaml|yarn.lock)"],
3+
"execMap": {
4+
"json": "ni && nodemon -w functions -x 'node -r @swc-node/register' $SERVER_PATH/server.ts"
5+
},
6+
"ext": "json,yaml,lock"
7+
}

server.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import path from 'path'
2+
3+
import express from 'express'
4+
import glob from 'glob'
5+
6+
const PORT = 3000
7+
8+
const main = async () => {
9+
const app = express()
10+
const functionsPath = path.join(process.cwd(), 'functions')
11+
const files = glob.sync('**/*.@(js|ts)', { cwd: functionsPath })
12+
13+
for (const file of files) {
14+
const { default: handler } = await import(path.join(functionsPath, file))
15+
if (handler) {
16+
const route = `/${file}`.replace(/(\.ts|\.js)$/, '').replace(/\/index$/, '/')
17+
app.all(route, handler)
18+
console.log(`Loaded route ${route} from ./functions/${file}`)
19+
} else {
20+
console.warn(`No default export in ./functions/${file}`)
21+
}
22+
}
23+
24+
app.listen(PORT, () => {
25+
console.log(`Listening on port ${PORT}`)
26+
})
27+
}
28+
29+
main()

start.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# * Set the default package manager to use if cannot be guessed from lock files
2+
echo "defaultAgent=$PACKAGE_MANAGER" > ~/.nirc
3+
4+
# * Create a default package.json file if it doesn't exist yet
5+
npm init -y 1> /dev/null
6+
7+
# * Start nodemon that listens to package.json and lock files and run npm/pnpm/yarn install,
8+
# * Then run another nodemon that listens to the functions directory and run the server
9+
nodemon --config $SERVER_PATH/nodemon.json package.json

0 commit comments

Comments
 (0)