├── .env.example ├── .gitignore ├── .dockerignore ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20230118213146_init │ │ └── migration.sql └── schema.prisma ├── litefs.yml ├── tsconfig.json ├── package.json ├── .github └── workflows │ └── deploy.yml ├── start.js ├── fly.toml ├── Dockerfile └── app └── index.ts /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./sqlite.db?connection_limit=1" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | /.vscode 5 | prisma/sqlite.db 6 | prisma/sqlite.db-journal 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /litefs.yml: -------------------------------------------------------------------------------- 1 | fuse: 2 | dir: "${LITEFS_DIR}" 3 | 4 | data: 5 | dir: "/data/litefs" 6 | 7 | proxy: 8 | addr: ":${INTERNAL_PORT}" 9 | target: "localhost:${PORT}" 10 | db: "${DATABASE_FILENAME}" 11 | 12 | lease: 13 | type: "consul" 14 | candidate: ${FLY_REGION == 'sjc'} 15 | advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202" 16 | 17 | consul: 18 | url: "${FLY_CONSUL_URL}" 19 | key: "litefs/${FLY_APP_NAME}" 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 && 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 | "cross-env": "^7.0.3", 20 | "tsx": "^3.12.2" 21 | }, 22 | "dependencies": { 23 | "@prisma/client": "^4.9.0", 24 | "cookie": "^0.5.0", 25 | "litefs-js": "^1.1.2", 26 | "prisma": "^4.9.0", 27 | "tiny-invariant": "^1.3.1", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.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 --remote-only --app ${{ 29 | steps.app_name.outputs.value}}-staging" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | app = "weathered-morning-92" 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 | enable_consul = 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 | -------------------------------------------------------------------------------- /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 fuse3 ca-certificates 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 LITEFS_DIR="/litefs" 47 | ENV DATABASE_FILENAME="$LITEFS_DIR/sqlite.db" 48 | ENV DATABASE_URL="file:$DATABASE_FILENAME" 49 | ENV INTERNAL_PORT="8080" 50 | ENV PORT="8081" 51 | ENV NODE_ENV="production" 52 | 53 | # Make SQLite CLI accessible via fly ssh console 54 | # $ fly ssh console -C database-cli 55 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 56 | 57 | RUN mkdir /app/ 58 | WORKDIR /app/ 59 | 60 | COPY --from=production-deps /app/node_modules /app/node_modules 61 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 62 | COPY --from=build /app/build /app/build 63 | 64 | ADD . . 65 | 66 | COPY --from=flyio/litefs:sha-8e7b332 /usr/local/bin/litefs /usr/local/bin/litefs 67 | ADD litefs.yml /etc/litefs.yml 68 | 69 | CMD ["litefs", "mount", "--", "npm", "start"] 70 | -------------------------------------------------------------------------------- /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 | const { PORT } = process.env; 16 | 17 | async function parseFormBody(req: http.IncomingMessage) { 18 | const body = await new Promise((resolve) => { 19 | let body = ""; 20 | req.on("data", (chunk) => { 21 | body += chunk; 22 | }); 23 | req.on("end", () => { 24 | resolve(body); 25 | }); 26 | }); 27 | const params = new URLSearchParams(body); 28 | return params; 29 | } 30 | 31 | const server = http 32 | .createServer(async (req, res) => { 33 | console.log(`${req.method} ${req.url}`); 34 | 35 | switch (`${req.method} ${req.url}`) { 36 | case "GET /healthcheck": { 37 | try { 38 | await getCurrentCount(); 39 | res.writeHead(200); 40 | res.end("OK"); 41 | } catch (error: unknown) { 42 | console.error(error); 43 | res.writeHead(500); 44 | res.end("ERROR"); 45 | } 46 | break; 47 | } 48 | case "POST /": { 49 | const params = await parseFormBody(req); 50 | const intent = params.get("intent"); 51 | const currentCount = await getCurrentCount(); 52 | if (intent !== "increment" && intent !== "decrement") { 53 | return res.end("Invalid intent"); 54 | } 55 | await prisma.count.update({ 56 | where: { id: currentCount.id }, 57 | data: { count: { [intent]: 1 } }, 58 | }); 59 | res.writeHead(302, { Location: "/" }); 60 | res.end(); 61 | break; 62 | } 63 | case "GET /": { 64 | let currentCount = await getCurrentCount(); 65 | res.setHeader("Content-Type", "text/html"); 66 | res.writeHead(200); 67 | res.end(/* html */ ` 68 | 69 | 70 | Demo App 71 | 72 | 73 |

Demo App

74 |
75 | 76 | ${currentCount.count} 77 | 78 |
79 | 80 | 81 | `); 82 | break; 83 | } 84 | default: { 85 | res.writeHead(404); 86 | return res.end("Not found"); 87 | } 88 | } 89 | }) 90 | .listen(PORT, () => { 91 | const address = server.address(); 92 | if (!address) { 93 | console.log("Server listening"); 94 | return; 95 | } 96 | const url = 97 | typeof address === "string" 98 | ? address 99 | : `http://localhost:${address.port}`; 100 | console.log(`Server listening at ${url}`); 101 | }); 102 | --------------------------------------------------------------------------------