feat(deploy): add Docker support with multi-stage build and compose
This commit is contained in:
parent
65b20f9ef9
commit
374bba1a93
8 changed files with 271 additions and 24 deletions
28
.playwright-mcp/page-2026-05-15T17-28-43-865Z.yml
Normal file
28
.playwright-mcp/page-2026-05-15T17-28-43-865Z.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
- generic [active] [ref=e1]:
|
||||
- main [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- generic [ref=e6]:
|
||||
- link "Strona główna" [ref=e8] [cursor=pointer]:
|
||||
- /url: /
|
||||
- generic [ref=e12]:
|
||||
- button "Light mode" [ref=e13]:
|
||||
- img
|
||||
- button "Dark mode" [ref=e14]:
|
||||
- img
|
||||
- main [ref=e15]:
|
||||
- heading "Witaj świecie!" [level=1] [ref=e16]
|
||||
- generic [ref=e17]:
|
||||
- button "Light mode" [ref=e18]:
|
||||
- img
|
||||
- button "Dark mode" [ref=e19]:
|
||||
- img
|
||||
- region "Notifications alt+T"
|
||||
- generic [ref=e21]:
|
||||
- generic [ref=e22]:
|
||||
- generic [ref=e23]: Używamy plików cookie
|
||||
- generic [ref=e24]: Używamy plików cookie, aby poprawić Twoje doświadczenie. Niezbędne pliki cookie są zawsze aktywne. Możesz zarządzać preferencjami poniżej.
|
||||
- generic [ref=e26]:
|
||||
- button "Akceptuj wszystkie" [ref=e27]
|
||||
- button "Akceptuj tylko niezbędne" [ref=e28]
|
||||
- button "Zarządzaj preferencjami" [ref=e29]
|
||||
- alert [ref=e30]
|
||||
|
|
@ -1,41 +1,49 @@
|
|||
{
|
||||
"active_plan": "/home/nxtkofi/dev/templates/convex-next-saas/.sisyphus/plans/app-shell-and-route-fallbacks.md",
|
||||
"started_at": "2026-04-21T20:19:23.457Z",
|
||||
"active_plan": "/home/nxtkofi/dev/templates/convex-next-saas/.sisyphus/plans/pwa-lite.md",
|
||||
"started_at": "2026-05-15T17:16:56.579Z",
|
||||
"session_ids": [
|
||||
"ses_24eb26a4bffeSxb1coIit8a4Dv"
|
||||
"ses_1d399daaaffebnoS7swOtGeMrQ",
|
||||
"ses_1d3539915ffep5QJEbHVd0wtbn",
|
||||
"ses_1d35391a6ffeamL03zDOdqK95a",
|
||||
"ses_1d3538b00ffeXlbkMEaiJiAGib",
|
||||
"ses_1d3538648ffejpOJeqKzTUP8fB"
|
||||
],
|
||||
"plan_name": "app-shell-and-route-fallbacks",
|
||||
"session_origins": {
|
||||
"ses_1d399daaaffebnoS7swOtGeMrQ": "direct",
|
||||
"ses_1d3539915ffep5QJEbHVd0wtbn": "appended",
|
||||
"ses_1d35391a6ffeamL03zDOdqK95a": "appended",
|
||||
"ses_1d3538b00ffeXlbkMEaiJiAGib": "appended",
|
||||
"ses_1d3538648ffejpOJeqKzTUP8fB": "appended"
|
||||
},
|
||||
"plan_name": "pwa-lite",
|
||||
"agent": "atlas",
|
||||
"task_sessions": {
|
||||
"todo:1": {
|
||||
"task_key": "todo:1",
|
||||
"task_label": "1",
|
||||
"task_title": "Establish shell and fallback translation contract",
|
||||
"session_id": "ses_24e43150cffe5Wc6MKMRzAzPi5",
|
||||
"task_title": "Add lightweight PWA manifest, icon assets, and metadata",
|
||||
"session_id": "ses_1d35a7e6effe7tixxqSuYdTznh",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "quick",
|
||||
"updated_at": "2026-04-21T20:40:03.597Z"
|
||||
"updated_at": "2026-05-15T17:20:57.345Z"
|
||||
},
|
||||
"todo:2": {
|
||||
"task_key": "todo:2",
|
||||
"task_label": "2",
|
||||
"task_title": "Build shared shell primitives and refactor ThemeChanger for header use",
|
||||
"session_id": "ses_24e395669ffe5qSSbdGO6z1tAh",
|
||||
"task_title": "Document PWA customization checklist and store caveats",
|
||||
"session_id": "ses_1d3563c89ffeh696YfGmAPPgMU",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "visual-engineering",
|
||||
"updated_at": "2026-04-21T20:41:37.491Z"
|
||||
"category": "quick",
|
||||
"updated_at": "2026-05-15T17:24:56.130Z"
|
||||
},
|
||||
"final-wave:f1": {
|
||||
"task_key": "final-wave:f1",
|
||||
"task_label": "F1",
|
||||
"task_title": "Plan Compliance Audit — oracle",
|
||||
"session_id": "ses_24e202cbbffe7XpuKb4O1csa5w",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-04-21T21:12:31.731Z"
|
||||
}
|
||||
},
|
||||
"session_origins": {
|
||||
"ses_24eb26a4bffeSxb1coIit8a4Dv": "direct"
|
||||
"session_id": "ses_1d3538648ffejpOJeqKzTUP8fB",
|
||||
"agent": "oracle",
|
||||
"category": "",
|
||||
"updated_at": "2026-05-15T17:40:02.032Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -268,3 +268,89 @@ Do not add aggressive caching for authenticated SaaS pages (dashboard, settings)
|
|||
### Store distribution
|
||||
- **Google Play**: requires a native/TWA wrapper such as Bubblewrap. The manifest alone is not enough.
|
||||
- **Apple App Store**: usually rejects simple repackaged websites without native value. A PWA wrapper alone is not sufficient.
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
The template includes a `Dockerfile` and `docker-compose.yml` for containerized deployment.
|
||||
|
||||
### Dockerfile
|
||||
|
||||
Multi-stage build optimized for production:
|
||||
|
||||
- **deps**: Installs dependencies
|
||||
- **builder**: Builds the Next.js app
|
||||
- **runner**: Minimal image with only production artifacts
|
||||
|
||||
```bash
|
||||
# Build locally
|
||||
docker build -t my-app .
|
||||
|
||||
# Run locally
|
||||
docker run -p 3000:3000 --env-file .env.local my-app
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Full-stack deployment with Convex backend:
|
||||
|
||||
```bash
|
||||
# Start everything (Next.js + Convex backend + Convex dashboard)
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Services:
|
||||
|
||||
| Service | Port | Description |
|
||||
|---------|------|-------------|
|
||||
| `app` | 3000 | Next.js application |
|
||||
| `backend` | 3210 | Convex backend API |
|
||||
| `dashboard` | 6791 | Convex admin dashboard |
|
||||
|
||||
### Server Components in Docker
|
||||
|
||||
Next.js 16 App Router with Server Components (RSC) works identically in Docker:
|
||||
|
||||
- **Build time**: `next build` generates static pages + server bundles
|
||||
- **Runtime**: `next start` serves both static and server-rendered content
|
||||
- **No special config needed** — `output: 'standalone'` in `next.config.ts` handles everything
|
||||
|
||||
The Dockerfile uses `output: 'standalone'` which creates a self-contained server bundle. This means:
|
||||
- Smaller image size (only production files)
|
||||
- Faster startup
|
||||
- No need for `node_modules` in final image
|
||||
|
||||
### Environment Variables
|
||||
|
||||
When running in Docker, pass env vars via:
|
||||
|
||||
1. `.env` file in the same directory as `docker-compose.yml`
|
||||
2. `environment` section in `docker-compose.yml`
|
||||
3. `-e` flags with `docker run`
|
||||
|
||||
Required variables:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_SITE_URL=https://your-domain.com
|
||||
NEXT_PUBLIC_CONVEX_URL=https://your-convex-backend.example.com
|
||||
NEXT_PUBLIC_CONVEX_SITE_URL=https://your-convex-site.example.com
|
||||
CONVEX_SELF_HOSTED_URL=https://your-convex-backend.example.com
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY=your-admin-key
|
||||
```
|
||||
|
||||
### Coolify Deployment
|
||||
|
||||
1. Push code to your Forgejo repo
|
||||
2. In Coolify, create a new service from your repo
|
||||
3. Select `docker-compose.yml` as the deployment method
|
||||
4. Set environment variables in Coolify UI
|
||||
5. Deploy
|
||||
|
||||
> **Note**: The `app` service depends on `backend` (Convex). Make sure your Convex backend is accessible from the Coolify server.
|
||||
|
||||
### Health Check
|
||||
|
||||
The Next.js app exposes a health endpoint at `/api/health` (add if needed) or you can check if the app is running:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000
|
||||
```
|
||||
|
|
|
|||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Dependencies
|
||||
FROM node:20-slim AS deps
|
||||
WORKDIR /app
|
||||
|
||||
# Enable pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Stage 2: Builder
|
||||
FROM node:20-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build
|
||||
|
||||
# Stage 3: Runner
|
||||
FROM node:20-slim AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
# Copy necessary files
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
|
@ -29,6 +29,7 @@ A personal, opinionated SaaS template built for speed. Next.js 16 + Convex self-
|
|||
- [x] GitHub Actions CI (lint + build)
|
||||
- [x] Project init script (`bin/init-template.mjs`)
|
||||
- [x] PWA Lite installability (manifest + icons)
|
||||
- [x] Docker deployment (Dockerfile + docker-compose)
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,15 @@ const EXCLUDE = [
|
|||
'node_modules',
|
||||
'.next',
|
||||
'.sisyphus',
|
||||
'.sisyphus',
|
||||
'.memsearch',
|
||||
'.playwright-mcp',
|
||||
'pnpm-lock.yaml',
|
||||
'bin',
|
||||
'features.md',
|
||||
'.env.local',
|
||||
'DEVELOPMENT.md',
|
||||
'tmuxi.template.yml',
|
||||
];
|
||||
|
||||
function copyRecursive(src, dest, replacements) {
|
||||
|
|
@ -23,6 +30,7 @@ function copyRecursive(src, dest, replacements) {
|
|||
copyRecursive(path.join(src, entry), path.join(dest, entry), replacements);
|
||||
}
|
||||
} else {
|
||||
if (EXCLUDE.includes(path.basename(src))) return;
|
||||
let content = fs.readFileSync(src, 'utf-8');
|
||||
for (const [key, value] of Object.entries(replacements)) {
|
||||
content = content.split(key).join(value);
|
||||
|
|
@ -35,7 +43,7 @@ function main() {
|
|||
const projectName = process.argv[2];
|
||||
|
||||
if (!projectName) {
|
||||
console.error('Usage: node bin/init-template.js <project-name>');
|
||||
console.error('Usage: node bin/init-template.mjs <project-name>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -63,15 +71,33 @@ function main() {
|
|||
'~/vaults/mentat/': '~/workspace/',
|
||||
};
|
||||
|
||||
console.log(`Creating ${projectName}...`);
|
||||
copyRecursive(templateDir, targetDir, replacements);
|
||||
// Safety: exclude target dir name to prevent recursion if run inside template
|
||||
EXCLUDE.push(projectName);
|
||||
|
||||
const srcDir = path.join(targetDir, 'src');
|
||||
const docsDir = path.join(targetDir, 'docs');
|
||||
|
||||
console.log(`Creating ${projectName}...`);
|
||||
|
||||
// Copy template files into <name>/src/
|
||||
copyRecursive(templateDir, srcDir, replacements);
|
||||
|
||||
// Move README.md to docs/readme.md
|
||||
const readmeSrc = path.join(srcDir, 'README.md');
|
||||
const readmeDest = path.join(docsDir, 'readme.md');
|
||||
if (fs.existsSync(readmeSrc)) {
|
||||
fs.mkdirSync(docsDir, { recursive: true });
|
||||
fs.renameSync(readmeSrc, readmeDest);
|
||||
}
|
||||
|
||||
console.log('Initializing git...');
|
||||
execSync('git init', { cwd: targetDir, stdio: 'inherit' });
|
||||
|
||||
console.log('Installing dependencies...');
|
||||
execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' });
|
||||
execSync('pnpm install', { cwd: srcDir, stdio: 'inherit' });
|
||||
|
||||
console.log(`\nDone! cd ${projectName} && pnpm dev --webpack`);
|
||||
console.log(`\nDone!`);
|
||||
console.log(` cd ${projectName}/src && pnpm dev --webpack`);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
services:
|
||||
# Next.js Application
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- '${PORT:-3000}:3000'
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NEXT_PUBLIC_SITE_URL: '${NEXT_PUBLIC_SITE_URL}'
|
||||
NEXT_PUBLIC_CONVEX_URL: '${NEXT_PUBLIC_CONVEX_URL}'
|
||||
NEXT_PUBLIC_CONVEX_SITE_URL: '${NEXT_PUBLIC_CONVEX_SITE_URL}'
|
||||
CONVEX_SELF_HOSTED_URL: '${CONVEX_SELF_HOSTED_URL}'
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY: '${CONVEX_SELF_HOSTED_ADMIN_KEY}'
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
# Convex Backend
|
||||
backend:
|
||||
image: 'ghcr.io/get-convex/convex-backend:latest'
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
ports:
|
||||
- '${PORT:-3210}:3210'
|
||||
- '${SITE_PROXY_PORT:-3211}:3211'
|
||||
volumes:
|
||||
- 'data:/convex/data'
|
||||
environment:
|
||||
INSTANCE_NAME: '${INSTANCE_NAME}'
|
||||
INSTANCE_SECRET: '${INSTANCE_SECRET}'
|
||||
CONVEX_CLOUD_ORIGIN: '${SERVICE_URL_BACKEND}'
|
||||
CONVEX_SITE_ORIGIN: '${SERVICE_URL_BACKEND_SITE}'
|
||||
DO_NOT_REQUIRE_SSL: '${DO_NOT_REQUIRE_SSL:-true}'
|
||||
DATABASE_URL: '${DATABASE_URL:-}'
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'curl -f http://localhost:3210/version']
|
||||
interval: 5s
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Convex Dashboard
|
||||
dashboard:
|
||||
image: 'ghcr.io/get-convex/convex-dashboard:latest'
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
ports:
|
||||
- '${DASHBOARD_PORT:-6791}:6791'
|
||||
environment:
|
||||
PORT: '6791'
|
||||
NEXT_PUBLIC_DEPLOYMENT_URL: '${SERVICE_URL_BACKEND}'
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
data: null
|
||||
|
|
@ -2,7 +2,7 @@ import type { NextConfig } from "next";
|
|||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: 'standalone',
|
||||
};
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue