diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7802814 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,68 @@ +# Dependencies +node_modules +app/node_modules +supabase/node_modules + +# Build outputs +.nuxt +app/.nuxt +.output +app/.output +dist +app/dist + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Environment files (use docker env vars instead) +.env +.env.local +.env.*.local +app/.env +app/.env.local + +# Editor directories and files +.vscode/ +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.DS_Store + +# Git +.git +.gitignore +.gitattributes + +# CI/CD +.github +.gitea + +# Documentation +*.md +docs/ +!README.md + +# Tests +test/ +tests/ +*.spec.js +*.spec.ts +*.test.js +*.test.ts + +# Temporary files +*.tmp +*.bak +*.swp +.cache + +# Supabase (handled separately) +supabase/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e2dae4f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +# Pantry Production Dockerfile +# Multi-stage build for optimized production image + +# Stage 1: Build the Nuxt application +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY app/package*.json ./ + +# Install dependencies +RUN npm ci --only=production && \ + npm cache clean --force + +# Copy application source +COPY app/ ./ + +# Build the application +RUN npm run build + +# Stage 2: Production runtime +FROM node:20-alpine AS runner + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Create app user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Set working directory +WORKDIR /app + +# Copy built application from builder +COPY --from=builder --chown=nodejs:nodejs /app/.output /app/.output +COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./ + +# Switch to non-root user +USER nodejs + +# Expose port +EXPOSE 3000 + +# Set environment variables +ENV NODE_ENV=production \ + HOST=0.0.0.0 \ + PORT=3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Use dumb-init to handle signals properly +ENTRYPOINT ["dumb-init", "--"] + +# Start the application +CMD ["node", ".output/server/index.mjs"] diff --git a/app/server/api/health.get.ts b/app/server/api/health.get.ts new file mode 100644 index 0000000..7587b60 --- /dev/null +++ b/app/server/api/health.get.ts @@ -0,0 +1,12 @@ +/** + * Health check endpoint for container monitoring + * + * Returns 200 OK if the server is running + */ +export default defineEventHandler(() => { + return { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime() + } +}) diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..5549c89 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,90 @@ +# Docker Deployment + +## Production Dockerfile + +The production Dockerfile uses a multi-stage build for optimized image size and security. + +### Build the image + +```bash +docker build -t pantry:latest -f Dockerfile . +``` + +### Run the container + +```bash +docker run -d \ + --name pantry \ + -p 3000:3000 \ + -e NUXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co \ + -e NUXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \ + pantry:latest +``` + +### Environment Variables + +Required: +- `NUXT_PUBLIC_SUPABASE_URL` - Your Supabase project URL +- `NUXT_PUBLIC_SUPABASE_ANON_KEY` - Your Supabase anon/public key + +Optional: +- `PORT` - Port to listen on (default: 3000) +- `HOST` - Host to bind to (default: 0.0.0.0) + +### Health Check + +The container includes a health check endpoint at `/api/health` + +```bash +curl http://localhost:3000/api/health +``` + +Expected response: +```json +{ + "status": "ok", + "timestamp": "2026-02-25T00:00:00.000Z", + "uptime": 123.456 +} +``` + +### Image Features + +- **Multi-stage build**: Separate build and runtime stages +- **Alpine Linux**: Minimal base image (~50MB base) +- **Non-root user**: Runs as unprivileged user (nodejs:1001) +- **dumb-init**: Proper signal handling and zombie reaping +- **Health checks**: Built-in container health monitoring +- **Production-optimized**: Only production dependencies included + +### Image Size + +Approximate sizes: +- Base Alpine + Node.js: ~50MB +- Dependencies: ~150MB +- Built app: ~20MB +- **Total**: ~220MB + +### Security + +- Runs as non-root user (nodejs) +- No unnecessary packages +- Minimal attack surface +- Regular security updates via Alpine base + +### Troubleshooting + +View logs: +```bash +docker logs pantry +``` + +Interactive shell: +```bash +docker exec -it pantry sh +``` + +Check health: +```bash +docker inspect --format='{{json .State.Health}}' pantry +```