feat(deploy): add Docker support with multi-stage build and compose

This commit is contained in:
nxtkofi 2026-05-17 18:48:34 +02:00
parent 65b20f9ef9
commit 374bba1a93
8 changed files with 271 additions and 24 deletions

View 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]

View file

@ -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"
}
}
}

View file

@ -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
View 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"]

View file

@ -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

View file

@ -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
View 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

View file

@ -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();