Dockerified!
This commit is contained in:
parent
a592ed320c
commit
b5f43ce616
15 changed files with 8293 additions and 195 deletions
|
@ -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"
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,4 +4,5 @@ node_modules
|
||||||
/build
|
/build
|
||||||
/public/build
|
/public/build
|
||||||
.env
|
.env
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
.idea/
|
43
Dockerfile
Normal file
43
Dockerfile
Normal 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" ]
|
|
@ -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
|
||||||
|
|
||||||
|
@ -18,4 +18,4 @@ A dead-simple real-time voting tool.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Feel free to propose changes via PR. I'm not awfully picky about formatting right now, so I'll accept/reject on a case-by-case basis. Please make sure to have an issue first though.
|
Feel free to propose changes via PR. I'm not awfully picky about formatting right now, so I'll accept/reject on a case-by-case basis. Please make sure to have an issue first though.
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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<
|
db.query.rooms
|
||||||
{
|
.findMany({
|
||||||
id: string;
|
where: eq(rooms.userId, userId || ""),
|
||||||
createdAt: Date;
|
})
|
||||||
roomName: string;
|
.then((roomList) => {
|
||||||
}[]
|
Promise.all([
|
||||||
>(`kv_roomlist_${userId}`, "sp").then((cachedResult) => {
|
send({ event: userId!, data: JSON.stringify(roomList) }),
|
||||||
if (cachedResult) {
|
]);
|
||||||
send({ event: userId!, data: JSON.stringify(cachedResult) });
|
});
|
||||||
} else {
|
|
||||||
db.query.rooms
|
|
||||||
.findMany({
|
|
||||||
where: eq(rooms.userId, userId || ""),
|
|
||||||
})
|
|
||||||
.then((roomList) => {
|
|
||||||
Promise.all([
|
|
||||||
setCache(`kv_roomlist_${userId}`, roomList, "sp"),
|
|
||||||
send({ event: userId!, data: JSON.stringify(roomList) }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch
|
||||||
fetchCache<
|
db.query.rooms
|
||||||
{
|
.findMany({
|
||||||
id: string;
|
where: eq(rooms.userId, userId || ""),
|
||||||
createdAt: Date;
|
})
|
||||||
roomName: string;
|
.then((roomList) => {
|
||||||
}[]
|
Promise.all([
|
||||||
>(`kv_roomlist_${userId}`, "sp").then((cachedResult) => {
|
send({ event: userId!, data: JSON.stringify(roomList) }),
|
||||||
if (cachedResult) {
|
]);
|
||||||
send({ event: userId!, data: JSON.stringify(cachedResult) });
|
});
|
||||||
} else {
|
|
||||||
db.query.rooms
|
|
||||||
.findMany({
|
|
||||||
where: eq(rooms.userId, userId || ""),
|
|
||||||
})
|
|
||||||
.then((roomList) => {
|
|
||||||
Promise.all([
|
|
||||||
setCache(`kv_roomlist_${userId}`, roomList, "sp"),
|
|
||||||
send({ event: userId!, data: JSON.stringify(roomList) }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on("roomlist", handler);
|
emitter.on("roomlist", handler);
|
||||||
|
|
||||||
|
|
|
@ -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 : "/"}
|
||||||
|
|
|
@ -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!`);
|
emitter.emit(message);
|
||||||
const parsedMessage = message.split('"')[1];
|
});
|
||||||
emitter.emit(parsedMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on("nodes", async (message: string) => {
|
|
||||||
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 };
|
||||||
|
|
|
@ -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
BIN
bun.lockb
Binary file not shown.
45
docker-compose.yml
Normal file
45
docker-compose.yml
Normal 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
|
|
@ -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",
|
||||||
|
@ -47,4 +46,4 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
8177
pnpm-lock.yaml
generated
Normal file
8177
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
BIN
public/.DS_Store
vendored
Normal file
BIN
public/.DS_Store
vendored
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue