From 63e33a45b4bab6cc0ea65375ed626a49f769d7de Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 28 Jan 2026 10:36:11 -0800 Subject: [PATCH] backend docker refactor and frontend cleanup --- backend/Dockerfile | 24 ++++++++--- backend/Dockerfile.dev | 13 ++++++ backend/package.json | 5 ++- backend/src/controllers/recipeController.ts | 12 ------ docker-compose.dev.yaml | 45 +++++++++++++++++++++ docker-compose.yaml | 23 +++++------ example.env | 7 ++++ frontend/Dockerfile.dev | 13 ++++++ frontend/package-lock.json | 20 +++++++-- frontend/package.json | 2 +- frontend/src/components/DemoModal.tsx | 28 ------------- frontend/src/pages/AddRecipe.tsx | 15 ------- frontend/src/pages/EditRecipe.tsx | 14 ------- frontend/src/pages/RecipePage.tsx | 22 ++-------- 14 files changed, 130 insertions(+), 113 deletions(-) create mode 100644 backend/Dockerfile.dev create mode 100644 docker-compose.dev.yaml create mode 100644 example.env create mode 100644 frontend/Dockerfile.dev delete mode 100644 frontend/src/components/DemoModal.tsx diff --git a/backend/Dockerfile b/backend/Dockerfile index 6b97cd7..5f357b7 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,15 +1,27 @@ -FROM node:22-slim +FROM node:22-slim AS base -WORKDIR /usr/src/app +WORKDIR /app -COPY package*.json ./ +COPY package*.json tsconfig*.json prisma.config.ts ./ +COPY prisma ./prisma +COPY src ./src RUN apt-get update -y && apt-get install -y openssl +RUN npm install --omit=dev +RUN npm run prisma:generate -RUN if [ "$NODE_ENV" = "dev" ]; then npm install; else npm install --omit=dev; fi +FROM base AS builder +RUN npm install +RUN npm run build + + +FROM base +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/prisma ./prisma +# COPY --from=builder /app/node_modules ./node_modules +COPY prisma.config.ts /app/ -COPY . . EXPOSE 3000 -CMD npm run $NODE_ENV +CMD ["npm", "run", "prod"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..31714c0 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM node:22-slim + +WORKDIR /app + +COPY package*.json ./ + +RUN apt-get update -y && apt-get install -y openssl + +RUN npm install + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] diff --git a/backend/package.json b/backend/package.json index e66c6a3..7fba4b5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,8 +4,9 @@ "main": "index.js", "scripts": { "dev": "nodemon ./src/index.ts", - "production": "npx tsc && node ./dist/index.js", - "ci": "tsc" + "build": "tsc", + "prisma:generate": "prisma generate", + "prod": "node ./dist/index.js" }, "keywords": [], "author": "", diff --git a/backend/src/controllers/recipeController.ts b/backend/src/controllers/recipeController.ts index f350ca6..94bb46a 100644 --- a/backend/src/controllers/recipeController.ts +++ b/backend/src/controllers/recipeController.ts @@ -51,9 +51,6 @@ export const getRecipeById = async ( }; export const addRecipe = async (req: Request, res: Response): Promise => { - if (process.env.NODE_ENV === "demo") { - return; - } try { console.log(req.body); const createdRecipe = await model.addRecipe(req.body); @@ -73,9 +70,6 @@ export const updateRecipe = async ( ): Promise => { console.log(req.body); const id = parseInt(req.params.id, 10); - if (process.env.NODE_ENV === "demo") { - return; - } try { const updatedRecipe = await model.updateRecipe(req.body, id); res.status(201).json(updatedRecipe); @@ -89,9 +83,6 @@ export const updateRecipe = async ( }; export const setStars = async (req: Request, res: Response): Promise => { - if (process.env.NODE_ENV === "demo") { - return; - } const id = parseInt(req.body.id, 10); const stars = parseInt(req.body.stars, 10); try { @@ -110,9 +101,6 @@ export const deleteRecipe = async ( req: Request, res: Response, ): Promise => { - if (process.env.NODE_ENV === "demo") { - return; - } const id = parseInt(req.body.id, 10); try { await model.deleteRecipe(id); diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..2fc6742 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,45 @@ +services: + db: + container_name: recipes_db_dev + image: docker.io/library/postgres:17 + restart: unless-stopped + env_file: + - .env + environment: + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME} + ports: + - "${DB_PORT}:5432" + volumes: + - ./db:/var/lib/postgresql/data + backend: + container_name: recipes_backend_dev + image: recipes_backend + restart: unless-stopped + build: + context: ./backend + dockerfile: Dockerfile.dev + ports: + - "${BACKEND_PORT}:3000" + volumes: + - ./backend:/app + - ./logs:/logs + environment: + - DB_USER=${DB_USER} + - DB_PASSWORD=${DB_PASSWORD} + - DB_NAME=${DB_NAME} + - DATABASE_URL=${DATABASE_URL} + frontend: + container_name: recipes_frontend_dev + image: recipes_frontend + restart: unless-stopped + environment: + - NODE_ENV=dev + build: + context: ./frontend + dockerfile: Dockerfile.dev + ports: + - "${FRONTEND_PORT}:80" + volumes: + - ./frontend:/app diff --git a/docker-compose.yaml b/docker-compose.yaml index 4210964..b93c8c3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,8 +1,8 @@ services: db: - container_name: recipes_postgres_${ID} + container_name: recipes_db image: docker.io/library/postgres:17 - # restart: always + restart: unless-stopped env_file: - .env environment: @@ -14,34 +14,29 @@ services: volumes: - ./db:/var/lib/postgresql/data backend: - image: recipes_backend - container_name: recipes_backend_${ID} + container_name: recipes_backend + image: forgejo.fredzernia.com/fred/recipes_backend:latest + restart: unless-stopped build: context: ./backend - args: - NODE_ENV: ${NODE_ENV} ports: - "${BACKEND_PORT}:3000" volumes: - ./backend:/usr/src/app - ./logs:/logs environment: - - NODE_ENV=${NODE_ENV} - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - DB_NAME=${DB_NAME} - - DATABASE_URL=${PRISMA_DB_URL} + - DATABASE_URL=${DATABASE_URL} frontend: - image: recipes_frontend - container_name: recipes_frontend_${ID} + container_name: recipes_frontend + image: forgejo.fredzernia.com/fred/recipes_frontend:latest + restart: unless-stopped build: context: ./backend - args: - NODE_ENV: ${NODE_ENV} ports: - "${FRONTEND_PORT}:80" volumes: - ./frontend:/usr/src/app - ./dist/recipes_frontend:/usr/src/app/dist - environment: - - NODE_ENV=${NODE_ENV} diff --git a/example.env b/example.env new file mode 100644 index 0000000..092cc7e --- /dev/null +++ b/example.env @@ -0,0 +1,7 @@ +DB_PORT=5432 +DB_USER=recipe_app +DB_PASSWORD=password +DB_NAME=recipe_app +BACKEND_PORT=3000 +FRONTEND_PORT=8080 +DATABASE_URL='postgres://recipe_app:password@db:5432/' diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..9a21cc7 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install; + +COPY . . + +EXPOSE 80 + +CMD npm run dev diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c7f20f4..4d46cd2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -87,6 +87,7 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1471,6 +1472,7 @@ "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -1481,6 +1483,7 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1541,6 +1544,7 @@ "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", @@ -1792,6 +1796,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1992,6 +1997,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -2026,9 +2032,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { @@ -2320,6 +2326,7 @@ "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3392,6 +3399,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3568,6 +3576,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3577,6 +3586,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -4094,6 +4104,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4153,6 +4164,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4245,6 +4257,7 @@ "integrity": "sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -4335,6 +4348,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/package.json b/frontend/package.json index 6c0d8e6..261fd14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,8 +15,8 @@ "react-router-dom": "^7.6.3" }, "devDependencies": { - "@types/node": "^24.2.0", "@eslint/js": "^9.29.0", + "@types/node": "^24.2.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.5.2", diff --git a/frontend/src/components/DemoModal.tsx b/frontend/src/components/DemoModal.tsx deleted file mode 100644 index 7a99a44..0000000 --- a/frontend/src/components/DemoModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Link } from 'react-router-dom'; - -interface DemoModalProps { - isOpen: boolean; - onClose: () => void; - closeModal: () => void; -} - -const DemoModal = ({ isOpen, onClose, closeModal }: DemoModalProps) => { - if (!isOpen) return null; - - return ( -
-
e.stopPropagation()}> -
-

Thanks for checking out my app! Database write operations are disabled in demo mode.

-

access@fredzernia.com to request access to the production build

-

Find out more about this app here

-
-
- -
-
-
- ); -}; - -export default DemoModal; diff --git a/frontend/src/pages/AddRecipe.tsx b/frontend/src/pages/AddRecipe.tsx index 108a656..19a6e8b 100644 --- a/frontend/src/pages/AddRecipe.tsx +++ b/frontend/src/pages/AddRecipe.tsx @@ -1,20 +1,12 @@ -import { useState } from "react"; import { addRecipe } from "../services/frontendApi.js"; import { useNavigate } from "react-router-dom"; import RecipeForm from "../components/RecipeForm.tsx"; -import DemoModal from "../components/DemoModal.tsx"; import { type Recipe } from "../types/Recipe.ts" function AddRecipe() { - const [showDemoModal, setShowDemoModal] = useState(false); const navigate = useNavigate(); const addRecipeForm = async (recipeData: Recipe) => { - if (process.env.NODE_ENV === "demo") { - setShowDemoModal(true); - return; - } - const data = await addRecipe(recipeData); navigate(`/recipe/${data.id}`); }; @@ -22,13 +14,6 @@ function AddRecipe() { return (
- {showDemoModal && ( - setShowDemoModal(false)} - closeModal={() => setShowDemoModal(false)} - /> - )}
); } diff --git a/frontend/src/pages/EditRecipe.tsx b/frontend/src/pages/EditRecipe.tsx index e027dde..e75496c 100644 --- a/frontend/src/pages/EditRecipe.tsx +++ b/frontend/src/pages/EditRecipe.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { getRecipeById, updateRecipe } from "../services/frontendApi.js"; import RecipeForm from "../components/RecipeForm.tsx"; -import DemoModal from "../components/DemoModal.tsx"; import { type Recipe } from "../types/Recipe.ts" function EditRecipe() { @@ -10,7 +9,6 @@ function EditRecipe() { const navigate = useNavigate(); const [recipe, setRecipe] = useState(); - const [showDemoModal, setShowDemoModal] = useState(false); useEffect(() => { const fetchRecipe = async () => { @@ -22,11 +20,6 @@ function EditRecipe() { }, [id]); const updateRecipeForm = async (recipeData: Recipe) => { - if (process.env.NODE_ENV === "demo") { - setShowDemoModal(true); - return; - } - await updateRecipe(id, recipeData); navigate(`/recipe/${id}`); }; @@ -36,13 +29,6 @@ function EditRecipe() { {recipe && ( )} - {showDemoModal && ( - setShowDemoModal(false)} - closeModal={() => setShowDemoModal(false)} - /> - )} ); } diff --git a/frontend/src/pages/RecipePage.tsx b/frontend/src/pages/RecipePage.tsx index 5043b1b..d0b41e9 100644 --- a/frontend/src/pages/RecipePage.tsx +++ b/frontend/src/pages/RecipePage.tsx @@ -7,7 +7,6 @@ import { } from "../services/frontendApi.js"; import { type Recipe } from "../types/Recipe"; import Modal from "../components/Modal.tsx"; -import DemoModal from "../components/DemoModal.tsx"; import StarRating from "../components/StarRating.tsx"; import TimeDisplay from "../components/TimeDisplay.tsx"; @@ -22,7 +21,6 @@ function RecipePage() { const [stars, setStars] = useState(0); const [initialStars, setInitialStars] = useState(null); const [showConfirmModal, setShowConfirmModal] = useState(false); - const [showDemoModal, setShowDemoModal] = useState(false); const { id } = useParams(); const isWebSource = recipe && recipe.details && recipe.details.author @@ -38,13 +36,8 @@ function RecipePage() { }; const confirmDelete = () => { - if (process.env.NODE_ENV === "demo") { - closeModal(); - setShowDemoModal(true); - } else { - handleDelete(recipe.details.id); - closeModal(); - } + handleDelete(recipe.details.id); + closeModal(); }; useEffect(() => { @@ -113,13 +106,13 @@ function RecipePage() {
); }