Dockerified!

This commit is contained in:
Atridad Lahiji 2024-04-09 11:29:47 -06:00
parent a592ed320c
commit b5f43ce616
No known key found for this signature in database
15 changed files with 8293 additions and 195 deletions

View file

@ -1,6 +1,5 @@
#Database #Database
DATABASE_URL="" DATABASE_URL=""
REDIS_URL=""
#Auth #Auth
CLERK_SIGN_UP_URL="/sign-up" CLERK_SIGN_UP_URL="/sign-up"

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ node_modules
/public/build /public/build
.env .env
dump.rdb dump.rdb
.idea/

43
Dockerfile Normal file
View file

@ -0,0 +1,43 @@
ARG NODE_VERSION=21.5.0
FROM node:${NODE_VERSION}-slim as base
# Remix app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
# Install pnpm
ARG PNPM_VERSION=8.9.2
RUN npm install -g pnpm@$PNPM_VERSION
# Throw-away build stage to reduce size of final image
FROM base as build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install -y build-essential pkg-config python-is-python3
# Install node modules
COPY --link package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod=false
# Copy application code
COPY --link . .
# Build application
RUN pnpm run build
# Remove development dependencies
RUN pnpm prune --prod
# Final stage for app image
FROM base
# Copy built application
COPY --from=build /app /app
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD [ "pnpm", "run", "start" ]

View file

@ -7,7 +7,7 @@ A dead-simple real-time voting tool.
- Front-end framework: Remix - Front-end framework: Remix
- Front-end library: React - Front-end library: React
- Rendering method: SSR - Rendering method: SSR
- Hosting: Fly - Hosting: ???
- ORM: Drizzle ORM - ORM: Drizzle ORM
- Database: Postgres - Database: Postgres

View file

@ -4,7 +4,6 @@ import { createId } from "@paralleldrive/cuid2";
import { db } from "~/services/db.server"; import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server"; import { emitter } from "~/services/emitter.server";
import { rooms } from "~/services/schema.server"; import { rooms } from "~/services/schema.server";
import { invalidateCache } from "~/services/redis.server";
export async function action({ request, params, context }: ActionFunctionArgs) { export async function action({ request, params, context }: ActionFunctionArgs) {
const { userId } = await getAuth({ context, params, request }); const { userId } = await getAuth({ context, params, request });
@ -34,7 +33,6 @@ export async function action({ request, params, context }: ActionFunctionArgs) {
const success = room.length > 0; const success = room.length > 0;
if (success) { if (success) {
await invalidateCache(`kv_roomlist_${userId}`, "sp");
emitter.emit("nodes", "roomlist"); emitter.emit("nodes", "roomlist");
return json(room, { return json(room, {

View file

@ -3,7 +3,6 @@ import { type ActionFunctionArgs, json } from "@remix-run/node";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "~/services/db.server"; import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server"; import { emitter } from "~/services/emitter.server";
import { invalidateCache } from "~/services/redis.server";
import { rooms } from "~/services/schema.server"; import { rooms } from "~/services/schema.server";
export async function action({ request, params, context }: ActionFunctionArgs) { export async function action({ request, params, context }: ActionFunctionArgs) {
@ -33,7 +32,6 @@ export async function action({ request, params, context }: ActionFunctionArgs) {
const success = deletedRoom.length > 0; const success = deletedRoom.length > 0;
if (success) { if (success) {
await invalidateCache(`kv_roomlist_${userId}`, "sp");
emitter.emit("nodes", "roomlist"); emitter.emit("nodes", "roomlist");
return json(deletedRoom, { return json(deletedRoom, {

View file

@ -4,7 +4,6 @@ import { eq } from "drizzle-orm";
import { eventStream } from "remix-utils/sse/server"; import { eventStream } from "remix-utils/sse/server";
import { db } from "~/services/db.server"; import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server"; import { emitter } from "~/services/emitter.server";
import { fetchCache, setCache } from "~/services/redis.server";
import { rooms } from "~/services/schema.server"; import { rooms } from "~/services/schema.server";
// Get Room List // Get Room List
@ -20,53 +19,27 @@ export async function loader({ context, params, request }: LoaderFunctionArgs) {
return eventStream(request.signal, function setup(send) { return eventStream(request.signal, function setup(send) {
async function handler() { async function handler() {
fetchCache<
{
id: string;
createdAt: Date;
roomName: string;
}[]
>(`kv_roomlist_${userId}`, "sp").then((cachedResult) => {
if (cachedResult) {
send({ event: userId!, data: JSON.stringify(cachedResult) });
} else {
db.query.rooms db.query.rooms
.findMany({ .findMany({
where: eq(rooms.userId, userId || ""), where: eq(rooms.userId, userId || ""),
}) })
.then((roomList) => { .then((roomList) => {
Promise.all([ Promise.all([
setCache(`kv_roomlist_${userId}`, roomList, "sp"),
send({ event: userId!, data: JSON.stringify(roomList) }), send({ event: userId!, data: JSON.stringify(roomList) }),
]); ]);
}); });
} }
});
}
// Initial fetch // Initial fetch
fetchCache<
{
id: string;
createdAt: Date;
roomName: string;
}[]
>(`kv_roomlist_${userId}`, "sp").then((cachedResult) => {
if (cachedResult) {
send({ event: userId!, data: JSON.stringify(cachedResult) });
} else {
db.query.rooms db.query.rooms
.findMany({ .findMany({
where: eq(rooms.userId, userId || ""), where: eq(rooms.userId, userId || ""),
}) })
.then((roomList) => { .then((roomList) => {
Promise.all([ Promise.all([
setCache(`kv_roomlist_${userId}`, roomList, "sp"),
send({ event: userId!, data: JSON.stringify(roomList) }), send({ event: userId!, data: JSON.stringify(roomList) }),
]); ]);
}); });
}
});
emitter.on("roomlist", handler); emitter.on("roomlist", handler);

View file

@ -16,7 +16,7 @@ export default function SignUpPage() {
return ( return (
<main className="flex flex-col text-center items-center justify-center px-4 py-16 gap-4 min-h-[100%]"> <main className="flex flex-col text-center items-center justify-center px-4 py-16 gap-4 min-h-[100%]">
<SignUp <SignUp
path="/sign-in" path="/sign-up"
routing="path" routing="path"
signInUrl="/sign-up" signInUrl="/sign-up"
redirectUrl={ENV.ROOT_URL ? ENV.ROOT_URL : "/"} redirectUrl={ENV.ROOT_URL ? ENV.ROOT_URL : "/"}

View file

@ -1,5 +1,4 @@
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { publishToChannel, subscribeToChannel } from "./redis.server";
import "dotenv/config"; import "dotenv/config";
let emitter: EventEmitter; let emitter: EventEmitter;
@ -17,22 +16,9 @@ if (process.env.NODE_ENV === "production") {
emitter = global.__emitter; emitter = global.__emitter;
} }
if (process.env.REDIS_URL) { emitter.on("nodes", async (message: string) => {
subscribeToChannel("nodes", (message: string) => { console.log(`RECEIVED ${message} EVENT!`);
console.log(`[MULTI-NODE] RECEIVED ${message} EVENT FROM ANOTHER NODE!`);
const parsedMessage = message.split('"')[1];
emitter.emit(parsedMessage);
});
emitter.on("nodes", async (message: string) => {
emitter.emit(message); emitter.emit(message);
await publishToChannel("nodes", message); });
});
} else {
emitter.on("nodes", async (message: string) => {
console.log(`[SINGLE NODE] RECEIVED ${message} EVENT!`);
emitter.emit(message);
});
}
export { emitter }; export { emitter };

View file

@ -1,121 +0,0 @@
import Redis from "ioredis";
import "dotenv/config";
let cache: Redis | null = null;
let pub: Redis | null = null;
let sub: Redis | null = null;
declare global {
var __cache: Redis | null;
var __pub: Redis | null;
var __sub: Redis | null;
}
if (process.env.NODE_ENV === "production") {
cache = process.env.REDIS_URL
? new Redis(process.env.REDIS_URL, {
family: 6,
})
: null;
} else {
if (!global.__cache) {
global.__cache = process.env.REDIS_URL
? new Redis(process.env.REDIS_URL, {
family: 6,
})
: null;
}
cache = global.__cache;
}
if (process.env.NODE_ENV === "production") {
pub = process.env.REDIS_URL
? new Redis(process.env.REDIS_URL, {
family: 6,
})
: null;
} else {
if (!global.__pub) {
global.__pub = process.env.REDIS_URL
? new Redis(process.env.REDIS_URL, {
family: 6,
})
: null;
}
pub = global.__pub;
}
if (process.env.NODE_ENV === "production") {
sub = process.env.REDIS_URL
? new Redis(process.env.REDIS_URL, {
family: 6,
})
: null;
} else {
if (!global.__sub) {
global.__sub = process.env.REDIS_URL
? new Redis(process.env.REDIS_URL, {
family: 6,
})
: null;
}
sub = global.__sub;
}
export const publishToChannel = async (channel: string, message: string) => {
await pub?.publish(channel, JSON.stringify(message));
};
export const subscribeToChannel = async (
channel: string,
callback: Function
) => {
await sub?.subscribe(channel, (err, count) => {
if (err) {
console.error("Failed to subscribe: %s", err.message);
} else {
console.log(
`Subscribed successfully! This client is currently subscribed to ${count} channels.`
);
}
});
sub?.on("message", (channel, message) => {
console.log(`Received ${message} from ${channel}`);
callback(message);
});
};
export const unsubscribeToChannel = (channel: string) => {
console.log(`Unsubscribed successfully from ${channel}!`);
Promise.resolve([sub?.unsubscribe(channel)]);
};
export const setCache = async <T>(key: string, value: T, prefix?: string) => {
try {
await cache?.set(prefix ? `${prefix}_${key}` : key, JSON.stringify(value));
return true;
} catch {
return false;
}
};
export const fetchCache = async <T>(key: string, prefix?: string) => {
try {
const result = (await cache?.get(
prefix ? `${prefix}_${key}` : key
)) as string;
return JSON.parse(result) as T;
} catch {
return null;
}
};
export const invalidateCache = async (key: string, prefix?: string) => {
try {
await cache?.del(prefix ? `${prefix}_${key}` : key);
return true;
} catch {
return false;
}
};

BIN
bun.lockb

Binary file not shown.

45
docker-compose.yml Normal file
View file

@ -0,0 +1,45 @@
version: "3.8"
services:
db:
image: postgres:latest
pull_policy: build
environment:
- POSTGRES_DB=$POSTGRES_DB
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
- POSTGRES_USER=$POSTGRES_USER
ports:
- 5432:5432
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- nginx_default
restart: on-failure:3
app:
build:
context: .
dockerfile: Dockerfile
pull_policy: build
restart: on-failure:3
networks:
- nginx_default
ports:
- "3100:3000"
environment:
- DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@db:5432/$POSTGRES_DB?sslmode=disable
- CLERK_SIGN_UP_URL=$CLERK_SIGN_UP_URL
- CLERK_SIGN_IN_URL=$CLERK_SIGN_IN_URL
- CLERK_PUBLISHABLE_KEY=$CLERK_PUBLISHABLE_KEY
- CLERK_SECRET_KEY=$CLERK_SECRET_KEY
- CLERK_WEBHOOK_SIGNING_SECRET=$CLERK_WEBHOOK_SIGNING_SECRET
- ROOT_URL=$ROOT_URL
- SHIT_LIST=$SHIT_LIST
volumes:
redis:
redis-config:
pgdata:
networks:
nginx_default:
external:
name: nginx_default

View file

@ -19,7 +19,6 @@
"csv42": "^5.0.0", "csv42": "^5.0.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"drizzle-orm": "^0.30.6", "drizzle-orm": "^0.30.6",
"ioredis": "^5.3.2",
"isbot": "5.1.3", "isbot": "5.1.3",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"postgres": "^3.4.4", "postgres": "^3.4.4",

8177
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

BIN
public/.DS_Store vendored Normal file

Binary file not shown.