Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# .dockerignore — loại trừ các file không cần copy vào Docker image
# Đặt ở root của project (cùng cấp với Dockerfile)

# Git
.git
.gitignore
.github

# Dependencies (sẽ được install lại trong container)
src/node_modules

# Build output (sẽ được build lại trong container)
src/.next
src/coverage

# Dev/test artifacts
src/.swc
src/tsconfig.tsbuildinfo

# Environment files (inject qua CI/CD secrets, không bake vào image)
src/.env.local
src/.env*.local

# Logs
*.log
npm-debug.log*

# OS files
.DS_Store
Thumbs.db

# IDE
.vscode
.idea
*.suo
*.user

# Docker files (không cần copy vào image)
Dockerfile
docker-compose*.yml
.dockerignore

# Docs
README.md
LICENSE
acc.txt
70 changes: 70 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# ============================================================
# Stage 1: base — base image with essential libraries
# ============================================================
FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Application version (passed at build time)
ARG NEXT_PUBLIC_APP_VERSION
ENV NEXT_PUBLIC_APP_VERSION=$NEXT_PUBLIC_APP_VERSION

# ============================================================
# Stage 2: development — prep for dev environment
# ============================================================
FROM base AS development

WORKDIR /app

# Copy ALL deps (including devDeps)
COPY src/package.json src/package-lock.json ./
RUN npm ci --ignore-scripts

# Copy source code
COPY src/ .

# Disable Next.js telemetry
ENV NEXT_TELEMETRY_DISABLED=1

# ============================================================
# Stage 3: builder — compile the Next.js app
# ============================================================
FROM development AS builder

# Build-time env vars (public ones only — never leak secrets here)
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY

ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
ENV NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=$NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY

RUN npm run build

# ============================================================
# Stage 3: runner — minimal production image
# ============================================================
FROM base AS runner
Comment thread
TamCter marked this conversation as resolved.

WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs

# Copy built assets from builder
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# Use Next.js standalone server
CMD ["node", "server.js"]
56 changes: 56 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# docker-compose.yml — dùng cho LOCAL DEVELOPMENT
# Không dùng file này ở production

name: task-master-pro

services:
app:
build:
context: .
dockerfile: Dockerfile
target: development # build xong là chạy ngay (npm install), không cần build sản phẩm
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY}
NEXT_PUBLIC_APP_VERSION: ${APP_VERSION:-0.1.0}
command: npm run dev
ports:
- "3000:3000"
volumes:
- ./src:/app # hot-reload: mount source vào container
- /app/node_modules # giữ nguyên node_modules của container
- /app/.next # giữ nguyên .next cache
environment:
- NODE_ENV=development
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
- NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=${NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY}
- NEXT_PUBLIC_APP_VERSION=${APP_VERSION:-0.1.0}
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
env_file:
- src/.env.local
Comment thread
TamCter marked this conversation as resolved.
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Comment thread
TamCter marked this conversation as resolved.

# ── Production preview (dùng `docker compose --profile prod up`) ──
app-prod:
profiles: ["prod"]
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY}
NEXT_PUBLIC_APP_VERSION: ${APP_VERSION:-0.1.0}
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_APP_VERSION=${APP_VERSION:-0.1.0}
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
env_file:
- src/.env.local
restart: unless-stopped
8 changes: 2 additions & 6 deletions src/app/api/cron/notifications/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";
import { createAdminClient } from "@/utils/supabase/admin";
import { getDeadlineStatus } from "@/utils/deadline";

// Use service role key to bypass RLS and act as an admin job
const supabaseAdminUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;

const supabase = createClient(supabaseAdminUrl, supabaseServiceRoleKey);

export async function GET(request: Request) {
try {
const supabase = createAdminClient();
// Only enforce auth check when CRON_SECRET is configured
const cronSecret = process.env.CRON_SECRET;
if (cronSecret) {
Expand Down
18 changes: 18 additions & 0 deletions src/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";

/**
* GET /api/health
* Health-check endpoint dùng cho Docker healthcheck & monitoring
*/
export async function GET() {
return NextResponse.json(
{
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.NEXT_PUBLIC_APP_VERSION ?? "unknown",
environment: process.env.NODE_ENV,
},
{ status: 200 }
);
}
2 changes: 2 additions & 0 deletions src/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { NextConfig } from "next";
const ALLOWED_ORIGINS = ["https://taskmasterpro.com", "http://localhost:3000"];

const nextConfig: NextConfig = {
// Bắt buộc để Dockerfile multi-stage hoạt động (copy .next/standalone)
output: "standalone",
images: {
remotePatterns: [
{
Expand Down
10 changes: 8 additions & 2 deletions src/utils/supabase/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import { createClient } from "@supabase/supabase-js";
* ⚠️ Use ONLY in server-side code (API routes / server actions).
*/
export function createAdminClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!url || !serviceRoleKey) {
throw new Error(
"Missing Supabase admin credentials. Ensure NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set."
);
}

return createClient(url, serviceRoleKey, {
auth: {
Expand Down
Loading