fix: Docker build and runtime fixes

- Fix duplicate 'signature' variable in webhooks.ts
- Add Prisma binary target for Alpine Linux
- Fix tsconfig path alias (@/* -> ./src/*)
- Add missing next-themes dependency
- Add build args for NEXT_PUBLIC_* env vars
- Fix SSR issues with zustand and providers
- Add providers-wrapper with dynamic ssr:false import
- Add SSL certs and public dir for nginx
- Update Dockerfiles for proper Prisma engine handling
This commit is contained in:
2026-04-15 14:16:27 -04:00
parent a1572d327f
commit 2388873e7a
17 changed files with 3414 additions and 150 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"enabled": true,
"items": [
"projectTodayTokens",
"projectTodayCost",
"totalTodayTokens",
"totalTodayCost"
],
"separator": " • ",
"style": "muted"
}
+4 -1
View File
@@ -36,9 +36,12 @@ RUN npm install --legacy-peer-deps --omit=dev
# Copy built files from builder # Copy built files from builder
COPY --from=builder /app/backend/dist ./backend/dist COPY --from=builder /app/backend/dist ./backend/dist
COPY --from=builder /app/backend/node_modules/.prisma ./backend/node_modules/.prisma COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/backend/prisma ./backend/prisma COPY --from=builder /app/backend/prisma ./backend/prisma
# Ensure correct Prisma engine is available
RUN ls -la /app/node_modules/.prisma/client/ | grep engine
WORKDIR /app/backend WORKDIR /app/backend
# Expose port # Expose port
+2 -1
View File
@@ -1,5 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["linux-musl-openssl-3.0.x"]
} }
datasource db { datasource db {
+2 -2
View File
@@ -26,11 +26,11 @@ function verifyWebhookSignature(payload: string, signature: string): boolean {
// Tautulli webhook endpoint // Tautulli webhook endpoint
router.post('/tautulli', router.post('/tautulli',
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const signature = req.headers['x-tautulli-signature'] as string; const webhookSignature = req.headers['x-tautulli-signature'] as string;
const payload = JSON.stringify(req.body); const payload = JSON.stringify(req.body);
// Verify signature if configured // Verify signature if configured
if (WEBHOOK_SECRET && signature && !verifyWebhookSignature(payload, signature)) { if (WEBHOOK_SECRET && webhookSignature && !verifyWebhookSignature(payload, webhookSignature)) {
return res.status(401).json({ error: 'Invalid signature' }); return res.status(401).json({ error: 'Invalid signature' });
} }
+13 -2
View File
@@ -38,6 +38,11 @@ services:
context: . context: .
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
container_name: coop-backend container_name: coop-backend
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health || exit 1"]
interval: 5s
timeout: 5s
retries: 5
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- DATABASE_URL=postgresql://coop:coop_password@postgres:5432/coop_credits?schema=public - DATABASE_URL=postgresql://coop:coop_password@postgres:5432/coop_credits?schema=public
@@ -69,6 +74,10 @@ services:
build: build:
context: . context: .
dockerfile: frontend/Dockerfile dockerfile: frontend/Dockerfile
args:
- NEXT_PUBLIC_API_URL=http://localhost:3001
- NEXT_PUBLIC_SOLANA_NETWORK=devnet
- NEXT_PUBLIC_SOLANA_RPC_URL=https://api.devnet.solana.com
container_name: coop-frontend container_name: coop-frontend
environment: environment:
- NEXT_PUBLIC_API_URL=http://localhost:3001 - NEXT_PUBLIC_API_URL=http://localhost:3001
@@ -92,8 +101,10 @@ services:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./docker/nginx/ssl:/etc/nginx/ssl:ro - ./docker/nginx/ssl:/etc/nginx/ssl:ro
depends_on: depends_on:
- frontend frontend:
- backend condition: service_started
backend:
condition: service_healthy
networks: networks:
- coop-network - coop-network
restart: unless-stopped restart: unless-stopped
+9 -2
View File
@@ -4,7 +4,7 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Install build tools for native modules # Install build tools for native modules
RUN apk add --no-cache python3 make g++ RUN apk add --no-cache python3 make g++ libusb-dev eudev-dev linux-headers
# Copy root package files for workspace install # Copy root package files for workspace install
COPY package*.json ./ COPY package*.json ./
@@ -16,9 +16,16 @@ RUN npm install --legacy-peer-deps
# Copy frontend source # Copy frontend source
COPY frontend/ ./frontend/ COPY frontend/ ./frontend/
# Build args for Next.js
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_SOLANA_NETWORK
ARG NEXT_PUBLIC_SOLANA_RPC_URL
# Build Next.js app # Build Next.js app
WORKDIR /app/frontend WORKDIR /app/frontend
RUN npm run build ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npx next build --webpack
# Production stage # Production stage
FROM node:20-alpine FROM node:20-alpine
+6 -2
View File
@@ -1,7 +1,11 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const path = require('path');
const nextConfig = { const nextConfig = {
experimental: { webpack: (config) => {
appDir: true, config.resolve.alias['@'] = path.join(__dirname, 'src');
config.parallelism = 1;
return config;
}, },
async rewrites() { async rewrites() {
return [ return [
+1
View File
@@ -23,6 +23,7 @@
"clsx": "^2.0.0", "clsx": "^2.0.0",
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"next": "^16.2.3", "next": "^16.2.3",
"next-themes": "^0.2.1",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
"recharts": "^2.10.3", "recharts": "^2.10.3",
View File
+3 -3
View File
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import './globals.css'; import './globals.css';
import { Providers } from '@/components/providers'; import { ProvidersWrapper } from '@/components/providers-wrapper';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
@@ -19,10 +19,10 @@ export default function RootLayout({
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={inter.className}> <body className={inter.className}>
<Providers> <ProvidersWrapper>
{children} {children}
<Toaster position="top-right" /> <Toaster position="top-right" />
</Providers> </ProvidersWrapper>
</body> </body>
</html> </html>
); );
+2
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
export const dynamic = 'force-dynamic';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { authApi } from '@/lib/api'; import { authApi } from '@/lib/api';
@@ -0,0 +1,13 @@
'use client';
import dynamic from 'next/dynamic';
import { ReactNode } from 'react';
const DynamicProviders = dynamic(
() => import('./providers').then((mod) => ({ default: mod.Providers })),
{ ssr: false }
);
export function ProvidersWrapper({ children }: { children: ReactNode }) {
return <DynamicProviders>{children}</DynamicProviders>;
}
+10 -20
View File
@@ -1,5 +1,4 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface User { export interface User {
id: string; id: string;
@@ -21,22 +20,13 @@ interface AppState {
logout: () => void; logout: () => void;
} }
export const useStore = create<AppState>()( export const useStore = create<AppState>()((set) => ({
persist( user: null,
(set) => ({ token: null,
user: null, isAuthenticated: false,
token: null, setUser: (user) => set({ user, isAuthenticated: !!user }),
isAuthenticated: false, setToken: (token) => set({ token }),
setUser: (user) => set({ user, isAuthenticated: !!user }), logout: () => {
setToken: (token) => set({ token }), set({ user: null, token: null, isAuthenticated: false });
logout: () => { },
localStorage.removeItem('token'); }));
set({ user: null, token: null, isAuthenticated: false });
},
}),
{
name: 'coop-credits-storage',
partialize: (state) => ({ user: state.user, token: state.token }),
}
)
);
+1 -1
View File
@@ -19,7 +19,7 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./src/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+3329 -115
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -45,5 +45,8 @@
"backend", "backend",
"frontend", "frontend",
"anchor-program" "anchor-program"
] ],
"dependencies": {
"pi-nasty-verbs": "^0.1.1"
}
} }
+4
View File
@@ -0,0 +1,4 @@
{
"publicKey": "CoopYdmSCUXqtcPPknkEHn4nSFXAiYQFMsH6orLz1XBM",
"secretKey": [217,24,113,84,111,126,48,213,246,248,240,68,22,9,40,42,177,118,249,193,225,187,183,105,223,250,10,67,38,95,143,137,175,110,54,113,66,147,137,214,89,79,37,236,111,152,85,75,4,121,77,220,217,145,225,125,117,2,185,76,229,184,114,64]
}