├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md └── steps ├── 01-init-app ├── .env.example ├── .gitignore ├── app │ └── index.ts ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 02-docker ├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 03-init-fly ├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 04-persisted-volume ├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 05-healthcheck ├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 06-ssh-sqlite-cli ├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 07-basic-github-deploy-action ├── .dockerignore ├── .env.example ├── .github │ └── workflows │ │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 08-staging-github-action ├── .dockerignore ├── .env.example ├── .github │ └── workflows │ │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 09-litefs ├── .dockerignore ├── .env.example ├── .github │ └── workflows │ │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── litefs.yml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 10-consul ├── .dockerignore ├── .env.example ├── .github │ └── workflows │ │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── litefs.yml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json ├── 11-multi-region ├── .dockerignore ├── .env.example ├── .github │ └── workflows │ │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── app │ └── index.ts ├── fly.toml ├── litefs.yml ├── package-lock.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230118213146_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── start.js └── tsconfig.json └── 12-transactional-consistency ├── .dockerignore ├── .env.example ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── app └── index.ts ├── fly.toml ├── litefs.yml ├── package-lock.json ├── package.json ├── prisma ├── migrations │ ├── 20230118213146_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── start.js └── tsconfig.json /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | 8 | jobs: 9 | deploy: 10 | name: 🚀 Deploy 11 | runs-on: ubuntu-latest 12 | if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} 13 | steps: 14 | - name: ⬇️ Checkout repo 15 | uses: actions/checkout@v3 16 | 17 | - name: 👀 Read app name 18 | uses: SebRollen/toml-action@v1.0.2 19 | id: app_name 20 | with: 21 | file: "steps/12-transactional-consistency/fly.toml" 22 | field: "app" 23 | 24 | - name: 🔩 Install fly 25 | uses: superfly/flyctl-actions/setup-flyctl@master 26 | 27 | - name: 🚀 Deploy Staging 28 | if: ${{ github.ref == 'refs/heads/dev' }} 29 | run: 30 | "cd ./steps/12-transactional-consistency && flyctl deploy --app ${{ 31 | steps.app_name.outputs.value }}-staging --remote-only" 32 | env: 33 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 34 | 35 | - name: 🚀 Deploy Production 36 | if: ${{ github.ref == 'refs/heads/main' }} 37 | run: 38 | "cd ./steps/12-transactional-consistency && flyctl deploy 39 | --remote-only" 40 | env: 41 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Distributed Node SQLite Demo 2 | 3 | This is a demo of a simple node server that uses Prisma and SQLite and is distributed across multiple regions on Fly.io. 4 | 5 | Watch the tutorial here: https://www.epicweb.dev/tutorials/deploy-web-applications 6 | 7 | -------------------------------------------------------------------------------- /steps/01-init-app/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/01-init-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/01-init-app/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "POST /": { 35 | const params = await parseFormBody(req); 36 | const intent = params.get("intent"); 37 | const currentCount = await getCurrentCount(); 38 | if (intent !== "increment" && intent !== "decrement") { 39 | return res.end("Invalid intent"); 40 | } 41 | await prisma.count.update({ 42 | where: { id: currentCount.id }, 43 | data: { count: { [intent]: 1 } }, 44 | }); 45 | res.writeHead(302, { Location: "/" }); 46 | res.end(); 47 | break; 48 | } 49 | case "GET /": { 50 | let currentCount = await getCurrentCount(); 51 | res.setHeader("Content-Type", "text/html"); 52 | res.writeHead(200); 53 | res.end(/* html */ ` 54 | 55 | 56 | Demo App 57 | 58 | 59 |

Demo App

60 |
61 | 62 | ${currentCount.count} 63 | 64 |
65 | 66 | 67 | `); 68 | break; 69 | } 70 | default: { 71 | res.writeHead(404); 72 | return res.end("Not found"); 73 | } 74 | } 75 | }) 76 | .listen(process.env.PORT, () => { 77 | const address = server.address(); 78 | if (!address) { 79 | console.log("Server listening"); 80 | return; 81 | } 82 | const url = 83 | typeof address === "string" 84 | ? address 85 | : `http://localhost:${address.port}`; 86 | console.log(`Server listening at ${url}`); 87 | }); 88 | -------------------------------------------------------------------------------- /steps/01-init-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/01-init-app/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/01-init-app/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/01-init-app/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/01-init-app/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/01-init-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/02-docker/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/02-docker/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/02-docker/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/02-docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | RUN apt-get update && apt-get install -y openssl sqlite3 6 | 7 | # install all node_modules, including dev 8 | FROM base as deps 9 | 10 | RUN mkdir /app/ 11 | WORKDIR /app/ 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install 15 | 16 | # setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app/ 20 | WORKDIR /app/ 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --omit=dev 25 | 26 | # build app 27 | FROM base as build 28 | 29 | RUN mkdir /app/ 30 | WORKDIR /app/ 31 | 32 | COPY --from=deps /app/node_modules /app/node_modules 33 | 34 | # schema doesn't change much so these will stay cached 35 | ADD prisma /app/prisma 36 | 37 | RUN npx prisma generate 38 | 39 | # app code changes all the time 40 | ADD . . 41 | RUN npm run build 42 | 43 | # build smaller image for running 44 | FROM base 45 | 46 | ENV DATABASE_URL="file:/app/data/sqlite.db" 47 | ENV PORT="8080" 48 | ENV NODE_ENV="production" 49 | 50 | RUN mkdir /app/ 51 | WORKDIR /app/ 52 | 53 | COPY --from=production-deps /app/node_modules /app/node_modules 54 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 55 | COPY --from=build /app/build /app/build 56 | 57 | ADD . . 58 | 59 | CMD ["npm", "start"] 60 | -------------------------------------------------------------------------------- /steps/02-docker/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "POST /": { 35 | const params = await parseFormBody(req); 36 | const intent = params.get("intent"); 37 | const currentCount = await getCurrentCount(); 38 | if (intent !== "increment" && intent !== "decrement") { 39 | return res.end("Invalid intent"); 40 | } 41 | await prisma.count.update({ 42 | where: { id: currentCount.id }, 43 | data: { count: { [intent]: 1 } }, 44 | }); 45 | res.writeHead(302, { Location: "/" }); 46 | res.end(); 47 | break; 48 | } 49 | case "GET /": { 50 | let currentCount = await getCurrentCount(); 51 | res.setHeader("Content-Type", "text/html"); 52 | res.writeHead(200); 53 | res.end(/* html */ ` 54 | 55 | 56 | Demo App 57 | 58 | 59 |

Demo App

60 |
61 | 62 | ${currentCount.count} 63 | 64 |
65 | 66 | 67 | `); 68 | break; 69 | } 70 | default: { 71 | res.writeHead(404); 72 | return res.end("Not found"); 73 | } 74 | } 75 | }) 76 | .listen(process.env.PORT, () => { 77 | const address = server.address(); 78 | if (!address) { 79 | console.log("Server listening"); 80 | return; 81 | } 82 | const url = 83 | typeof address === "string" 84 | ? address 85 | : `http://localhost:${address.port}`; 86 | console.log(`Server listening at ${url}`); 87 | }); 88 | -------------------------------------------------------------------------------- /steps/02-docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/02-docker/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/02-docker/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/02-docker/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/02-docker/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/02-docker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/03-init-fly/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/03-init-fly/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/03-init-fly/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/03-init-fly/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | RUN apt-get update && apt-get install -y openssl sqlite3 6 | 7 | # install all node_modules, including dev 8 | FROM base as deps 9 | 10 | RUN mkdir /app/ 11 | WORKDIR /app/ 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install 15 | 16 | # setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app/ 20 | WORKDIR /app/ 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --omit=dev 25 | 26 | # build app 27 | FROM base as build 28 | 29 | RUN mkdir /app/ 30 | WORKDIR /app/ 31 | 32 | COPY --from=deps /app/node_modules /app/node_modules 33 | 34 | # schema doesn't change much so these will stay cached 35 | ADD prisma /app/prisma 36 | 37 | RUN npx prisma generate 38 | 39 | # app code changes all the time 40 | ADD . . 41 | RUN npm run build 42 | 43 | # build smaller image for running 44 | FROM base 45 | 46 | ENV DATABASE_URL="file:/app/data/sqlite.db" 47 | ENV PORT="8080" 48 | ENV NODE_ENV="production" 49 | 50 | RUN mkdir /app/ 51 | WORKDIR /app/ 52 | 53 | COPY --from=production-deps /app/node_modules /app/node_modules 54 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 55 | COPY --from=build /app/build /app/build 56 | 57 | ADD . . 58 | 59 | CMD ["npm", "start"] 60 | -------------------------------------------------------------------------------- /steps/03-init-fly/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "POST /": { 35 | const params = await parseFormBody(req); 36 | const intent = params.get("intent"); 37 | const currentCount = await getCurrentCount(); 38 | if (intent !== "increment" && intent !== "decrement") { 39 | return res.end("Invalid intent"); 40 | } 41 | await prisma.count.update({ 42 | where: { id: currentCount.id }, 43 | data: { count: { [intent]: 1 } }, 44 | }); 45 | res.writeHead(302, { Location: "/" }); 46 | res.end(); 47 | break; 48 | } 49 | case "GET /": { 50 | let currentCount = await getCurrentCount(); 51 | res.setHeader("Content-Type", "text/html"); 52 | res.writeHead(200); 53 | res.end(/* html */ ` 54 | 55 | 56 | Demo App 57 | 58 | 59 |

Demo App

60 |
61 | 62 | ${currentCount.count} 63 | 64 |
65 | 66 | 67 | `); 68 | break; 69 | } 70 | default: { 71 | res.writeHead(404); 72 | return res.end("Not found"); 73 | } 74 | } 75 | }) 76 | .listen(process.env.PORT, () => { 77 | const address = server.address(); 78 | if (!address) { 79 | console.log("Server listening"); 80 | return; 81 | } 82 | const url = 83 | typeof address === "string" 84 | ? address 85 | : `http://localhost:${address.port}`; 86 | console.log(`Server listening at ${url}`); 87 | }); 88 | -------------------------------------------------------------------------------- /steps/03-init-fly/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [experimental] 7 | auto_rollback = true 8 | 9 | [[services]] 10 | http_checks = [] 11 | internal_port = 8080 12 | processes = ["app"] 13 | protocol = "tcp" 14 | script_checks = [] 15 | [services.concurrency] 16 | hard_limit = 25 17 | soft_limit = 20 18 | type = "connections" 19 | 20 | [[services.ports]] 21 | force_https = true 22 | handlers = ["http"] 23 | port = 80 24 | 25 | [[services.ports]] 26 | handlers = ["tls", "http"] 27 | port = 443 28 | 29 | [[services.tcp_checks]] 30 | grace_period = "1s" 31 | interval = "15s" 32 | restart_limit = 0 33 | timeout = "2s" 34 | -------------------------------------------------------------------------------- /steps/03-init-fly/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/03-init-fly/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/03-init-fly/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/03-init-fly/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/03-init-fly/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/03-init-fly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | RUN apt-get update && apt-get install -y openssl sqlite3 6 | 7 | # install all node_modules, including dev 8 | FROM base as deps 9 | 10 | RUN mkdir /app/ 11 | WORKDIR /app/ 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install 15 | 16 | # setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app/ 20 | WORKDIR /app/ 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --omit=dev 25 | 26 | # build app 27 | FROM base as build 28 | 29 | RUN mkdir /app/ 30 | WORKDIR /app/ 31 | 32 | COPY --from=deps /app/node_modules /app/node_modules 33 | 34 | # schema doesn't change much so these will stay cached 35 | ADD prisma /app/prisma 36 | 37 | RUN npx prisma generate 38 | 39 | # app code changes all the time 40 | ADD . . 41 | RUN npm run build 42 | 43 | # build smaller image for running 44 | FROM base 45 | 46 | ENV DATABASE_URL="file:/data/sqlite.db" 47 | ENV PORT="8080" 48 | ENV NODE_ENV="production" 49 | 50 | RUN mkdir /app/ 51 | WORKDIR /app/ 52 | 53 | COPY --from=production-deps /app/node_modules /app/node_modules 54 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 55 | COPY --from=build /app/build /app/build 56 | 57 | ADD . . 58 | 59 | CMD ["npm", "start"] 60 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "POST /": { 35 | const params = await parseFormBody(req); 36 | const intent = params.get("intent"); 37 | const currentCount = await getCurrentCount(); 38 | if (intent !== "increment" && intent !== "decrement") { 39 | return res.end("Invalid intent"); 40 | } 41 | await prisma.count.update({ 42 | where: { id: currentCount.id }, 43 | data: { count: { [intent]: 1 } }, 44 | }); 45 | res.writeHead(302, { Location: "/" }); 46 | res.end(); 47 | break; 48 | } 49 | case "GET /": { 50 | let currentCount = await getCurrentCount(); 51 | res.setHeader("Content-Type", "text/html"); 52 | res.writeHead(200); 53 | res.end(/* html */ ` 54 | 55 | 56 | Demo App 57 | 58 | 59 |

Demo App

60 |
61 | 62 | ${currentCount.count} 63 | 64 |
65 | 66 | 67 | `); 68 | break; 69 | } 70 | default: { 71 | res.writeHead(404); 72 | return res.end("Not found"); 73 | } 74 | } 75 | }) 76 | .listen(process.env.PORT, () => { 77 | const address = server.address(); 78 | if (!address) { 79 | console.log("Server listening"); 80 | return; 81 | } 82 | const url = 83 | typeof address === "string" 84 | ? address 85 | : `http://localhost:${address.port}`; 86 | console.log(`Server listening at ${url}`); 87 | }); 88 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | auto_rollback = true 12 | 13 | [[services]] 14 | internal_port = 8080 15 | processes = ["app"] 16 | protocol = "tcp" 17 | script_checks = [] 18 | [services.concurrency] 19 | hard_limit = 25 20 | soft_limit = 20 21 | type = "connections" 22 | 23 | [[services.ports]] 24 | force_https = true 25 | handlers = ["http"] 26 | port = 80 27 | 28 | [[services.ports]] 29 | handlers = ["tls", "http"] 30 | port = 443 31 | 32 | [[services.tcp_checks]] 33 | grace_period = "1s" 34 | interval = "15s" 35 | restart_limit = 0 36 | timeout = "2s" 37 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/04-persisted-volume/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/04-persisted-volume/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/05-healthcheck/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/05-healthcheck/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/05-healthcheck/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/05-healthcheck/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | RUN apt-get update && apt-get install -y openssl sqlite3 6 | 7 | # install all node_modules, including dev 8 | FROM base as deps 9 | 10 | RUN mkdir /app/ 11 | WORKDIR /app/ 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install 15 | 16 | # setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app/ 20 | WORKDIR /app/ 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --omit=dev 25 | 26 | # build app 27 | FROM base as build 28 | 29 | RUN mkdir /app/ 30 | WORKDIR /app/ 31 | 32 | COPY --from=deps /app/node_modules /app/node_modules 33 | 34 | # schema doesn't change much so these will stay cached 35 | ADD prisma /app/prisma 36 | 37 | RUN npx prisma generate 38 | 39 | # app code changes all the time 40 | ADD . . 41 | RUN npm run build 42 | 43 | # build smaller image for running 44 | FROM base 45 | 46 | ENV DATABASE_URL="file:/data/sqlite.db" 47 | ENV PORT="8080" 48 | ENV NODE_ENV="production" 49 | 50 | RUN mkdir /app/ 51 | WORKDIR /app/ 52 | 53 | COPY --from=production-deps /app/node_modules /app/node_modules 54 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 55 | COPY --from=build /app/build /app/build 56 | 57 | ADD . . 58 | 59 | CMD ["npm", "start"] 60 | -------------------------------------------------------------------------------- /steps/05-healthcheck/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/05-healthcheck/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | auto_rollback = true 12 | 13 | [[services]] 14 | internal_port = 8080 15 | processes = ["app"] 16 | protocol = "tcp" 17 | script_checks = [] 18 | [services.concurrency] 19 | hard_limit = 25 20 | soft_limit = 20 21 | type = "connections" 22 | 23 | [[services.ports]] 24 | force_https = true 25 | handlers = ["http"] 26 | port = 80 27 | 28 | [[services.ports]] 29 | handlers = ["tls", "http"] 30 | port = 443 31 | 32 | [[services.tcp_checks]] 33 | grace_period = "1s" 34 | interval = "15s" 35 | restart_limit = 0 36 | timeout = "2s" 37 | 38 | [[services.http_checks]] 39 | interval = 10000 40 | grace_period = "1s" 41 | method = "get" 42 | path = "/" 43 | protocol = "http" 44 | restart_limit = 0 45 | timeout = 500 46 | tls_skip_verify = false 47 | [services.http_checks.headers] 48 | 49 | [[services.http_checks]] 50 | interval = 10000 51 | grace_period = "1s" 52 | method = "get" 53 | path = "/healthcheck" 54 | protocol = "http" 55 | restart_limit = 0 56 | timeout = 500 57 | tls_skip_verify = false 58 | [services.http_checks.headers] 59 | -------------------------------------------------------------------------------- /steps/05-healthcheck/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/05-healthcheck/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/05-healthcheck/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/05-healthcheck/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/05-healthcheck/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/05-healthcheck/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | RUN apt-get update && apt-get install -y openssl sqlite3 6 | 7 | # install all node_modules, including dev 8 | FROM base as deps 9 | 10 | RUN mkdir /app/ 11 | WORKDIR /app/ 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install 15 | 16 | # setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app/ 20 | WORKDIR /app/ 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --omit=dev 25 | 26 | # build app 27 | FROM base as build 28 | 29 | RUN mkdir /app/ 30 | WORKDIR /app/ 31 | 32 | COPY --from=deps /app/node_modules /app/node_modules 33 | 34 | # schema doesn't change much so these will stay cached 35 | ADD prisma /app/prisma 36 | 37 | RUN npx prisma generate 38 | 39 | # app code changes all the time 40 | ADD . . 41 | RUN npm run build 42 | 43 | # build smaller image for running 44 | FROM base 45 | 46 | ENV DATABASE_URL="file:/data/sqlite.db" 47 | ENV PORT="8080" 48 | ENV NODE_ENV="production" 49 | 50 | # Make SQLite CLI accessible via fly ssh console 51 | # $ fly ssh console -C database-cli 52 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 53 | 54 | RUN mkdir /app/ 55 | WORKDIR /app/ 56 | 57 | COPY --from=production-deps /app/node_modules /app/node_modules 58 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 59 | COPY --from=build /app/build /app/build 60 | 61 | ADD . . 62 | 63 | CMD ["npm", "start"] 64 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | auto_rollback = true 12 | 13 | [[services]] 14 | internal_port = 8080 15 | processes = ["app"] 16 | protocol = "tcp" 17 | script_checks = [] 18 | [services.concurrency] 19 | hard_limit = 25 20 | soft_limit = 20 21 | type = "connections" 22 | 23 | [[services.ports]] 24 | force_https = true 25 | handlers = ["http"] 26 | port = 80 27 | 28 | [[services.ports]] 29 | handlers = ["tls", "http"] 30 | port = 443 31 | 32 | [[services.tcp_checks]] 33 | grace_period = "1s" 34 | interval = "15s" 35 | restart_limit = 0 36 | timeout = "2s" 37 | 38 | [[services.http_checks]] 39 | interval = 10000 40 | grace_period = "1s" 41 | method = "get" 42 | path = "/" 43 | protocol = "http" 44 | restart_limit = 0 45 | timeout = 500 46 | tls_skip_verify = false 47 | [services.http_checks.headers] 48 | 49 | [[services.http_checks]] 50 | interval = 10000 51 | grace_period = "1s" 52 | method = "get" 53 | path = "/healthcheck" 54 | protocol = "http" 55 | restart_limit = 0 56 | timeout = 500 57 | tls_skip_verify = false 58 | [services.http_checks.headers] 59 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/06-ssh-sqlite-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | deploy: 9 | name: 🚀 Deploy 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: ⬇️ Checkout repo 13 | uses: actions/checkout@v3 14 | 15 | - name: 🚀 Deploy Production 16 | uses: superfly/flyctl-actions@1.3 17 | with: 18 | args: "deploy --remote-only" 19 | env: 20 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 21 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | RUN apt-get update && apt-get install -y openssl sqlite3 6 | 7 | # install all node_modules, including dev 8 | FROM base as deps 9 | 10 | RUN mkdir /app/ 11 | WORKDIR /app/ 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install 15 | 16 | # setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app/ 20 | WORKDIR /app/ 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --omit=dev 25 | 26 | # build app 27 | FROM base as build 28 | 29 | RUN mkdir /app/ 30 | WORKDIR /app/ 31 | 32 | COPY --from=deps /app/node_modules /app/node_modules 33 | 34 | # schema doesn't change much so these will stay cached 35 | ADD prisma /app/prisma 36 | 37 | RUN npx prisma generate 38 | 39 | # app code changes all the time 40 | ADD . . 41 | RUN npm run build 42 | 43 | # build smaller image for running 44 | FROM base 45 | 46 | ENV DATABASE_URL="file:/data/sqlite.db" 47 | ENV PORT="8080" 48 | ENV NODE_ENV="production" 49 | 50 | # Make SQLite CLI accessible via fly ssh console 51 | # $ fly ssh console -C database-cli 52 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 53 | 54 | RUN mkdir /app/ 55 | WORKDIR /app/ 56 | 57 | COPY --from=production-deps /app/node_modules /app/node_modules 58 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 59 | COPY --from=build /app/build /app/build 60 | 61 | ADD . . 62 | 63 | CMD ["npm", "start"] 64 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | auto_rollback = true 12 | 13 | [[services]] 14 | internal_port = 8080 15 | processes = ["app"] 16 | protocol = "tcp" 17 | script_checks = [] 18 | [services.concurrency] 19 | hard_limit = 25 20 | soft_limit = 20 21 | type = "connections" 22 | 23 | [[services.ports]] 24 | force_https = true 25 | handlers = ["http"] 26 | port = 80 27 | 28 | [[services.ports]] 29 | handlers = ["tls", "http"] 30 | port = 443 31 | 32 | [[services.tcp_checks]] 33 | grace_period = "1s" 34 | interval = "15s" 35 | restart_limit = 0 36 | timeout = "2s" 37 | 38 | [[services.http_checks]] 39 | interval = 10000 40 | grace_period = "1s" 41 | method = "get" 42 | path = "/" 43 | protocol = "http" 44 | restart_limit = 0 45 | timeout = 500 46 | tls_skip_verify = false 47 | [services.http_checks.headers] 48 | 49 | [[services.http_checks]] 50 | interval = 10000 51 | grace_period = "1s" 52 | method = "get" 53 | path = "/healthcheck" 54 | protocol = "http" 55 | restart_limit = 0 56 | timeout = 500 57 | tls_skip_verify = false 58 | [services.http_checks.headers] 59 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/07-basic-github-deploy-action/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | 8 | jobs: 9 | deploy: 10 | name: 🚀 Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: ⬇️ Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: 👀 Read app name 17 | uses: SebRollen/toml-action@v1.0.2 18 | id: app_name 19 | with: 20 | file: "fly.toml" 21 | field: "app" 22 | 23 | - name: 🚀 Deploy Staging 24 | if: ${{ github.ref == 'refs/heads/dev' }} 25 | uses: superfly/flyctl-actions@1.3 26 | with: 27 | args: 28 | "deploy --app ${{ steps.app_name.outputs.value }}-staging 29 | --remote-only" 30 | env: 31 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 32 | 33 | - name: 🚀 Deploy Production 34 | if: ${{ github.ref == 'refs/heads/main' }} 35 | uses: superfly/flyctl-actions@1.3 36 | with: 37 | args: "deploy --remote-only" 38 | env: 39 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | RUN apt-get update && apt-get install -y openssl sqlite3 6 | 7 | # install all node_modules, including dev 8 | FROM base as deps 9 | 10 | RUN mkdir /app/ 11 | WORKDIR /app/ 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install 15 | 16 | # setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app/ 20 | WORKDIR /app/ 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --omit=dev 25 | 26 | # build app 27 | FROM base as build 28 | 29 | RUN mkdir /app/ 30 | WORKDIR /app/ 31 | 32 | COPY --from=deps /app/node_modules /app/node_modules 33 | 34 | # schema doesn't change much so these will stay cached 35 | ADD prisma /app/prisma 36 | 37 | RUN npx prisma generate 38 | 39 | # app code changes all the time 40 | ADD . . 41 | RUN npm run build 42 | 43 | # build smaller image for running 44 | FROM base 45 | 46 | ENV DATABASE_URL="file:/data/sqlite.db" 47 | ENV PORT="8080" 48 | ENV NODE_ENV="production" 49 | 50 | # Make SQLite CLI accessible via fly ssh console 51 | # $ fly ssh console -C database-cli 52 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 53 | 54 | RUN mkdir /app/ 55 | WORKDIR /app/ 56 | 57 | COPY --from=production-deps /app/node_modules /app/node_modules 58 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 59 | COPY --from=build /app/build /app/build 60 | 61 | ADD . . 62 | 63 | CMD ["npm", "start"] 64 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | auto_rollback = true 12 | 13 | [[services]] 14 | internal_port = 8080 15 | processes = ["app"] 16 | protocol = "tcp" 17 | script_checks = [] 18 | [services.concurrency] 19 | hard_limit = 25 20 | soft_limit = 20 21 | type = "connections" 22 | 23 | [[services.ports]] 24 | force_https = true 25 | handlers = ["http"] 26 | port = 80 27 | 28 | [[services.ports]] 29 | handlers = ["tls", "http"] 30 | port = 443 31 | 32 | [[services.tcp_checks]] 33 | grace_period = "1s" 34 | interval = "15s" 35 | restart_limit = 0 36 | timeout = "2s" 37 | 38 | [[services.http_checks]] 39 | interval = 10000 40 | grace_period = "1s" 41 | method = "get" 42 | path = "/" 43 | protocol = "http" 44 | restart_limit = 0 45 | timeout = 500 46 | tls_skip_verify = false 47 | [services.http_checks.headers] 48 | 49 | [[services.http_checks]] 50 | interval = 10000 51 | grace_period = "1s" 52 | method = "get" 53 | path = "/healthcheck" 54 | protocol = "http" 55 | restart_limit = 0 56 | timeout = 500 57 | tls_skip_verify = false 58 | [services.http_checks.headers] 59 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/08-staging-github-action/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/08-staging-github-action/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/09-litefs/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/09-litefs/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/09-litefs/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | 8 | jobs: 9 | deploy: 10 | name: 🚀 Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: ⬇️ Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: 👀 Read app name 17 | uses: SebRollen/toml-action@v1.0.2 18 | id: app_name 19 | with: 20 | file: "fly.toml" 21 | field: "app" 22 | 23 | - name: 🚀 Deploy Staging 24 | if: ${{ github.ref == 'refs/heads/dev' }} 25 | uses: superfly/flyctl-actions@1.3 26 | with: 27 | args: 28 | "deploy --app ${{ steps.app_name.outputs.value }}-staging 29 | --remote-only" 30 | env: 31 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 32 | 33 | - name: 🚀 Deploy Production 34 | if: ${{ github.ref == 'refs/heads/main' }} 35 | uses: superfly/flyctl-actions@1.3 36 | with: 37 | args: "deploy --remote-only" 38 | env: 39 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /steps/09-litefs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/09-litefs/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | # ca-certificates and fuse3 for litefs 6 | RUN apt-get update && apt-get install -y fuse3 openssl sqlite3 7 | 8 | # install all node_modules, including dev 9 | FROM base as deps 10 | 11 | RUN mkdir /app/ 12 | WORKDIR /app/ 13 | 14 | ADD package.json package-lock.json ./ 15 | RUN npm install 16 | 17 | # setup production node_modules 18 | FROM base as production-deps 19 | 20 | RUN mkdir /app/ 21 | WORKDIR /app/ 22 | 23 | COPY --from=deps /app/node_modules /app/node_modules 24 | ADD package.json package-lock.json ./ 25 | RUN npm prune --omit=dev 26 | 27 | # build app 28 | FROM base as build 29 | 30 | RUN mkdir /app/ 31 | WORKDIR /app/ 32 | 33 | COPY --from=deps /app/node_modules /app/node_modules 34 | 35 | # schema doesn't change much so these will stay cached 36 | ADD prisma /app/prisma 37 | 38 | RUN npx prisma generate 39 | 40 | # app code changes all the time 41 | ADD . . 42 | RUN npm run build 43 | 44 | # build smaller image for running 45 | FROM base 46 | 47 | ENV LITEFS_DIR="/litefs" 48 | ENV DATABASE_URL="file:$LITEFS_DIR/sqlite.db" 49 | ENV PORT="8080" 50 | ENV NODE_ENV="production" 51 | 52 | # Make SQLite CLI accessible via fly ssh console 53 | # $ fly ssh console -C database-cli 54 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 55 | 56 | RUN mkdir /app/ 57 | WORKDIR /app/ 58 | 59 | COPY --from=production-deps /app/node_modules /app/node_modules 60 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 61 | COPY --from=build /app/build /app/build 62 | 63 | ADD . . 64 | 65 | # prepare for litefs 66 | COPY --from=flyio/litefs:0.4.0 /usr/local/bin/litefs /usr/local/bin/litefs 67 | ADD litefs.yml /etc/litefs.yml 68 | 69 | CMD ["litefs", "mount", "--", "npm", "start"] 70 | -------------------------------------------------------------------------------- /steps/09-litefs/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/09-litefs/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | auto_rollback = true 12 | 13 | [[services]] 14 | internal_port = 8080 15 | processes = ["app"] 16 | protocol = "tcp" 17 | script_checks = [] 18 | [services.concurrency] 19 | hard_limit = 25 20 | soft_limit = 20 21 | type = "connections" 22 | 23 | [[services.ports]] 24 | force_https = true 25 | handlers = ["http"] 26 | port = 80 27 | 28 | [[services.ports]] 29 | handlers = ["tls", "http"] 30 | port = 443 31 | 32 | [[services.tcp_checks]] 33 | grace_period = "1s" 34 | interval = "15s" 35 | restart_limit = 0 36 | timeout = "2s" 37 | 38 | [[services.http_checks]] 39 | interval = 10000 40 | grace_period = "1s" 41 | method = "get" 42 | path = "/" 43 | protocol = "http" 44 | restart_limit = 0 45 | timeout = 500 46 | tls_skip_verify = false 47 | [services.http_checks.headers] 48 | 49 | [[services.http_checks]] 50 | interval = 10000 51 | grace_period = "1s" 52 | method = "get" 53 | path = "/healthcheck" 54 | protocol = "http" 55 | restart_limit = 0 56 | timeout = 500 57 | tls_skip_verify = false 58 | [services.http_checks.headers] 59 | -------------------------------------------------------------------------------- /steps/09-litefs/litefs.yml: -------------------------------------------------------------------------------- 1 | # Documented example: https://github.com/superfly/litefs/blob/dec5a7353292068b830001bd2df4830e646f6a2f/cmd/litefs/etc/litefs.yml 2 | fuse: 3 | # Required. This is the mount directory that applications will 4 | # use to access their SQLite databases. 5 | dir: "${LITEFS_DIR}" 6 | 7 | data: 8 | # Path to internal data storage. 9 | dir: "/data/litefs" 10 | 11 | # The lease section specifies how the cluster will be managed. We're using the 12 | # "consul" lease type so that our application can dynamically change the primary. 13 | # 14 | # These environment variables will be available in your Fly.io application. 15 | # You must specify "experiement.enable_consul" for FLY_CONSUL_URL to be available. 16 | lease: 17 | type: "static" 18 | -------------------------------------------------------------------------------- /steps/09-litefs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/09-litefs/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/09-litefs/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/09-litefs/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/09-litefs/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/09-litefs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/10-consul/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/10-consul/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/10-consul/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | 8 | jobs: 9 | deploy: 10 | name: 🚀 Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: ⬇️ Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: 👀 Read app name 17 | uses: SebRollen/toml-action@v1.0.2 18 | id: app_name 19 | with: 20 | file: "fly.toml" 21 | field: "app" 22 | 23 | - name: 🚀 Deploy Staging 24 | if: ${{ github.ref == 'refs/heads/dev' }} 25 | uses: superfly/flyctl-actions@1.3 26 | with: 27 | args: 28 | "deploy --app ${{ steps.app_name.outputs.value }}-staging 29 | --remote-only" 30 | env: 31 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 32 | 33 | - name: 🚀 Deploy Production 34 | if: ${{ github.ref == 'refs/heads/main' }} 35 | uses: superfly/flyctl-actions@1.3 36 | with: 37 | args: "deploy --remote-only" 38 | env: 39 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /steps/10-consul/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/10-consul/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | # ca-certificates and fuse3 for litefs 6 | RUN apt-get update && apt-get install -y fuse3 openssl sqlite3 ca-certificates 7 | 8 | # install all node_modules, including dev 9 | FROM base as deps 10 | 11 | RUN mkdir /app/ 12 | WORKDIR /app/ 13 | 14 | ADD package.json package-lock.json ./ 15 | RUN npm install 16 | 17 | # setup production node_modules 18 | FROM base as production-deps 19 | 20 | RUN mkdir /app/ 21 | WORKDIR /app/ 22 | 23 | COPY --from=deps /app/node_modules /app/node_modules 24 | ADD package.json package-lock.json ./ 25 | RUN npm prune --omit=dev 26 | 27 | # build app 28 | FROM base as build 29 | 30 | RUN mkdir /app/ 31 | WORKDIR /app/ 32 | 33 | COPY --from=deps /app/node_modules /app/node_modules 34 | 35 | # schema doesn't change much so these will stay cached 36 | ADD prisma /app/prisma 37 | 38 | RUN npx prisma generate 39 | 40 | # app code changes all the time 41 | ADD . . 42 | RUN npm run build 43 | 44 | # build smaller image for running 45 | FROM base 46 | 47 | ENV LITEFS_DIR="/litefs" 48 | ENV DATABASE_URL="file:$LITEFS_DIR/sqlite.db" 49 | ENV PORT="8080" 50 | ENV NODE_ENV="production" 51 | 52 | # Make SQLite CLI accessible via fly ssh console 53 | # $ fly ssh console -C database-cli 54 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 55 | 56 | RUN mkdir /app/ 57 | WORKDIR /app/ 58 | 59 | COPY --from=production-deps /app/node_modules /app/node_modules 60 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 61 | COPY --from=build /app/build /app/build 62 | 63 | ADD . . 64 | 65 | # prepare for litefs 66 | COPY --from=flyio/litefs:0.4.0 /usr/local/bin/litefs /usr/local/bin/litefs 67 | ADD litefs.yml /etc/litefs.yml 68 | 69 | CMD ["litefs", "mount", "--", "npm", "start"] 70 | -------------------------------------------------------------------------------- /steps/10-consul/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/10-consul/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | enable_consul = true 12 | auto_rollback = true 13 | 14 | [[services]] 15 | internal_port = 8080 16 | processes = ["app"] 17 | protocol = "tcp" 18 | script_checks = [] 19 | [services.concurrency] 20 | hard_limit = 25 21 | soft_limit = 20 22 | type = "connections" 23 | 24 | [[services.ports]] 25 | force_https = true 26 | handlers = ["http"] 27 | port = 80 28 | 29 | [[services.ports]] 30 | handlers = ["tls", "http"] 31 | port = 443 32 | 33 | [[services.tcp_checks]] 34 | grace_period = "1s" 35 | interval = "15s" 36 | restart_limit = 0 37 | timeout = "2s" 38 | 39 | [[services.http_checks]] 40 | interval = 10000 41 | grace_period = "1s" 42 | method = "get" 43 | path = "/" 44 | protocol = "http" 45 | restart_limit = 0 46 | timeout = 500 47 | tls_skip_verify = false 48 | [services.http_checks.headers] 49 | 50 | [[services.http_checks]] 51 | interval = 10000 52 | grace_period = "1s" 53 | method = "get" 54 | path = "/healthcheck" 55 | protocol = "http" 56 | restart_limit = 0 57 | timeout = 500 58 | tls_skip_verify = false 59 | [services.http_checks.headers] 60 | -------------------------------------------------------------------------------- /steps/10-consul/litefs.yml: -------------------------------------------------------------------------------- 1 | # Documented example: https://github.com/superfly/litefs/blob/dec5a7353292068b830001bd2df4830e646f6a2f/cmd/litefs/etc/litefs.yml 2 | fuse: 3 | # Required. This is the mount directory that applications will 4 | # use to access their SQLite databases. 5 | dir: "${LITEFS_DIR}" 6 | 7 | data: 8 | # Path to internal data storage. 9 | dir: "/data/litefs" 10 | 11 | # The lease section specifies how the cluster will be managed. We're using the 12 | # "consul" lease type so that our application can dynamically change the primary. 13 | # 14 | # These environment variables will be available in your Fly.io application. 15 | # You must specify "experiement.enable_consul" for FLY_CONSUL_URL to be available. 16 | lease: 17 | type: "consul" 18 | candidate: true 19 | advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202" 20 | 21 | consul: 22 | url: "${FLY_CONSUL_URL}" 23 | key: "litefs/${FLY_APP_NAME}" 24 | -------------------------------------------------------------------------------- /steps/10-consul/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /steps/10-consul/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/10-consul/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/10-consul/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/10-consul/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | 6 | async function go() { 7 | await exec("npx prisma migrate deploy"); 8 | 9 | console.log("Starting app..."); 10 | await exec("node ./build"); 11 | } 12 | go(); 13 | 14 | async function exec(command) { 15 | const child = spawn(command, { shell: true, stdio: "inherit" }); 16 | await new Promise((res, rej) => { 17 | child.on("exit", (code) => { 18 | if (code === 0) { 19 | res(); 20 | } else { 21 | rej(); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /steps/10-consul/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/11-multi-region/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/11-multi-region/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/11-multi-region/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | 8 | jobs: 9 | deploy: 10 | name: 🚀 Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: ⬇️ Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: 👀 Read app name 17 | uses: SebRollen/toml-action@v1.0.2 18 | id: app_name 19 | with: 20 | file: "fly.toml" 21 | field: "app" 22 | 23 | - name: 🚀 Deploy Staging 24 | if: ${{ github.ref == 'refs/heads/dev' }} 25 | uses: superfly/flyctl-actions@1.3 26 | with: 27 | args: 28 | "deploy --app ${{ steps.app_name.outputs.value }}-staging 29 | --remote-only" 30 | env: 31 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 32 | 33 | - name: 🚀 Deploy Production 34 | if: ${{ github.ref == 'refs/heads/main' }} 35 | uses: superfly/flyctl-actions@1.3 36 | with: 37 | args: "deploy --remote-only" 38 | env: 39 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /steps/11-multi-region/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/11-multi-region/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | # ca-certificates and fuse3 for litefs 6 | RUN apt-get update && apt-get install -y fuse3 openssl sqlite3 ca-certificates 7 | 8 | # install all node_modules, including dev 9 | FROM base as deps 10 | 11 | RUN mkdir /app/ 12 | WORKDIR /app/ 13 | 14 | ADD package.json package-lock.json ./ 15 | RUN npm install 16 | 17 | # setup production node_modules 18 | FROM base as production-deps 19 | 20 | RUN mkdir /app/ 21 | WORKDIR /app/ 22 | 23 | COPY --from=deps /app/node_modules /app/node_modules 24 | ADD package.json package-lock.json ./ 25 | RUN npm prune --omit=dev 26 | 27 | # build app 28 | FROM base as build 29 | 30 | RUN mkdir /app/ 31 | WORKDIR /app/ 32 | 33 | COPY --from=deps /app/node_modules /app/node_modules 34 | 35 | # schema doesn't change much so these will stay cached 36 | ADD prisma /app/prisma 37 | 38 | RUN npx prisma generate 39 | 40 | # app code changes all the time 41 | ADD . . 42 | RUN npm run build 43 | 44 | # build smaller image for running 45 | FROM base 46 | 47 | ENV LITEFS_DIR="/litefs" 48 | ENV DATABASE_URL="file:$LITEFS_DIR/sqlite.db" 49 | ENV PORT="8080" 50 | ENV NODE_ENV="production" 51 | 52 | # Make SQLite CLI accessible via fly ssh console 53 | # $ fly ssh console -C database-cli 54 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 55 | 56 | RUN mkdir /app/ 57 | WORKDIR /app/ 58 | 59 | COPY --from=production-deps /app/node_modules /app/node_modules 60 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 61 | COPY --from=build /app/build /app/build 62 | 63 | ADD . . 64 | 65 | # prepare for litefs 66 | COPY --from=flyio/litefs:0.4.0 /usr/local/bin/litefs /usr/local/bin/litefs 67 | ADD litefs.yml /etc/litefs.yml 68 | 69 | CMD ["litefs", "mount", "--", "npm", "start"] 70 | -------------------------------------------------------------------------------- /steps/11-multi-region/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/11-multi-region/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | enable_consul = true 12 | auto_rollback = true 13 | 14 | [[services]] 15 | internal_port = 8080 16 | processes = ["app"] 17 | protocol = "tcp" 18 | script_checks = [] 19 | [services.concurrency] 20 | hard_limit = 25 21 | soft_limit = 20 22 | type = "connections" 23 | 24 | [[services.ports]] 25 | force_https = true 26 | handlers = ["http"] 27 | port = 80 28 | 29 | [[services.ports]] 30 | handlers = ["tls", "http"] 31 | port = 443 32 | 33 | [[services.tcp_checks]] 34 | grace_period = "1s" 35 | interval = "15s" 36 | restart_limit = 0 37 | timeout = "2s" 38 | 39 | [[services.http_checks]] 40 | interval = 10000 41 | grace_period = "1s" 42 | method = "get" 43 | path = "/" 44 | protocol = "http" 45 | restart_limit = 0 46 | timeout = 500 47 | tls_skip_verify = false 48 | [services.http_checks.headers] 49 | 50 | [[services.http_checks]] 51 | interval = 10000 52 | grace_period = "1s" 53 | method = "get" 54 | path = "/healthcheck" 55 | protocol = "http" 56 | restart_limit = 0 57 | timeout = 500 58 | tls_skip_verify = false 59 | [services.http_checks.headers] 60 | -------------------------------------------------------------------------------- /steps/11-multi-region/litefs.yml: -------------------------------------------------------------------------------- 1 | # Documented example: https://github.com/superfly/litefs/blob/dec5a7353292068b830001bd2df4830e646f6a2f/cmd/litefs/etc/litefs.yml 2 | fuse: 3 | # Required. This is the mount directory that applications will 4 | # use to access their SQLite databases. 5 | dir: "${LITEFS_DIR}" 6 | 7 | data: 8 | # Path to internal data storage. 9 | dir: "/data/litefs" 10 | 11 | # The lease section specifies how the cluster will be managed. We're using the 12 | # "consul" lease type so that our application can dynamically change the primary. 13 | # 14 | # These environment variables will be available in your Fly.io application. 15 | # You must specify "experiement.enable_consul" for FLY_CONSUL_URL to be available. 16 | lease: 17 | type: "consul" 18 | candidate: ${FLY_REGION == 'sjc'} 19 | advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202" 20 | 21 | consul: 22 | url: "${FLY_CONSUL_URL}" 23 | key: "litefs/${FLY_APP_NAME}" 24 | -------------------------------------------------------------------------------- /steps/11-multi-region/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "litefs-js": "^1.1.2", 27 | "prisma": "^4.9.0", 28 | "tiny-invariant": "^1.3.1", 29 | "typescript": "^4.9.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /steps/11-multi-region/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/11-multi-region/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/11-multi-region/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/11-multi-region/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | const { getInstanceInfo } = require("litefs-js"); 6 | 7 | async function go() { 8 | const { currentInstance, currentIsPrimary, primaryInstance } = 9 | await getInstanceInfo(); 10 | 11 | if (currentIsPrimary) { 12 | console.log( 13 | `Instance (${currentInstance}) in ${process.env.FLY_REGION} is primary. Deploying migrations.` 14 | ); 15 | await exec("npx prisma migrate deploy"); 16 | } else { 17 | console.log( 18 | `Instance (${currentInstance}) in ${process.env.FLY_REGION} is not primary (the primary instance is ${primaryInstance}). Skipping migrations.` 19 | ); 20 | } 21 | 22 | console.log("Starting app..."); 23 | await exec("node ./build"); 24 | } 25 | go(); 26 | 27 | async function exec(command) { 28 | const child = spawn(command, { shell: true, stdio: "inherit" }); 29 | await new Promise((res, rej) => { 30 | child.on("exit", (code) => { 31 | if (code === 0) { 32 | res(); 33 | } else { 34 | rej(); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /steps/11-multi-region/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | Dockerfile 8 | .dockerignore 9 | .git 10 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/.env.example: -------------------------------------------------------------------------------- 1 | LITEFS_DIR="./prisma" 2 | DATABASE_FILENAME="sqlite.db" 3 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 4 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | 8 | jobs: 9 | deploy: 10 | name: 🚀 Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: ⬇️ Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: 👀 Read app name 17 | uses: SebRollen/toml-action@v1.0.2 18 | id: app_name 19 | with: 20 | file: "fly.toml" 21 | field: "app" 22 | 23 | - name: 🚀 Deploy Staging 24 | if: ${{ github.ref == 'refs/heads/dev' }} 25 | uses: superfly/flyctl-actions@1.3 26 | with: 27 | args: 28 | "deploy --app ${{ steps.app_name.outputs.value }}-staging 29 | --remote-only" 30 | env: 31 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 32 | 33 | - name: 🚀 Deploy Production 34 | if: ${{ github.ref == 'refs/heads/main' }} 35 | uses: superfly/flyctl-actions@1.3 36 | with: 37 | args: "deploy --remote-only" 38 | env: 39 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:18-bullseye-slim as base 3 | 4 | # install openssl and sqlite3 for prisma 5 | # ca-certificates and fuse3 for litefs 6 | RUN apt-get update && apt-get install -y fuse3 openssl sqlite3 ca-certificates 7 | 8 | # install all node_modules, including dev 9 | FROM base as deps 10 | 11 | RUN mkdir /app/ 12 | WORKDIR /app/ 13 | 14 | ADD package.json package-lock.json ./ 15 | RUN npm install 16 | 17 | # setup production node_modules 18 | FROM base as production-deps 19 | 20 | RUN mkdir /app/ 21 | WORKDIR /app/ 22 | 23 | COPY --from=deps /app/node_modules /app/node_modules 24 | ADD package.json package-lock.json ./ 25 | RUN npm prune --omit=dev 26 | 27 | # build app 28 | FROM base as build 29 | 30 | RUN mkdir /app/ 31 | WORKDIR /app/ 32 | 33 | COPY --from=deps /app/node_modules /app/node_modules 34 | 35 | # schema doesn't change much so these will stay cached 36 | ADD prisma /app/prisma 37 | 38 | RUN npx prisma generate 39 | 40 | # app code changes all the time 41 | ADD . . 42 | RUN npm run build 43 | 44 | # build smaller image for running 45 | FROM base 46 | 47 | ENV LITEFS_DIR="/litefs" 48 | ENV DATABASE_FILENAME="$LITEFS_DIR/sqlite.db" 49 | ENV DATABASE_URL="file:$DATABASE_FILENAME" 50 | ENV INTERNAL_PORT="8080" 51 | ENV PORT="8081" 52 | ENV NODE_ENV="production" 53 | 54 | # Make SQLite CLI accessible via fly ssh console 55 | # $ fly ssh console -C database-cli 56 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 57 | 58 | RUN mkdir /app/ 59 | WORKDIR /app/ 60 | 61 | COPY --from=production-deps /app/node_modules /app/node_modules 62 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 63 | COPY --from=build /app/build /app/build 64 | 65 | ADD . . 66 | 67 | # prepare for litefs 68 | COPY --from=flyio/litefs:0.4.0 /usr/local/bin/litefs /usr/local/bin/litefs 69 | ADD litefs.yml /etc/litefs.yml 70 | 71 | CMD ["litefs", "mount", "--", "npm", "start"] 72 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/app/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import { PrismaClient } from "@prisma/client"; 3 | const prisma = new PrismaClient(); 4 | 5 | async function getCurrentCount() { 6 | let currentCount = await prisma.count.findFirst(); 7 | if (!currentCount) { 8 | currentCount = await prisma.count.create({ 9 | data: { count: 0 }, 10 | }); 11 | } 12 | return currentCount; 13 | } 14 | 15 | async function parseFormBody(req: http.IncomingMessage) { 16 | const body = await new Promise((resolve) => { 17 | let body = ""; 18 | req.on("data", (chunk) => { 19 | body += chunk; 20 | }); 21 | req.on("end", () => { 22 | resolve(body); 23 | }); 24 | }); 25 | const params = new URLSearchParams(body); 26 | return params; 27 | } 28 | 29 | const server = http 30 | .createServer(async (req, res) => { 31 | console.log(`${req.method} ${req.url}`); 32 | 33 | switch (`${req.method} ${req.url}`) { 34 | case "GET /healthcheck": { 35 | try { 36 | await getCurrentCount(); 37 | res.writeHead(200); 38 | res.end("OK"); 39 | } catch (error: unknown) { 40 | console.error(error); 41 | res.writeHead(500); 42 | res.end("ERROR"); 43 | } 44 | break; 45 | } 46 | case "POST /": { 47 | const params = await parseFormBody(req); 48 | const intent = params.get("intent"); 49 | const currentCount = await getCurrentCount(); 50 | if (intent !== "increment" && intent !== "decrement") { 51 | return res.end("Invalid intent"); 52 | } 53 | await prisma.count.update({ 54 | where: { id: currentCount.id }, 55 | data: { count: { [intent]: 1 } }, 56 | }); 57 | res.writeHead(302, { Location: "/" }); 58 | res.end(); 59 | break; 60 | } 61 | case "GET /": { 62 | let currentCount = await getCurrentCount(); 63 | res.setHeader("Content-Type", "text/html"); 64 | res.writeHead(200); 65 | res.end(/* html */ ` 66 | 67 | 68 | Demo App 69 | 70 | 71 |

Demo App

72 |
73 | 74 | ${currentCount.count} 75 | 76 |
77 | 78 | 79 | `); 80 | break; 81 | } 82 | default: { 83 | res.writeHead(404); 84 | return res.end("Not found"); 85 | } 86 | } 87 | }) 88 | .listen(process.env.PORT, () => { 89 | const address = server.address(); 90 | if (!address) { 91 | console.log("Server listening"); 92 | return; 93 | } 94 | const url = 95 | typeof address === "string" 96 | ? address 97 | : `http://localhost:${address.port}`; 98 | console.log(`Server listening at ${url}`); 99 | }); 100 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/fly.toml: -------------------------------------------------------------------------------- 1 | app = "node-sqlite-fly-tutorial" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [mounts] 7 | source = "data" 8 | destination = "/data" 9 | 10 | [experimental] 11 | enable_consul = true 12 | auto_rollback = true 13 | 14 | [[services]] 15 | internal_port = 8080 16 | processes = ["app"] 17 | protocol = "tcp" 18 | script_checks = [] 19 | [services.concurrency] 20 | hard_limit = 25 21 | soft_limit = 20 22 | type = "connections" 23 | 24 | [[services.ports]] 25 | force_https = true 26 | handlers = ["http"] 27 | port = 80 28 | 29 | [[services.ports]] 30 | handlers = ["tls", "http"] 31 | port = 443 32 | 33 | [[services.tcp_checks]] 34 | grace_period = "1s" 35 | interval = "15s" 36 | restart_limit = 0 37 | timeout = "2s" 38 | 39 | [[services.http_checks]] 40 | interval = 10000 41 | grace_period = "1s" 42 | method = "get" 43 | path = "/" 44 | protocol = "http" 45 | restart_limit = 0 46 | timeout = 500 47 | tls_skip_verify = false 48 | [services.http_checks.headers] 49 | 50 | [[services.http_checks]] 51 | interval = 10000 52 | grace_period = "1s" 53 | method = "get" 54 | path = "/healthcheck" 55 | protocol = "http" 56 | restart_limit = 0 57 | timeout = 500 58 | tls_skip_verify = false 59 | [services.http_checks.headers] 60 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/litefs.yml: -------------------------------------------------------------------------------- 1 | # Documented example: https://github.com/superfly/litefs/blob/dec5a7353292068b830001bd2df4830e646f6a2f/cmd/litefs/etc/litefs.yml 2 | fuse: 3 | # Required. This is the mount directory that applications will 4 | # use to access their SQLite databases. 5 | dir: "${LITEFS_DIR}" 6 | 7 | data: 8 | # Path to internal data storage. 9 | dir: "/data/litefs" 10 | 11 | proxy: 12 | # matches the internal_port in fly.toml 13 | addr: ":${INTERNAL_PORT}" 14 | target: "localhost:${PORT}" 15 | db: "${DATABASE_FILENAME}" 16 | 17 | # The lease section specifies how the cluster will be managed. We're using the 18 | # "consul" lease type so that our application can dynamically change the primary. 19 | # 20 | # These environment variables will be available in your Fly.io application. 21 | # You must specify "experiement.enable_consul" for FLY_CONSUL_URL to be available. 22 | lease: 23 | type: "consul" 24 | candidate: ${FLY_REGION == 'sjc'} 25 | advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202" 26 | 27 | consul: 28 | url: "${FLY_CONSUL_URL}" 29 | key: "litefs/${FLY_APP_NAME}" 30 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sqlite-fly-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./start.js", 8 | "setup": "npm install && cpy .env.example . --rename=.env && prisma migrate reset --force", 9 | "build": "tsc", 10 | "dev": "cross-env PORT=3333 tsx watch ./app" 11 | }, 12 | "keywords": [], 13 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/chokidar": "^2.1.3", 17 | "@types/cookie": "^0.5.1", 18 | "@types/node": "^18.11.18", 19 | "cpy-cli": "^4.2.0", 20 | "cross-env": "^7.0.3", 21 | "tsx": "^3.12.2" 22 | }, 23 | "dependencies": { 24 | "@prisma/client": "^4.9.0", 25 | "cookie": "^0.5.0", 26 | "litefs-js": "^1.1.2", 27 | "prisma": "^4.9.0", 28 | "tiny-invariant": "^1.3.1", 29 | "typescript": "^4.9.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/prisma/migrations/20230118213146_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Count" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "count" INTEGER NOT NULL DEFAULT 0 5 | ); 6 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /steps/12-transactional-consistency/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Count { 14 | id String @id @default(uuid()) 15 | count Int @default(0) 16 | } 17 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/start.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | const { getInstanceInfo } = require("litefs-js"); 6 | 7 | async function go() { 8 | const { currentInstance, currentIsPrimary, primaryInstance } = 9 | await getInstanceInfo(); 10 | 11 | if (currentIsPrimary) { 12 | console.log( 13 | `Instance (${currentInstance}) in ${process.env.FLY_REGION} is primary. Deploying migrations.` 14 | ); 15 | await exec("npx prisma migrate deploy"); 16 | } else { 17 | console.log( 18 | `Instance (${currentInstance}) in ${process.env.FLY_REGION} is not primary (the primary instance is ${primaryInstance}). Skipping migrations.` 19 | ); 20 | } 21 | 22 | console.log("Starting app..."); 23 | await exec("node ./build"); 24 | } 25 | go(); 26 | 27 | async function exec(command) { 28 | const child = spawn(command, { shell: true, stdio: "inherit" }); 29 | await new Promise((res, rej) => { 30 | child.on("exit", (code) => { 31 | if (code === 0) { 32 | res(); 33 | } else { 34 | rej(); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /steps/12-transactional-consistency/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | "skipLibCheck": true, 19 | "outDir": "./build" 20 | } 21 | } 22 | --------------------------------------------------------------------------------