├── .env.example ├── etc ├── litestream.primary.yml └── litestream.replica.yml ├── .dockerignore ├── .gitignore ├── public └── favicon.ico ├── remix.env.d.ts ├── app ├── prisma.ts ├── entry.client.tsx ├── root.tsx ├── routes │ └── index.tsx └── entry.server.tsx ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20220222090753_initial │ │ └── migration.sql └── schema.prisma ├── remix.config.js ├── start_with_migrations.sh ├── tsconfig.json ├── setup-litestream.js ├── fly.toml ├── package.json ├── server.ts ├── README.md ├── Dockerfile └── .github └── workflows └── deploy.production.yml /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./db.sqlite" 2 | -------------------------------------------------------------------------------- /etc/litestream.primary.yml: -------------------------------------------------------------------------------- 1 | addr: ":9090" 2 | 3 | dbs: 4 | - path: /data/db 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .cache 3 | /build 4 | /public/build 5 | node_modules 6 | 7 | /prisma/db.sqlite* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .cache 3 | /build 4 | /public/build 5 | node_modules 6 | 7 | /prisma/db.sqlite* -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/litestream-remix/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /app/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const client = new PrismaClient(); 4 | 5 | export default client; 6 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /etc/litestream.replica.yml: -------------------------------------------------------------------------------- 1 | addr: ":9090" 2 | 3 | dbs: 4 | - path: /data/db 5 | upstream: 6 | path: /data/db 7 | url: http://${FLY_PRIMARY_REGION}.${FLY_APP_NAME}.internal:9090 8 | -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev').AppConfig} 3 | */ 4 | module.exports = { 5 | server: "./server.ts", 6 | ignoredRouteFiles: [".*"], 7 | devServerBroadcastDelay: 1000, 8 | }; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20220222090753_initial/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Post" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "title" TEXT NOT NULL, 5 | "body" TEXT NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /start_with_migrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | npx prisma migrate deploy 6 | 7 | node ./setup-litestream.js 8 | 9 | # npm run start 10 | # Start litestream and the main application 11 | litestream replicate -exec "npm run start" -------------------------------------------------------------------------------- /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 | datasource db { 5 | provider = "sqlite" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model Post { 14 | id Int @id @default(autoincrement()) 15 | title String 16 | body String 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | 17 | // Remix takes care of building everything in `remix build`. 18 | "noEmit": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | LiveReload, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "remix"; 9 | import type { MetaFunction } from "remix"; 10 | 11 | export const meta: MetaFunction = () => { 12 | return { title: "New Remix App" }; 13 | }; 14 | 15 | export default function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /setup-litestream.js: -------------------------------------------------------------------------------- 1 | let fs = require("fs"); 2 | 3 | let { PrismaClient } = require("@prisma/client"); 4 | 5 | let client = new PrismaClient(); 6 | 7 | if (process.env.FLY_REGION === process.env.FLY_PRIMARY_REGION) { 8 | console.log("COPYING PRIMARY LITESTREAM CONFIG"); 9 | fs.copyFileSync("/etc/litestream.primary.yml", "/etc/litestream.yml"); 10 | } else { 11 | console.log("COPYING REPLICA LITESTREAM CONFIG"); 12 | fs.copyFileSync("/etc/litestream.replica.yml", "/etc/litestream.yml"); 13 | } 14 | 15 | client.$queryRaw`PRAGMA journal_mode = WAL;` 16 | .then(() => { 17 | console.log("ENABLED WAL MODE FOR DATABASE"); 18 | }) 19 | .catch((err) => { 20 | console.log("DB SETUP FAILED", err); 21 | process.exit(1); 22 | }); 23 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for remix-litestream-test on 2022-02-23T00:27:48+07:00 2 | 3 | app = "remix-litestream-test" 4 | 5 | kill_signal = "SIGINT" 6 | kill_timeout = 5 7 | processes = [] 8 | 9 | [env] 10 | DATABASE_URL = "file:/data/db" 11 | FLY_PRIMARY_REGION = "ord" 12 | PORT = 8080 13 | 14 | [mounts] 15 | destination = "/data" 16 | source = "data" 17 | 18 | [experimental] 19 | allowed_public_ports = [] 20 | auto_rollback = true 21 | 22 | [[services]] 23 | http_checks = [] 24 | internal_port = 8080 25 | processes = ["app"] 26 | protocol = "tcp" 27 | script_checks = [] 28 | 29 | [services.concurrency] 30 | hard_limit = 25 31 | soft_limit = 20 32 | type = "connections" 33 | 34 | [[services.ports]] 35 | handlers = ["http"] 36 | port = 80 37 | 38 | [[services.ports]] 39 | handlers = ["tls", "http"] 40 | port = 443 41 | 42 | [[services.tcp_checks]] 43 | grace_period = "1s" 44 | interval = "15s" 45 | restart_limit = 0 46 | timeout = "2s" 47 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { redirect, useLoaderData } from "remix"; 2 | import type { ActionFunction, HeadersFunction } from "remix"; 3 | import prisma from "~/prisma"; 4 | 5 | export const action: ActionFunction = async ({ request }) => { 6 | let count = (await prisma.post.count()) + 1; 7 | await prisma.post.create({ 8 | data: { 9 | title: `Post ${count}`, 10 | body: `Post body ${count}`, 11 | }, 12 | }); 13 | 14 | return redirect("/"); 15 | }; 16 | 17 | export const headers: HeadersFunction = ({ actionHeaders }) => { 18 | return actionHeaders; 19 | }; 20 | 21 | export const loader = async () => { 22 | return await prisma.post.count(); 23 | }; 24 | 25 | export default function Index() { 26 | let count = useLoaderData(); 27 | 28 | return ( 29 |
30 |

{count} Posts

31 |
32 | 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from "react-dom/server"; 2 | import { RemixServer } from "remix"; 3 | import type { EntryContext, HandleDataRequestFunction } from "remix"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | const markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set("Content-Type", "text/html"); 16 | if (process.env.FLY_REGION) { 17 | responseHeaders.set("X-Fly-Region", process.env.FLY_REGION); 18 | } 19 | 20 | return new Response("" + markup, { 21 | status: responseStatusCode, 22 | headers: responseHeaders, 23 | }); 24 | } 25 | 26 | export const handleDataRequest: HandleDataRequestFunction = async ( 27 | response: Response 28 | ) => { 29 | if (process.env.FLY_REGION) { 30 | response.headers.set("X-Fly-Region", process.env.FLY_REGION); 31 | } 32 | 33 | return response; 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-app-template", 3 | "private": true, 4 | "description": "", 5 | "license": "", 6 | "sideEffects": false, 7 | "scripts": { 8 | "postinstall": "npx prisma generate && remix setup node", 9 | "build": "cross-env NODE_ENV=production remix build", 10 | "dev": "cross-env NODE_ENV=development remix build && run-p dev:*", 11 | "dev:node": "cross-env NODE_ENV=development nodemon ./build/index.js", 12 | "dev:remix": "cross-env NODE_ENV=development remix watch", 13 | "start": "node ./build/index.js" 14 | }, 15 | "dependencies": { 16 | "@prisma/client": "^3.9.2", 17 | "@remix-run/express": "^1.2.2", 18 | "@remix-run/react": "^1.2.2", 19 | "compression": "^1.7.4", 20 | "express": "^4.17.3", 21 | "morgan": "^1.10.0", 22 | "prisma": "^3.9.2", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "remix": "^1.2.2" 26 | }, 27 | "devDependencies": { 28 | "@remix-run/dev": "^1.2.2", 29 | "@types/morgan": "^1.9.3", 30 | "@types/react": "^17.0.24", 31 | "@types/react-dom": "^17.0.9", 32 | "cross-env": "^7.0.3", 33 | "nodemon": "^2.0.15", 34 | "npm-run-all": "^4.1.5", 35 | "typescript": "^4.5.5" 36 | }, 37 | "engines": { 38 | "node": ">=14" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import compression from "compression"; 3 | import morgan from "morgan"; 4 | import { createRequestHandler } from "@remix-run/express"; 5 | 6 | import * as serverBuild from "@remix-run/dev/server-build"; 7 | 8 | const app = express(); 9 | 10 | app.use(compression()); 11 | 12 | app.disable("x-powered-by"); 13 | 14 | app.use( 15 | "/build", 16 | express.static("public/build", { immutable: true, maxAge: "1y" }) 17 | ); 18 | app.use(express.static("public", { maxAge: "1h" })); 19 | 20 | app.use(morgan("tiny")); 21 | 22 | app.post("*", (_, res, next) => { 23 | if ( 24 | process.env.FLY_REGION && 25 | process.env.FLY_PRIMARY_REGION && 26 | process.env.FLY_REGION !== process.env.FLY_PRIMARY_REGION 27 | ) { 28 | res 29 | .status(202) 30 | .setHeader("fly-replay", `region=${process.env.FLY_PRIMARY_REGION}`) 31 | .end(`rewriting to ${process.env.FLY_PRIMARY_REGION}`); 32 | } else { 33 | next(); 34 | } 35 | }); 36 | 37 | app.all( 38 | "*", 39 | createRequestHandler({ 40 | build: serverBuild, 41 | mode: process.env.NODE_ENV, 42 | }) 43 | ); 44 | 45 | const port = process.env.PORT || 3000; 46 | 47 | app.listen(port, () => { 48 | console.log(`Express server listening on port ${port}`); 49 | }); 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Fly Setup 6 | 7 | 1. [Install `flyctl`](https://fly.io/docs/getting-started/installing-flyctl/) 8 | 9 | 2. Sign up and log in to Fly 10 | 11 | ```sh 12 | flyctl auth signup 13 | ``` 14 | 15 | 3. Create Fly application. We don't want to deploy yet as we have to add volumes for SQLite. 16 | 17 | ```sh 18 | flyctl launch --region ord --no-deploy 19 | ``` 20 | 21 | 4. Add volumes to your app in two regions 22 | 23 | ```sh 24 | flyctl volumes create --region ord --size 1 data 25 | flyctl volumes create --region hkg --size 1 data 26 | ``` 27 | 28 | 5. Scale your app to match the number of volumes you have added 29 | 30 | ```sh 31 | flyctl scale count 2 32 | ``` 33 | 34 | 6. Deploy your app from the CLI the first time 35 | 36 | ```sh 37 | flyctl deploy --remote-only 38 | ``` 39 | 40 | ## Development 41 | 42 | From your terminal: 43 | 44 | ```sh 45 | npm run dev 46 | ``` 47 | 48 | This starts your app in development mode, rebuilding assets on file changes. 49 | 50 | ## Deployment 51 | 52 | If you've followed the setup instructions already, all you need to do is run this: 53 | 54 | ```sh 55 | npm run deploy 56 | ``` 57 | 58 | You can run `flyctl info` to get the url and ip address of your server. 59 | 60 | Check out the [fly docs](https://fly.io/docs/getting-started/node/) for more information. 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.7-bullseye as litestream-builder 2 | 3 | # set gopath for easy reference later 4 | ENV GOPATH=/go 5 | 6 | # install wget and unzip to download and extract litestream source 7 | RUN apt-get update && apt-get install -y wget unzip 8 | 9 | # download and extract litestream source 10 | RUN wget https://github.com/benbjohnson/litestream/archive/refs/heads/main.zip 11 | RUN unzip ./main.zip -d /src 12 | 13 | # set working dir to litestream source 14 | WORKDIR /src/litestream-main 15 | 16 | # build and install litestream binary 17 | RUN go install ./cmd/litestream 18 | 19 | # base node image 20 | FROM node:16-bullseye-slim as base 21 | 22 | # install openssl for Prisma 23 | RUN apt-get update && apt-get install -y openssl 24 | 25 | FROM base as deps 26 | 27 | # create dir for app and set working dir 28 | RUN mkdir /app 29 | WORKDIR /app 30 | 31 | # install all node_modules, including dev dependencies 32 | ADD package.json package-lock.json ./ 33 | RUN npm install --production=false 34 | 35 | FROM base as production-deps 36 | 37 | # create dir for app and set working dir 38 | RUN mkdir /app 39 | WORKDIR /app 40 | 41 | # Copy deps and prune off dev ones 42 | COPY --from=deps /app/node_modules /app/node_modules 43 | ADD package.json package-lock.json ./ 44 | RUN npm prune --production 45 | 46 | FROM base as build 47 | 48 | ENV NODE_ENV=production 49 | 50 | # create dir for app and set working dir 51 | RUN mkdir /app 52 | WORKDIR /app 53 | 54 | COPY --from=deps /app/node_modules /app/node_modules 55 | 56 | # cache the prisma schema 57 | ADD prisma . 58 | RUN npx prisma generate 59 | 60 | # build the app 61 | ADD . . 62 | RUN npm run build 63 | 64 | # finally, build the production image with minimal footprint 65 | FROM base 66 | 67 | ENV NODE_ENV=production 68 | 69 | # copy litestream binary to /usr/local/bin 70 | COPY --from=litestream-builder /go/bin/litestream /usr/bin/litestream 71 | # copy litestream setup script 72 | ADD setup-litestream.js /app/setup-litestream.js 73 | # copy litestream configs 74 | ADD etc/litestream.primary.yml /etc/litestream.primary.yml 75 | ADD etc/litestream.replica.yml /etc/litestream.replica.yml 76 | 77 | # copy over production deps 78 | COPY --from=production-deps /app/node_modules /app/node_modules 79 | # copy over generated prisma client 80 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 81 | # copy over built application and assets 82 | COPY --from=build /app/build /app/build 83 | COPY --from=build /app/public /app/public 84 | 85 | # set working dir 86 | WORKDIR /app 87 | 88 | # add stuff 89 | ADD . . 90 | 91 | CMD ["sh", "start_with_migrations.sh"] 92 | -------------------------------------------------------------------------------- /.github/workflows/deploy.production.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: {} 7 | 8 | jobs: 9 | build: 10 | name: 🐳 Build 11 | # only build/deploy main branch on pushes 12 | if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: 🛑 Cancel Previous Runs 16 | uses: styfle/cancel-workflow-action@0.9.1 17 | 18 | - name: ⬇️ Checkout repo 19 | uses: actions/checkout@v2 20 | 21 | - name: Read Fly.toml name 22 | uses: SebRollen/toml-action@v1.0.0 23 | id: app_name 24 | with: 25 | file: "fly.toml" 26 | field: "app" 27 | 28 | - name: 🐳 Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v1 30 | 31 | # Setup cache 32 | - name: ⚡️ Cache Docker layers 33 | uses: actions/cache@v2 34 | with: 35 | path: /tmp/.buildx-cache 36 | key: ${{ runner.os }}-buildx-${{ github.sha }} 37 | restore-keys: | 38 | ${{ runner.os }}-buildx- 39 | 40 | - name: 🔑 Fly Registry Auth 41 | uses: docker/login-action@v1 42 | with: 43 | registry: registry.fly.io 44 | username: x 45 | password: ${{ secrets.FLY_API_TOKEN }} 46 | 47 | - name: 🐳 Docker build 48 | uses: docker/build-push-action@v2 49 | with: 50 | context: . 51 | push: true 52 | tags: registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.sha }} 53 | build-args: | 54 | COMMIT_SHA=${{ github.sha }} 55 | cache-from: type=local,src=/tmp/.buildx-cache 56 | cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new 57 | 58 | # This ugly bit is necessary if you don't want your cache to grow forever 59 | # till it hits GitHub's limit of 5GB. 60 | # Temp fix 61 | # https://github.com/docker/build-push-action/issues/252 62 | # https://github.com/moby/buildkit/issues/1896 63 | - name: Move cache 64 | run: | 65 | rm -rf /tmp/.buildx-cache 66 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 67 | 68 | deploy: 69 | name: 🚀 Deploy 70 | runs-on: ubuntu-latest 71 | needs: [build] 72 | # only build/deploy main branch on pushes 73 | if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} 74 | 75 | steps: 76 | - name: 🛑 Cancel Previous Runs 77 | uses: styfle/cancel-workflow-action@0.9.1 78 | 79 | - name: ⬇️ Checkout repo 80 | uses: actions/checkout@v2 81 | 82 | - name: Read Fly.toml name 83 | uses: SebRollen/toml-action@v1.0.0 84 | id: app_name 85 | with: 86 | file: "fly.toml" 87 | field: "app" 88 | 89 | - name: 🚀 Deploy 90 | uses: superfly/flyctl-actions@1.1 91 | with: 92 | args: "deploy --config ./fly.toml --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.sha }}" 93 | env: 94 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 95 | --------------------------------------------------------------------------------