├── .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 |
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 |
--------------------------------------------------------------------------------