From 572d38e4902b8693fbebef30913ab3577a8509ef Mon Sep 17 00:00:00 2001 From: elpatron Date: Thu, 28 May 2026 12:23:50 +0200 Subject: [PATCH] Dockerize client, server, and postgres database for production with container healthchecks --- client/Dockerfile | 28 ++++++++++++++++++++ client/nginx.conf | 19 ++++++++++++++ client/src/services/auth.ts | 2 +- client/src/services/logbook.ts | 2 +- client/src/services/sync.ts | 2 +- client/vite.config.ts | 9 +++++++ docker-compose.yml | 48 ++++++++++++++++++++++++++++++++++ server/Dockerfile | 48 ++++++++++++++++++++++++++++++++++ server/package.json | 4 +-- server/src/index.ts | 25 +++++++++++++----- 10 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 client/Dockerfile create mode 100644 client/nginx.conf create mode 100644 docker-compose.yml create mode 100644 server/Dockerfile diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..e83680c --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,28 @@ +# --- Build Stage --- +FROM node:20-alpine AS builder +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci + +# Copy code and build production bundle +COPY . . +RUN npm run build + +# --- Production Stage --- +FROM nginx:1.25-alpine +WORKDIR /usr/share/nginx/html + +# Copy custom Nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built assets from builder +COPY --from=builder /app/dist . + +# Expose HTTP port +EXPOSE 80 + +# Health check to verify Nginx is actively running +HEALTHCHECK --interval=30s --timeout=5s --start-period=3s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1 diff --git a/client/nginx.conf b/client/nginx.conf new file mode 100644 index 0000000..58b29fb --- /dev/null +++ b/client/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend:5000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/client/src/services/auth.ts b/client/src/services/auth.ts index 2cb7bfb..abee67a 100644 --- a/client/src/services/auth.ts +++ b/client/src/services/auth.ts @@ -10,7 +10,7 @@ import { bufferToBase64 } from './crypto.js' -const API_BASE = 'http://localhost:5000/api/auth' +const API_BASE = '/api/auth' // Shared in-memory container for the active user's session master key let activeMasterKey: ArrayBuffer | null = null diff --git a/client/src/services/logbook.ts b/client/src/services/logbook.ts index 00893cf..d5a15d4 100644 --- a/client/src/services/logbook.ts +++ b/client/src/services/logbook.ts @@ -2,7 +2,7 @@ import { db, type LocalLogbook } from './db.js' import { getActiveMasterKey } from './auth.js' import { encryptJson, decryptJson } from './crypto.js' -const API_BASE = 'http://localhost:5000/api/logbooks' +const API_BASE = '/api/logbooks' export interface DecryptedLogbook { id: string diff --git a/client/src/services/sync.ts b/client/src/services/sync.ts index aea952b..8831504 100644 --- a/client/src/services/sync.ts +++ b/client/src/services/sync.ts @@ -1,7 +1,7 @@ import { db } from './db.js' import { getActiveMasterKey } from './auth.js' -const API_BASE = 'http://localhost:5000/api/sync' +const API_BASE = '/api/sync' const syncingLogbooks = new Set() let isSyncing = false diff --git a/client/vite.config.ts b/client/vite.config.ts index 02980e7..146b714 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -4,6 +4,15 @@ import { VitePWA } from 'vite-plugin-pwa' // https://vite.dev/config/ export default defineConfig({ + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:5000', + changeOrigin: true + } + } + }, plugins: [ react(), VitePWA({ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..633795d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + db: + image: postgres:16-alpine + container_name: daagbox-prod-db + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: daagbox + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d daagbox"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + build: + context: ./server + dockerfile: Dockerfile + container_name: daagbox-prod-backend + restart: always + environment: + PORT: 5000 + DATABASE_URL: "postgresql://postgres:postgres@db:5432/daagbox?schema=public" + command: sh -c "npx prisma db push && node dist/index.js" + depends_on: + db: + condition: service_healthy + + frontend: + build: + context: ./client + dockerfile: Dockerfile + container_name: daagbox-prod-frontend + restart: always + ports: + - "80:80" + depends_on: + backend: + condition: service_healthy + +volumes: + pgdata: + name: daagbox-prod-pgdata diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..10ad1da --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,48 @@ +# --- Build Stage --- +FROM node:20-alpine AS builder +WORKDIR /app +RUN apk add --no-cache openssl libc6-compat + +# Copy package configurations +COPY package*.json ./ + +# Install all dependencies (including devDependencies for tsc) +RUN npm ci + +# Copy Prisma schema and generate Client code +COPY prisma ./prisma +RUN npx prisma generate + +# Copy source and compile TypeScript +COPY src ./src +COPY tsconfig.json ./ +RUN npm run build + +# --- Production Runner Stage --- +FROM node:20-alpine +WORKDIR /app +RUN apk add --no-cache openssl libc6-compat + +# Copy package configurations +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --omit=dev + +# Copy generated Prisma Client from builder stage +COPY --from=builder /app/node_modules/@prisma/client ./node_modules/@prisma/client +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma + +# Copy built code and runtime configs +COPY --from=builder /app/dist ./dist +COPY prisma ./prisma + +# Expose backend PORT +EXPOSE 5000 + +# Health check utilizing native http module to query the backend health API (which queries PG database) +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD node -e "const http = require('http'); http.get('http://localhost:5000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));" + +# Launch backend Express server +CMD ["node", "dist/index.js"] diff --git a/server/package.json b/server/package.json index f9dad19..1af6f13 100644 --- a/server/package.json +++ b/server/package.json @@ -14,13 +14,13 @@ "@simplewebauthn/server": "^9.0.3", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.19.2" + "express": "^4.19.2", + "prisma": "^5.10.2" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.11.24", - "prisma": "^5.10.2", "tsx": "^4.7.1", "typescript": "^5.3.3" } diff --git a/server/src/index.ts b/server/src/index.ts index c35157e..a48fcce 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -4,6 +4,7 @@ import dotenv from 'dotenv' import authRouter from './routes/auth.js' import logbooksRouter from './routes/logbooks.js' import syncRouter from './routes/sync.js' +import { prisma } from './db.js' dotenv.config() @@ -19,12 +20,24 @@ app.use('/api/logbooks', logbooksRouter) app.use('/api/sync', syncRouter) // Health check endpoint -app.get('/api/health', (req, res) => { - res.json({ - status: 'ok', - timestamp: new Date().toISOString(), - service: 'Kapteins Daagbox Backend' - }) +app.get('/api/health', async (req, res) => { + try { + await prisma.$queryRaw`SELECT 1` + res.json({ + status: 'ok', + database: 'connected', + timestamp: new Date().toISOString(), + service: 'Kapteins Daagbox Backend' + }) + } catch (err: any) { + res.status(500).json({ + status: 'error', + database: 'disconnected', + error: err.message, + timestamp: new Date().toISOString(), + service: 'Kapteins Daagbox Backend' + }) + } }) app.listen(PORT, () => {