diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e7079dc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# Canvas Website Dockerfile +# Builds Vite frontend and serves with nginx +# Backend (sync) still uses Cloudflare Workers + +# Build stage +FROM node:20-alpine AS build +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci + +# Copy source +COPY . . + +# Build args for environment +ARG VITE_TLDRAW_WORKER_URL=https://jeffemmett-canvas.jeffemmett.workers.dev +ARG VITE_DAILY_API_KEY +ARG VITE_RUNPOD_API_KEY +ARG VITE_RUNPOD_IMAGE_ENDPOINT_ID +ARG VITE_RUNPOD_VIDEO_ENDPOINT_ID +ARG VITE_RUNPOD_TEXT_ENDPOINT_ID +ARG VITE_RUNPOD_WHISPER_ENDPOINT_ID + +# Set environment for build +ENV VITE_TLDRAW_WORKER_URL=$VITE_TLDRAW_WORKER_URL +ENV VITE_DAILY_API_KEY=$VITE_DAILY_API_KEY +ENV VITE_RUNPOD_API_KEY=$VITE_RUNPOD_API_KEY +ENV VITE_RUNPOD_IMAGE_ENDPOINT_ID=$VITE_RUNPOD_IMAGE_ENDPOINT_ID +ENV VITE_RUNPOD_VIDEO_ENDPOINT_ID=$VITE_RUNPOD_VIDEO_ENDPOINT_ID +ENV VITE_RUNPOD_TEXT_ENDPOINT_ID=$VITE_RUNPOD_TEXT_ENDPOINT_ID +ENV VITE_RUNPOD_WHISPER_ENDPOINT_ID=$VITE_RUNPOD_WHISPER_ENDPOINT_ID + +# Build the app +RUN npm run build + +# Production stage +FROM nginx:alpine AS production +WORKDIR /usr/share/nginx/html + +# Remove default nginx static assets +RUN rm -rf ./* + +# Copy built assets from build stage +COPY --from=build /app/dist . + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2b086b2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +# Canvas Website Docker Compose +# Staging deployment at staging.jeffemmett.com +# Production deployment at jeffemmett.com (once tested) + +services: + canvas-website: + build: + context: . + dockerfile: Dockerfile + args: + - VITE_TLDRAW_WORKER_URL=https://jeffemmett-canvas.jeffemmett.workers.dev + # Add other build args from .env if needed + container_name: canvas-website + restart: unless-stopped + labels: + # Staging deployment + - "traefik.enable=true" + - "traefik.http.routers.canvas-staging.rule=Host(`staging.jeffemmett.com`)" + - "traefik.http.routers.canvas-staging.entrypoints=web" + - "traefik.http.services.canvas-staging.loadbalancer.server.port=80" + - "traefik.docker.network=traefik-public" + networks: + - traefik-public + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +networks: + traefik-public: + external: true diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..fb5b259 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json; + gzip_disable "MSIE [1-6]\."; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Handle SPA routing - all routes serve index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + return 200 'OK'; + add_header Content-Type text/plain; + } +}