From 54e887e0e5f11eed50ea912b5526c6c44e3e4b3d Mon Sep 17 00:00:00 2001 From: Yavnik Sharma <17024368+Yavnik@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:22:16 +0530 Subject: [PATCH 1/5] Add Docker support and environment configuration - Created .dockerignore to exclude unnecessary files from Docker context. - Updated docker-compose.yml to include Redis service and improved PostgreSQL configuration with environment variables. - Added migration script (scripts/migrate.ts) for database migrations using drizzle-orm. - Modified Dockerfile to streamline dependency installation and build process. - Updated package.json to reflect changes in dependencies, replacing 'pg' with 'postgres' and updating 'drizzle-orm'. - Refactored database connection logic in src/db/index.ts to use postgres-js. - Added health check endpoint in src/app/api/health/route.ts for application status monitoring. - Updated start script to use bun for running migrations and starting the application. - Adjusted .env.example to include new environment variables for Redis and other configurations. --- .dockerignore | 84 +++++++++++++++++++++++++++++++++++++ .env.example | 6 ++- Dockerfile | 55 +++++++++++++----------- bun.lock | 23 +++------- docker-compose.yml | 66 ++++++++++++++++++++++------- drizzle.config.ts | 1 - package.json | 5 +-- scripts/migrate.ts | 25 +++++++++++ scripts/start.sh | 4 +- src/app/api/health/route.ts | 5 +++ src/db/index.ts | 13 +++--- 11 files changed, 214 insertions(+), 73 deletions(-) create mode 100644 .dockerignore create mode 100644 scripts/migrate.ts create mode 100644 src/app/api/health/route.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d3584a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,84 @@ +# Dependencies +node_modules +bun.lock +package-lock.json +yarn.lock + +# Next.js build output +.next +out + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Documentation +README.md +CLAUDE.md +LICENSE + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# ESLint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache +.cache +.parcel-cache + +# Temporary folders +tmp/ +temp/ + + +# Development scripts +scripts/dev* \ No newline at end of file diff --git a/.env.example b/.env.example index 3f88738..0a06a83 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,8 @@ -# Database Configuration -DATABASE_URL=postgresql://username:password@localhost:5432/command_center +NEXT_PUBLIC_BASE_URL= +# DATABASE_URL is used for local dev only; compose overrides it for containers. +DATABASE_URL=postgresql://username:password@localhost:5432/command_ops +POSTGRES_PASSWORD= # Authentication BETTER_AUTH_SECRET=your-secret-key-here BETTER_AUTH_URL=http://localhost:3000 diff --git a/Dockerfile b/Dockerfile index fadc23a..c5111bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,23 @@ # Use Bun with Node.js compatibility FROM oven/bun:1.2 AS base -# Install Node.js for tooling compatibility (drizzle-kit) -RUN apt-get update && apt-get install -y \ - curl \ - && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ - && apt-get install -y nodejs \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install dependencies only when needed +# Install dependencies for building FROM base AS deps WORKDIR /app # Copy package files COPY package.json bun.lock* ./ -# Install all dependencies, including devDependencies for drizzle-kit +# Install all dependencies for build RUN bun install --frozen-lockfile -# Rebuild the source code only when needed +# Install production dependencies for runtime (without dev tools) +FROM base AS prod-deps +WORKDIR /app +COPY package.json bun.lock* ./ +RUN bun install --frozen-lockfile --production + +# Build the application FROM base AS builder WORKDIR /app @@ -29,7 +27,18 @@ COPY --from=deps /app/node_modules ./node_modules # Copy source code COPY . . -# Build the application +# Ensure build doesn't require real runtime secrets +ENV NEXT_TELEMETRY_DISABLED=1 +ENV CI=1 +ENV DATABASE_URL=postgresql://dummy_user:dummy_password@postgres:5432/command_ops +ENV BETTER_AUTH_SECRET=BETTER_AUTH_SECRET +ENV GOOGLE_CLIENT_ID=GOOGLE_CLIENT_ID +ENV GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET +ENV GITHUB_CLIENT_ID=GITHUB_CLIENT_ID +ENV GITHUB_CLIENT_SECRET=GITHUB_CLIENT_SECRET + + +# Build Next.js (no server secrets passed at build time) RUN bun run build # Production image @@ -40,7 +49,7 @@ WORKDIR /app RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs -# Copy built application +# Copy built application artifacts COPY --from=builder /app/public ./public # Set the correct permission for prerender cache @@ -50,27 +59,23 @@ RUN mkdir .next && chown nextjs:nodejs .next COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -# Copy migration files and scripts -COPY --from=deps /app/node_modules ./node_modules +# Copy migrations and migrator script COPY --chown=nextjs:nodejs drizzle ./drizzle -COPY --chown=nextjs:nodejs src/db ./src/db -COPY --chown=nextjs:nodejs drizzle.config.ts ./ -COPY --chown=nextjs:nodejs package.json ./ -COPY --chown=nextjs:nodejs scripts/start.sh ./start.sh - -RUN chmod +x ./start.sh +COPY --chown=nextjs:nodejs scripts/migrate.ts ./scripts/migrate.ts +COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./node_modules -# Switch to non-root user +# Run as non-root USER nextjs EXPOSE 3000 +ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME="0.0.0.0" # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ - CMD bun --version || exit 1 + CMD curl -f http://localhost:3000/api/health || exit 1 -# Start the application with migrations -CMD ["./start.sh"] \ No newline at end of file +# Start the application +CMD ["bun", "server.js"] \ No newline at end of file diff --git a/bun.lock b/bun.lock index 973fc1a..0092c34 100644 --- a/bun.lock +++ b/bun.lock @@ -23,15 +23,14 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", - "dotenv": "^17.2.1", - "drizzle-orm": "^0.44.3", + "drizzle-orm": "^0.44.6", "framer-motion": "^12.23.11", "ioredis": "^5.6.1", "lucide-react": "^0.525.0", "nanoid": "^5.1.5", "next": "15.3.4", "next-themes": "^0.4.6", - "pg": "^8.16.3", + "postgres": "^3.4.7", "posthog-js": "^1.258.2", "posthog-node": "^5.6.0", "react": "^19.1.1", @@ -598,11 +597,9 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - "dotenv": ["dotenv@17.2.1", "", {}, "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ=="], - "drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], - "drizzle-orm": ["drizzle-orm@0.44.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-8nIiYQxOpgUicEL04YFojJmvC4DNO4KoyXsEIqN44+g6gNBr6hmVpWk3uyAt4CaTiRGDwoU+alfqNNeonLAFOQ=="], + "drizzle-orm": ["drizzle-orm@0.44.6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -936,22 +933,12 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], - - "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], - - "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], - "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], - "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], @@ -960,6 +947,8 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], @@ -1062,8 +1051,6 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], diff --git a/docker-compose.yml b/docker-compose.yml index 406c6cc..cc906a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,44 +1,80 @@ version: "3.8" services: - # PostgreSQL Database postgres: image: postgres:17-alpine - container_name: command-ops-db environment: POSTGRES_DB: command_ops POSTGRES_USER: postgres - POSTGRES_PASSWORD: password + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data - ports: - - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 + restart: unless-stopped + + redis: + image: redis:7-alpine + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + healthcheck: + test: ["CMD-SHELL", 'redis-cli -a "$REDIS_PASSWORD" PING | grep PONG'] + interval: 10s + timeout: 3s + retries: 5 + restart: unless-stopped + + migrate: + build: + context: . + dockerfile: Dockerfile + image: command-ops:latest + env_file: + - .env + environment: + DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/command_ops + depends_on: + postgres: + condition: service_healthy + command: ["bun", "./scripts/migrate.ts"] + restart: "no" - # Development App app: + image: command-ops:latest build: context: . dockerfile: Dockerfile - container_name: command-ops-app + env_file: + - .env environment: - DATABASE_URL: postgresql://postgres:password@postgres:5432/command_ops - BETTER_AUTH_SECRET: dev-secret-key-change-in-production - BETTER_AUTH_URL: http://localhost:3000 - NODE_ENV: development + DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/command_ops + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD} + REDIS_DB: 0 + NODE_ENV: production + NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL:-http://localhost:3000} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} + GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-} + NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY:-} + NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST:-} ports: - "3000:3000" depends_on: postgres: condition: service_healthy - volumes: - - .:/app - - /app/node_modules - command: bun dev + redis: + condition: service_healthy + migrate: + condition: service_completed_successfully + restart: unless-stopped volumes: postgres_data: + redis_data: diff --git a/drizzle.config.ts b/drizzle.config.ts index 0b5c1c9..ae29231 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,4 +1,3 @@ -import 'dotenv/config'; import { defineConfig } from 'drizzle-kit'; export default defineConfig({ diff --git a/package.json b/package.json index ca0586d..34d4d89 100644 --- a/package.json +++ b/package.json @@ -32,15 +32,14 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", - "dotenv": "^17.2.1", - "drizzle-orm": "^0.44.3", + "drizzle-orm": "^0.44.6", "framer-motion": "^12.23.11", "ioredis": "^5.6.1", "lucide-react": "^0.525.0", "nanoid": "^5.1.5", "next": "15.3.4", "next-themes": "^0.4.6", - "pg": "^8.16.3", + "postgres": "^3.4.7", "posthog-js": "^1.258.2", "posthog-node": "^5.6.0", "react": "^19.1.1", diff --git a/scripts/migrate.ts b/scripts/migrate.ts new file mode 100644 index 0000000..c08f16c --- /dev/null +++ b/scripts/migrate.ts @@ -0,0 +1,25 @@ +import postgres from 'postgres'; +import { drizzle } from 'drizzle-orm/postgres-js'; +import { migrate } from 'drizzle-orm/postgres-js/migrator'; + +const url = process.env.DATABASE_URL; +if (!url) { + console.error('DATABASE_URL is required'); + process.exit(1); +} + +const client = postgres(url, { max: 1 }); +const db = drizzle(client); + +try { + await migrate(db, { migrationsFolder: 'drizzle' }); + console.log('Migrations applied successfully'); + process.exit(0); +} catch (err) { + console.error('Migration failed:', err); + process.exit(1); +} finally { + await client.end({ timeout: 1 }); +} + + diff --git a/scripts/start.sh b/scripts/start.sh index 0587572..63ec7f7 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -2,7 +2,7 @@ set -e echo "Running database migrations..." -npx drizzle-kit migrate +bunx drizzle-kit migrate -echo "Starting application..." +echo "Starting Command Ops application..." exec bun server.js \ No newline at end of file diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..03a8fb7 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,5 @@ +export async function GET() { + return Response.json({ ok: true }); +} + + diff --git a/src/db/index.ts b/src/db/index.ts index a72272b..7018a5b 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,12 +1,11 @@ -import 'dotenv/config'; -import { drizzle } from 'drizzle-orm/node-postgres'; +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; -function validateDatabaseUrl(): string { +function requireDatabaseUrl(): string { const url = process.env.DATABASE_URL; - if (!url) { - throw new Error('DATABASE_URL environment variable is required'); - } + if (!url) throw new Error('DATABASE_URL is required'); return url; } -export const db = drizzle(validateDatabaseUrl()); +const client = postgres(requireDatabaseUrl()); +export const db = drizzle(client); \ No newline at end of file From 85c77622860ab426085b6b36006bb0723a96b91c Mon Sep 17 00:00:00 2001 From: Yavnik Sharma <17024368+Yavnik@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:30:47 +0530 Subject: [PATCH 2/5] Enhance environment configuration and add deployment documentation - Updated .env.example to include required variables for Docker deployment and local development. - Created DEPLOY.md for a comprehensive self-hosting guide, detailing setup and configuration steps. - Modified Dockerfile to use a smaller Alpine base image and improved health check command. - Updated README.md to reflect new self-hosting capabilities and quick start instructions. --- .env.example | 55 ++++++++++++++++++------- DEPLOY.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 24 +++++------ README.md | 107 +++++++++++++++++++++++++++++++---------------- 4 files changed, 237 insertions(+), 63 deletions(-) create mode 100644 DEPLOY.md diff --git a/.env.example b/.env.example index 0a06a83..a774e0b 100644 --- a/.env.example +++ b/.env.example @@ -1,27 +1,52 @@ -NEXT_PUBLIC_BASE_URL= +# =========================================== +# REQUIRED FOR DOCKER DEPLOYMENT +# =========================================== + +# Your public URL (e.g., https://commandops.yourdomain.com or http://localhost:3000) +NEXT_PUBLIC_BASE_URL=http://localhost:3000 + +# Authentication secret - Generate with: openssl rand -base64 32 +BETTER_AUTH_SECRET=your-super-secure-secret-key-min-32-chars + +# Database password for PostgreSQL (used by Docker Compose) +POSTGRES_PASSWORD=your-secure-db-password + +# Redis password (used by Docker Compose) +REDIS_PASSWORD=your-secure-redis-password + +# =========================================== +# REQUIRED FOR LOCAL DEVELOPMENT ONLY +# =========================================== + +# Database URL (for local dev only; Docker Compose overrides this) +DATABASE_URL=postgresql://postgres:your-secure-db-password@localhost:5432/command_ops -# DATABASE_URL is used for local dev only; compose overrides it for containers. -DATABASE_URL=postgresql://username:password@localhost:5432/command_ops -POSTGRES_PASSWORD= -# Authentication -BETTER_AUTH_SECRET=your-secret-key-here BETTER_AUTH_URL=http://localhost:3000 -# Google OAuth +# =========================================== +# OAUTH (Currently Required) +# =========================================== + +# Google OAuth (get from Google Cloud Console) GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= -# GitHub OAuth +# GitHub OAuth (get from GitHub Developer Settings) GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= -# Self-hosted Redis for Rate Limiting -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= # Leave empty if no password -REDIS_DB=0 # Default database +# =========================================== +# OPTIONAL - ANALYTICS +# =========================================== - -# PostHog Configuration +# PostHog Analytics (optional) NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_HOST= + +# =========================================== +# LOCAL DEV REDIS SETTINGS (Not needed for Docker) +# =========================================== + +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..9c7ed26 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,114 @@ +# Self-Hosting Guide + +Deploy Command Ops on your own infrastructure using Docker. + +## Quick Start + +1. **Clone and configure:** + ```bash + git clone https://github.com/Yavnik/commandops.git + cd commandops + cp .env.example .env + ``` + +2. **Edit your `.env` file:** + ```bash + nano .env + ``` + + Set these required variables: + - `NEXT_PUBLIC_BASE_URL` - Your domain (e.g., `https://commandops.yourdomain.com` or `http://192.168.1.100:3000`) + - `BETTER_AUTH_SECRET` - Random string, 32+ characters (generate with `openssl rand -base64 32`) + - `POSTGRES_PASSWORD` - Secure database password + - `REDIS_PASSWORD` - Secure Redis password + +3. **Deploy:** + ```bash + docker compose up -d --build + ``` + +4. **Access your instance:** + + Open `http://localhost:3000` (or your configured domain) and create your account. + +That's it! Your self-hosted Command Ops is ready. + +## Configuration + +### Required Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `NEXT_PUBLIC_BASE_URL` | Public URL of your deployment | `https://commandops.example.com` | +| `BETTER_AUTH_SECRET` | Auth secret key (32+ chars) | `your-random-secret-key-here` | +| `POSTGRES_PASSWORD` | Database password | `secure_db_password` | +| `REDIS_PASSWORD` | Redis password | `secure_redis_password` | + +### OAuth Login (Currently Required) + +> **Note:** Google and GitHub OAuth credentials are currently required for authentication. We're working on making them optional for self-hosted instances. Stay tuned! + +**Google OAuth:** + +1. Create OAuth credentials at [Google Cloud Console](https://console.cloud.google.com/) +2. Add authorized origins and redirect URIs +3. Set in `.env`: + ```bash + GOOGLE_CLIENT_ID=your_client_id + GOOGLE_CLIENT_SECRET=your_client_secret + ``` + +**GitHub OAuth:** + +1. Create OAuth App at [GitHub Settings > Developer settings](https://github.com/settings/developers) +2. Set callback URL to `{YOUR_DOMAIN}/api/auth/callback/github` +3. Set in `.env`: + ```bash + GITHUB_CLIENT_ID=your_client_id + GITHUB_CLIENT_SECRET=your_client_secret + ``` + +### Optional: Analytics + +To enable PostHog analytics: +```bash +NEXT_PUBLIC_POSTHOG_KEY=your_posthog_key +NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com +``` + +## Maintenance + +### View Logs + +```bash +docker compose logs -f # All services +docker compose logs -f app # Application only +docker compose logs -f postgres # Database only +``` + +### Update to Latest Version + +```bash +git pull +docker compose down +docker compose up -d --build +``` + +## What's Included + +The `docker-compose.yml` sets up: +- **App**: Next.js application (exposed on port 3000) +- **PostgreSQL**: Database with persistent storage +- **Redis**: For rate limiting and caching +- **Migrations**: Automatic database schema setup on first run + +All data is stored in Docker volumes and persists across restarts. + +## Need Help? + +- Check logs: `docker compose logs -f` +- Ensure all required environment variables are set +- Verify Docker and Docker Compose are up to date +- [Open an issue](https://github.com/Yavnik/commandops/issues) on GitHub + +--- \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c5111bb..1151d00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Use Bun with Node.js compatibility -FROM oven/bun:1.2 AS base +# Use Bun with Alpine for smaller base image +FROM oven/bun:1.2-alpine AS base # Install dependencies for building FROM base AS deps @@ -11,12 +11,6 @@ COPY package.json bun.lock* ./ # Install all dependencies for build RUN bun install --frozen-lockfile -# Install production dependencies for runtime (without dev tools) -FROM base AS prod-deps -WORKDIR /app -COPY package.json bun.lock* ./ -RUN bun install --frozen-lockfile --production - # Build the application FROM base AS builder WORKDIR /app @@ -59,10 +53,14 @@ RUN mkdir .next && chown nextjs:nodejs .next COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -# Copy migrations and migrator script +# Copy migrations and migrator script with dependencies COPY --chown=nextjs:nodejs drizzle ./drizzle COPY --chown=nextjs:nodejs scripts/migrate.ts ./scripts/migrate.ts -COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./node_modules + +# Copy node_modules needed for migration script +# The standalone build doesn't include these, so we copy them from deps +COPY --from=deps --chown=nextjs:nodejs /app/node_modules/postgres ./node_modules/postgres +COPY --from=deps --chown=nextjs:nodejs /app/node_modules/drizzle-orm ./node_modules/drizzle-orm # Run as non-root USER nextjs @@ -73,9 +71,9 @@ ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -# Health check +# Health check using wget (already available in Alpine) HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:3000/api/health || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 # Start the application -CMD ["bun", "server.js"] \ No newline at end of file +CMD ["bun", "server.js"] diff --git a/README.md b/README.md index 8a1dfeb..4fe0f7c 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,51 @@ # Command Ops - Life Management System -Transform your daily productivity into an engaging sci-fi command ops experience. This app gamifies task management with a futuristic military/gaming interface. +> An open-source, self-hostable task management system with a sci-fi command center interface -## Features +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://www.docker.com/) +[![Next.js](https://img.shields.io/badge/Next.js-15-black?logo=next.js&logoColor=white)](https://nextjs.org/) + +Transform your daily productivity into an engaging sci-fi command ops experience. Command Ops is a fully open-source task management app that you can self-host on your own infrastructure - own your data, customize your experience. + +## ✨ Features - **Quest System**: Tasks are "Quests" with sub-tasks as "Objectives" - **XP & Leveling**: Gain experience and rank up as you complete quests - **Priority Levels**: Critical, High, Standard, and Low priority missions - **Performance Tracking**: Monitor your success rate and weekly stats - **Immersive Theme**: Dark sci-fi UI with glowing elements and animations -- **Database**: PostgreSQL with Drizzle ORM for persistent data storage +- **Self-Hostable**: Full control over your data with Docker deployment +- **PostgreSQL Backend**: Robust database with Drizzle ORM -## Getting Started +## 🚀 Quick Start (Self-Hosting) -### Installation +Deploy Command Ops on your own server in minutes with Docker: ```bash # Clone the repository git clone https://github.com/Yavnik/commandops.git cd commandops -# Install dependencies -bun install - -# Set up environment variables +# Configure your environment cp .env.example .env -# Edit .env with your database credentials and other settings - -# Set up the database -bunx drizzle-kit push +nano .env # Set your secrets and domain -# Run the development server -bun dev +# Deploy with Docker +docker compose up -d --build ``` -Open [http://localhost:3000](http://localhost:3000) to access the Command Ops. +Open `http://localhost:3000` and start your first mission! + +📖 **[Full Deployment Guide](DEPLOY.md)** - Complete instructions for self-hosting + +## 🎯 Why Self-Host? + +- **Data Privacy**: Your tasks, your server, your control +- **Customization**: Modify and extend to fit your workflow +- **No Vendor Lock-in**: Own your productivity data forever +- **Free Forever**: No subscriptions or usage limits +- **Open Source**: Transparent code you can audit and trust ## Usage @@ -63,33 +74,59 @@ Open [http://localhost:3000](http://localhost:3000) to access the Command Ops. - Theme customization (Quartermaster) - Campaign system for grouping related quests -## Development +## 💻 Local Development -### Database Management +Want to contribute or customize Command Ops? Set up a local development environment: ```bash -# Generate new migrations after schema changes -bunx drizzle-kit generate +# Install dependencies +bun install -# Apply migrations to database -bunx drizzle-kit migrate +# Set up environment variables +cp .env.example .env + +# Initialize database +bunx drizzle-kit push + +# Start development server +bun dev +``` -# Open Drizzle Studio for database inspection -bunx drizzle-kit studio +### Development Commands + +```bash +bun dev # Start dev server with Turbopack +bun run build # Build for production +bun run lint # Run ESLint +bunx drizzle-kit generate # Generate database migrations +bunx drizzle-kit studio # Open Drizzle Studio ``` -### Code Structure +### Architecture + +- **Next.js 15** with App Router and Turbopack +- **TypeScript** with strict mode +- **PostgreSQL** + Drizzle ORM +- **Better Auth** for authentication +- **Zustand** for client state +- **Tailwind CSS** with custom sci-fi themes + +## 🤝 Contributing + +Contributions are welcome! Whether it's bug fixes, new features, or documentation improvements. Raise a PR or bring it up in the issues section. + +## 📄 License + +This project is open source and available under the [MIT License](LICENSE). -The app follows Next.js 15 App Router conventions with a clear separation between server and client components: +## 🙏 Support -- **Server Components**: Handle data fetching and server-side logic -- **Client Components**: Handle user interactions and client-side state -- **Server Actions**: Handle form submissions and mutations -- **Optimistic Updates**: Zustand store implements optimistic UI updates with server sync +If you find Command Ops useful, consider: +- ⭐ Starring the repository +- 🐛 Reporting bugs or requesting features via [Issues](https://github.com/Yavnik/commandops/issues) +- 🔧 Contributing code or documentation +- 📢 Sharing with others who might benefit -### Key Patterns +--- -- **Progressive Hydration**: `StoreInitializer` prevents hydration mismatches -- **Error Boundaries**: Comprehensive error handling with recovery options -- **Type Safety**: Strict TypeScript with proper type definitions -- **Theme System**: Custom CSS variables for three sci-fi themes \ No newline at end of file +**Built with ❤️ for self-hosters and productivity enthusiasts** \ No newline at end of file From 552cad55f953450eebf68fa95171dc7b78a7ff64 Mon Sep 17 00:00:00 2001 From: Yavnik Sharma <17024368+Yavnik@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:34:50 +0530 Subject: [PATCH 3/5] Update package.json with project metadata and keywords --- package.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/package.json b/package.json index 34d4d89..cc2189d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,25 @@ { "name": "command-ops", "version": "0.1.0", + "description": "An open-source, self-hostable task management system with a sci-fi command center interface", + "author": "Yavnik Sharma", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Yavnik/commandops.git" + }, + "bugs": { + "url": "https://github.com/Yavnik/commandops/issues" + }, + "homepage": "https://github.com/Yavnik/commandops#readme", + "keywords": [ + "task-management", + "productivity", + "self-hosted", + "docker", + "nextjs", + "gamification" + ], "private": true, "scripts": { "dev": "next dev --turbopack", From c9348f6a4fbb51946d130a798312e62366e2f039 Mon Sep 17 00:00:00 2001 From: Yavnik Sharma <17024368+Yavnik@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:35:52 +0530 Subject: [PATCH 4/5] Update src/app/api/health/route.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/api/health/route.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 03a8fb7..9146308 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,5 +1,3 @@ export async function GET() { return Response.json({ ok: true }); } - - From 6998d16b0c404d6ff406d135480c488ce74fde22 Mon Sep 17 00:00:00 2001 From: Yavnik Sharma <17024368+Yavnik@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:36:14 +0530 Subject: [PATCH 5/5] Update Dockerfile Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1151d00..c972e67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,6 @@ ENV GOOGLE_CLIENT_ID=GOOGLE_CLIENT_ID ENV GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET ENV GITHUB_CLIENT_ID=GITHUB_CLIENT_ID ENV GITHUB_CLIENT_SECRET=GITHUB_CLIENT_SECRET - - # Build Next.js (no server secrets passed at build time) RUN bun run build