diff --git a/Pottery-website/.dockerignore b/Pottery-website/.dockerignore new file mode 100644 index 0000000..ce670f9 --- /dev/null +++ b/Pottery-website/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.git +.gitignore +README.md +npm-debug.log +server/node_modules +server/.env +.env diff --git a/Pottery-website/.env.example b/Pottery-website/.env.example new file mode 100644 index 0000000..beb8462 --- /dev/null +++ b/Pottery-website/.env.example @@ -0,0 +1,4 @@ +SITE_HOST=example.com +POSTGRES_DB=pottery_db +POSTGRES_USER=pottery +POSTGRES_PASSWORD=change-this-password diff --git a/Pottery-website/Caddyfile b/Pottery-website/Caddyfile new file mode 100644 index 0000000..61a4dfe --- /dev/null +++ b/Pottery-website/Caddyfile @@ -0,0 +1,10 @@ +{$SITE_HOST:localhost} { + encode zstd gzip + + @api path /api/* /health + reverse_proxy @api backend:5000 + + root * /srv + try_files {path} /index.html + file_server +} diff --git a/Pottery-website/Dockerfile.frontend b/Pottery-website/Dockerfile.frontend new file mode 100644 index 0000000..1b981f4 --- /dev/null +++ b/Pottery-website/Dockerfile.frontend @@ -0,0 +1,12 @@ +FROM node:22-alpine AS build +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build:docker + +FROM caddy:2-alpine +COPY Caddyfile /etc/caddy/Caddyfile +COPY --from=build /app/dist /srv diff --git a/Pottery-website/README.md b/Pottery-website/README.md index e28fc4f..736f5fa 100644 --- a/Pottery-website/README.md +++ b/Pottery-website/README.md @@ -115,4 +115,30 @@ Access the dashboard by navigating to `/admin`. --- -**Crafted with care by Antigravity.** +**Crafted with care by Antigravity.** + +## Docker Deployment with Caddy + +For a self-hosted deployment, this repository now includes: + +- `Dockerfile.frontend`: Builds the Vite frontend and serves it with Caddy. +- `server/Dockerfile`: Runs the Node/Express API. +- `docker-compose.yml`: Starts Caddy, the API, and PostgreSQL together. +- `server/init.sql`: Initializes the database schema on first startup. + +### Quick Start + +1. Copy `.env.example` to `.env`. +2. Set `SITE_HOST` to your domain name. +3. Set a strong `POSTGRES_PASSWORD`. +4. Start the stack: + +```bash +docker compose up -d --build +``` + +### Notes + +- Caddy terminates TLS automatically when `SITE_HOST` points to a public domain with correct DNS. +- The frontend uses `/api` in production, and Caddy proxies that path to the backend container. +- PostgreSQL data is stored in the `postgres_data` Docker volume. diff --git a/Pottery-website/docker-compose.yml b/Pottery-website/docker-compose.yml new file mode 100644 index 0000000..aa4b29c --- /dev/null +++ b/Pottery-website/docker-compose.yml @@ -0,0 +1,63 @@ +services: + caddy: + build: + context: . + dockerfile: Dockerfile.frontend + container_name: pottery-caddy + depends_on: + backend: + condition: service_healthy + environment: + SITE_HOST: ${SITE_HOST} + ports: + - "80:80" + - "443:443" + volumes: + - caddy_data:/data + - caddy_config:/config + restart: unless-stopped + + backend: + build: + context: ./server + dockerfile: Dockerfile + container_name: pottery-backend + depends_on: + db: + condition: service_healthy + environment: + PORT: 5000 + DB_USER: ${POSTGRES_USER} + DB_HOST: db + DB_NAME: ${POSTGRES_DB} + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_PORT: 5432 + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/health"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 15s + + db: + image: postgres:16-alpine + container_name: pottery-db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./server/init.sql:/docker-entrypoint-initdb.d/00-init.sql:ro + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + postgres_data: + caddy_data: + caddy_config: diff --git a/Pottery-website/package.json b/Pottery-website/package.json index 02335e7..18958ac 100644 --- a/Pottery-website/package.json +++ b/Pottery-website/package.json @@ -3,12 +3,13 @@ "private": true, "version": "0.0.0", "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "postbuild": "react-snap", - "preview": "vite preview" - }, + "scripts": { + "dev": "vite", + "build": "vite build", + "build:docker": "vite build", + "postbuild": "react-snap", + "preview": "vite preview" + }, "reactSnap": { "source": "dist", "include": [ diff --git a/Pottery-website/server/Dockerfile b/Pottery-website/server/Dockerfile new file mode 100644 index 0000000..dcbb8ff --- /dev/null +++ b/Pottery-website/server/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-alpine +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --omit=dev + +COPY . . + +EXPOSE 5000 + +CMD ["node", "index.js"] diff --git a/Pottery-website/server/index.js b/Pottery-website/server/index.js index 42309bd..a9b172b 100644 --- a/Pottery-website/server/index.js +++ b/Pottery-website/server/index.js @@ -3,8 +3,8 @@ const { Pool } = require('pg'); const cors = require('cors'); require('dotenv').config(); -const app = express(); -const port = process.env.PORT || 5000; +const app = express(); +const port = process.env.PORT || 5000; // Middleware app.use(cors()); @@ -20,12 +20,22 @@ const pool = new Pool({ port: process.env.DB_PORT, }); -pool.on('error', (err) => { - console.error('Unexpected error on idle client', err); - process.exit(-1); -}); - -// Routes +pool.on('error', (err) => { + console.error('Unexpected error on idle client', err); + process.exit(-1); +}); + +app.get('/health', async (_req, res) => { + try { + await pool.query('SELECT 1'); + res.json({ ok: true }); + } catch (err) { + console.error('Healthcheck failed', err); + res.status(500).json({ ok: false }); + } +}); + +// Routes // --- PRODUCTS --- app.get('/api/products', async (req, res) => { diff --git a/Pottery-website/server/init.sql b/Pottery-website/server/init.sql new file mode 100644 index 0000000..45a0a09 --- /dev/null +++ b/Pottery-website/server/init.sql @@ -0,0 +1,36 @@ +CREATE TABLE IF NOT EXISTS products ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + price DECIMAL(10, 2) NOT NULL, + image TEXT NOT NULL, + description TEXT, + gallery JSONB DEFAULT '[]'::jsonb, + slug TEXT, + number TEXT, + aspect_ratio TEXT, + details JSONB DEFAULT '[]'::jsonb +); + +CREATE TABLE IF NOT EXISTS articles ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + date VARCHAR(50) NOT NULL, + image TEXT NOT NULL, + sections JSONB DEFAULT '[]'::jsonb, + slug TEXT, + category TEXT, + description TEXT, + is_featured BOOLEAN DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS orders ( + id SERIAL PRIMARY KEY, + customer_email TEXT NOT NULL, + customer_name TEXT NOT NULL, + shipping_address JSONB NOT NULL, + items JSONB NOT NULL, + total_amount DECIMAL(10, 2) NOT NULL, + payment_status TEXT DEFAULT 'pending', + shipping_status TEXT DEFAULT 'pending', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); diff --git a/Pottery-website/src/context/StoreContext.tsx b/Pottery-website/src/context/StoreContext.tsx index 1217ded..55ae5de 100644 --- a/Pottery-website/src/context/StoreContext.tsx +++ b/Pottery-website/src/context/StoreContext.tsx @@ -43,7 +43,7 @@ interface StoreContextType { const StoreContext = createContext(undefined); -const API_URL = 'http://localhost:5000/api'; +const API_URL = import.meta.env.VITE_API_URL || '/api'; export const StoreProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [products, setProducts] = useState([]); @@ -95,10 +95,10 @@ export const StoreProvider: React.FC<{ children: ReactNode }> = ({ children }) = fetch(`${API_URL}/products`), fetch(`${API_URL}/articles`) ]); - const prods = await prodRes.json(); - const arts = await artRes.json(); - setProducts(prods); - setArticles(arts); + const prods = await prodRes.json(); + const arts = await artRes.json(); + setProducts(Array.isArray(prods) && prods.length > 0 ? prods : COLLECTIONS); + setArticles(Array.isArray(arts) && arts.length > 0 ? arts : JOURNAL_ENTRIES); } catch (err) { console.error('Failed to fetch data from backend, falling back to static data', err); setProducts(COLLECTIONS);