diff --git a/.env.example b/.env.example index eac4eb9..e2957c5 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,6 @@ POSTGRES_DB=flockpal POSTGRES_USER=flockpal POSTGRES_PASSWORD=change_me_for_production FRONTEND_URL=http://localhost:3000 +BACKEND_URL=http://localhost:5000 VITE_API_BASE_URL=http://localhost:5000/api NODE_ENV=development diff --git a/README.md b/README.md index cbdb1a4..d162b95 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ FlockPal is a Dockerized TypeScript app for tracking flock health with a clean, - Scheduled reminder delivery for birthdays, gotcha days, and care events - Audit logging for workspace access changes and bird transfers -## Run locally +## Development 1. Copy `.env.example` to `.env` if you want custom settings. -2. Start the stack: +2. Start the development stack: ```bash docker compose up --build @@ -39,6 +39,24 @@ docker compose up --build 3. Open `http://localhost:3000`. 4. The API health check is available at `http://localhost:5000/api/health`. +The default `docker-compose.yml` is development-only. It mounts source files, installs dev dependencies, and runs the backend and frontend in watch mode. + +## Production + +1. Set production values in your environment or `.env`, especially: + - `POSTGRES_PASSWORD` + - `FRONTEND_URL` + - `BACKEND_URL` + - `VITE_API_BASE_URL` +2. Build and start the production stack: + +```bash +docker compose -f docker-compose.prod.yml up --build -d +``` + +3. The production backend runs the compiled Node app from `dist/app.js`. +4. The production frontend is built with Vite and served by Nginx on port `3000`. + ## Auth and workspace notes - One user can belong to multiple workspaces. diff --git a/backend/Dockerfile b/backend/Dockerfile index 6e23554..8f6b45e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,16 @@ -FROM node:22-alpine +FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm ci COPY tsconfig.json ./ COPY src ./src +RUN npm run build + +FROM node:22-alpine AS production +WORKDIR /app +ENV NODE_ENV=production +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=build /app/dist ./dist EXPOSE 5000 -CMD ["npm", "run", "dev"] +CMD ["npm", "run", "start"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..a3bc9f4 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM node:22-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install --include=dev +COPY tsconfig.json ./ +COPY src ./src +EXPOSE 5000 +CMD ["npm", "run", "dev"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..5bfbce4 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,85 @@ +services: + postgres: + image: postgres:16-alpine + container_name: flockpal-postgres + environment: + POSTGRES_DB: ${POSTGRES_DB:-flockpal} + POSTGRES_USER: ${POSTGRES_USER:-flockpal} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD for production} + volumes: + - ./data/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-flockpal} -d ${POSTGRES_DB:-flockpal}"] + interval: 10s + timeout: 5s + retries: 10 + restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: flockpal-backend + environment: + PORT: 5000 + NODE_ENV: production + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_DB: ${POSTGRES_DB:-flockpal} + POSTGRES_USER: ${POSTGRES_USER:-flockpal} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD for production} + FRONTEND_URL: ${FRONTEND_URL:?set FRONTEND_URL for production} + BACKEND_URL: ${BACKEND_URL:?set BACKEND_URL for production} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} + MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-} + MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-} + APPLE_CLIENT_ID: ${APPLE_CLIENT_ID:-} + APPLE_CLIENT_SECRET: ${APPLE_CLIENT_SECRET:-} + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_SECURE: ${SMTP_SECURE:-false} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASS: ${SMTP_PASS:-} + SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-} + SMTP_FROM_NAME: ${SMTP_FROM_NAME:-FlockPal} + depends_on: + postgres: + condition: service_healthy + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.flockpal-api.rule=Host(`${URL:?set URL for production}`) && PathPrefix(`/api`) + - traefik.http.routers.flockpal-api.entrypoints=websecure + - traefik.http.routers.flockpal-api.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt} + - traefik.http.routers.flockpal-api.priority=100 + - traefik.http.services.flockpal-api.loadbalancer.server.port=5000 + networks: + - default + - traefik + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_BASE_URL: ${VITE_API_BASE_URL:-/api} + container_name: flockpal-frontend + depends_on: + - backend + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.flockpal-web.rule=Host(`${URL:?set URL for production}`) + - traefik.http.routers.flockpal-web.entrypoints=websecure + - traefik.http.routers.flockpal-web.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt} + - traefik.http.routers.flockpal-web.priority=10 + - traefik.http.services.flockpal-web.loadbalancer.server.port=80 + networks: + - traefik + restart: unless-stopped + +networks: + traefik: + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 0b688d0..3258123 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,11 +17,11 @@ services: backend: build: context: ./backend - dockerfile: Dockerfile + dockerfile: Dockerfile.dev container_name: flockpal-backend environment: PORT: 5000 - NODE_ENV: ${NODE_ENV:-development} + NODE_ENV: development POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_DB: ${POSTGRES_DB:-flockpal} @@ -48,7 +48,7 @@ services: ports: - "5000:5000" command: > - sh -c "npm install && + sh -c "npm install --include=dev && npm run dev" volumes: - ./backend:/app @@ -66,7 +66,7 @@ services: ports: - "3000:3000" command: > - sh -c "npm install && + sh -c "npm install --include=dev && npm run dev -- --host" volumes: - ./frontend:/app diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..86caab1 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22-alpine AS build +WORKDIR /app +ARG VITE_API_BASE_URL=http://localhost:5000/api +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +COPY package*.json ./ +RUN npm ci +COPY tsconfig*.json ./ +COPY vite.config.ts ./ +COPY index.html ./ +COPY src ./src +RUN npm run build + +FROM nginx:1.27-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..79fd959 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +}