974d06fff8
- Add core notification service: types, email-service (nodemailer/Brevo SMTP), notification-service (digest builder, preference CRUD, HTML renderer) - Add API routes: POST /api/notifications/digest (N8N cron, Bearer auth), GET/PUT /api/notifications/preferences (session auth) - Add NotificationPreferences UI component (Bell button + dialog with per-type toggles) in Registratura toolbar - Add 7 Brevo SMTP env vars to docker-compose.yml - Update CLAUDE.md, ROADMAP.md, DATA-MODEL.md, SYSTEM-ARCHITECTURE.md, CONFIGURATION.md, DOCKER-DEPLOYMENT.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
731 lines
26 KiB
Markdown
731 lines
26 KiB
Markdown
# Docker Deployment Guide
|
|
|
|
> ArchiTools internal reference -- containerized deployment on the on-premise Ubuntu server.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
ArchiTools runs as a single Docker container behind Nginx Proxy Manager on the internal network. The deployment pipeline is:
|
|
|
|
```
|
|
Developer pushes to Gitea
|
|
--> Portainer webhook triggers stack redeploy (or Watchtower detects image change)
|
|
--> Docker builds multi-stage image
|
|
--> Container starts on port 3000
|
|
--> Nginx Proxy Manager routes tools.internal --> localhost:3000
|
|
--> Users access via browser
|
|
```
|
|
|
|
The container runs a standalone Next.js production server. No Node.js process manager (PM2, forever) is needed -- the container runtime handles restarts via `restart: unless-stopped`.
|
|
|
|
---
|
|
|
|
## Dockerfile
|
|
|
|
Multi-stage build that produces a minimal production image.
|
|
|
|
```dockerfile
|
|
# Stage 1: Dependencies
|
|
FROM node:20-alpine AS deps
|
|
WORKDIR /app
|
|
COPY package.json package-lock.json ./
|
|
RUN npm ci --only=production
|
|
|
|
# Stage 2: Build
|
|
FROM node:20-alpine AS builder
|
|
WORKDIR /app
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
# Stage 3: Runner
|
|
FROM node:20-alpine AS runner
|
|
WORKDIR /app
|
|
ENV NODE_ENV=production
|
|
RUN addgroup --system --gid 1001 nodejs
|
|
RUN adduser --system --uid 1001 nextjs
|
|
COPY --from=builder /app/public ./public
|
|
COPY --from=builder /app/.next/standalone ./
|
|
COPY --from=builder /app/.next/static ./.next/static
|
|
USER nextjs
|
|
EXPOSE 3000
|
|
ENV PORT=3000
|
|
CMD ["node", "server.js"]
|
|
```
|
|
|
|
### Stage Breakdown
|
|
|
|
| Stage | Base | Purpose | Output |
|
|
|---|---|---|---|
|
|
| `deps` | `node:20-alpine` | Install production dependencies only | `node_modules/` |
|
|
| `builder` | `node:20-alpine` | Compile TypeScript, build Next.js bundle | `.next/standalone/`, `.next/static/`, `public/` |
|
|
| `runner` | `node:20-alpine` | Minimal runtime image with non-root user | Final image (~120 MB) |
|
|
|
|
### Why Multi-Stage
|
|
|
|
- The `deps` stage caches `node_modules` independently of source code changes. If only application code changes, Docker reuses the cached dependency layer.
|
|
- The `builder` stage contains all dev dependencies and source files but is discarded after the build.
|
|
- The `runner` stage contains only the standalone server output, static assets, and public files. No `node_modules` directory, no source code, no dev tooling.
|
|
|
|
### Security Notes
|
|
|
|
- The `nextjs` user (UID 1001) is a non-root system user. The container never runs as root.
|
|
- Alpine Linux has a minimal attack surface. No shell utilities beyond BusyBox basics.
|
|
- The `NODE_ENV=production` flag disables React development warnings, enables Next.js production optimizations, and prevents accidental dev-mode behavior.
|
|
|
|
---
|
|
|
|
## next.config.ts Requirements
|
|
|
|
The standalone output mode is mandatory for the Docker deployment. Without it, Next.js expects the full `node_modules` directory at runtime.
|
|
|
|
```typescript
|
|
// next.config.ts
|
|
|
|
import type { NextConfig } from 'next';
|
|
|
|
const nextConfig: NextConfig = {
|
|
output: 'standalone',
|
|
|
|
// Required for Docker: trust the reverse proxy headers
|
|
// so that Next.js resolves the correct protocol and host
|
|
experimental: {
|
|
// If needed in future Next.js versions
|
|
},
|
|
};
|
|
|
|
export default nextConfig;
|
|
```
|
|
|
|
### What `output: 'standalone'` Does
|
|
|
|
1. Traces all required Node.js dependencies at build time.
|
|
2. Copies only the needed files into `.next/standalone/`.
|
|
3. Generates a self-contained `server.js` that starts a production HTTP server.
|
|
4. Eliminates the need for `node_modules` in the runtime image.
|
|
|
|
The standalone output does **not** include the `public/` or `.next/static/` directories. These must be copied explicitly in the Dockerfile (which the Dockerfile above does).
|
|
|
|
---
|
|
|
|
## docker-compose.yml
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
services:
|
|
architools:
|
|
build: .
|
|
container_name: architools
|
|
restart: unless-stopped
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
- NODE_ENV=production
|
|
- NEXT_PUBLIC_APP_URL=${APP_URL:-http://localhost:3000}
|
|
env_file:
|
|
- .env
|
|
volumes:
|
|
- architools-data:/app/data
|
|
networks:
|
|
- proxy-network
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
|
|
volumes:
|
|
architools-data:
|
|
|
|
networks:
|
|
proxy-network:
|
|
external: true
|
|
```
|
|
|
|
### Field Reference
|
|
|
|
| Field | Purpose |
|
|
|---|---|
|
|
| `build: .` | Build from the Dockerfile in the repository root. |
|
|
| `container_name: architools` | Fixed name for predictable Portainer/Dozzle references. |
|
|
| `restart: unless-stopped` | Auto-restart on crash or server reboot. Only stops if explicitly stopped. |
|
|
| `ports: "3000:3000"` | Map container port 3000 to host port 3000. Nginx Proxy Manager connects here. |
|
|
| `env_file: .env` | Load environment variables from `.env`. Never committed to Gitea. |
|
|
| `volumes: architools-data:/app/data` | Persistent volume for future server-side data. Not used in localStorage phase. |
|
|
| `networks: proxy-network` | Shared Docker network with Nginx Proxy Manager and other services. |
|
|
| `labels: watchtower.enable=true` | Opt in to Watchtower automatic image updates. |
|
|
|
|
### The `proxy-network` Network
|
|
|
|
All services that Nginx Proxy Manager routes to must be on the same Docker network. This network is created once and shared across all stacks:
|
|
|
|
```bash
|
|
docker network create proxy-network
|
|
```
|
|
|
|
If the network already exists (it should -- other services like Authentik, MinIO, N8N use it), the `external: true` declaration tells Docker Compose not to create it.
|
|
|
|
---
|
|
|
|
## Environment Configuration
|
|
|
|
### `.env` File
|
|
|
|
```bash
|
|
# ──────────────────────────────────────────
|
|
# Application
|
|
# ──────────────────────────────────────────
|
|
NODE_ENV=production
|
|
NEXT_PUBLIC_APP_URL=https://tools.internal
|
|
NEXT_PUBLIC_APP_ENV=production
|
|
|
|
# ──────────────────────────────────────────
|
|
# Feature Flags (override defaults from src/config/flags.ts)
|
|
# ──────────────────────────────────────────
|
|
NEXT_PUBLIC_FLAG_MODULE_REGISTRATURA=true
|
|
NEXT_PUBLIC_FLAG_MODULE_PROMPT_GENERATOR=true
|
|
NEXT_PUBLIC_FLAG_MODULE_EMAIL_SIGNATURE=true
|
|
NEXT_PUBLIC_FLAG_MODULE_AI_CHAT=false
|
|
|
|
# ──────────────────────────────────────────
|
|
# Storage
|
|
# ──────────────────────────────────────────
|
|
NEXT_PUBLIC_STORAGE_ADAPTER=localStorage
|
|
|
|
# Future: API backend
|
|
# STORAGE_API_URL=http://localhost:4000/api/storage
|
|
|
|
# Future: MinIO
|
|
# MINIO_ENDPOINT=minio.internal
|
|
# MINIO_ACCESS_KEY=architools
|
|
# MINIO_SECRET_KEY=<secret>
|
|
# MINIO_BUCKET=architools
|
|
|
|
# ──────────────────────────────────────────
|
|
# Authentication (Authentik SSO)
|
|
# ──────────────────────────────────────────
|
|
# AUTHENTIK_ISSUER=https://auth.internal
|
|
# AUTHENTIK_CLIENT_ID=architools
|
|
# AUTHENTIK_CLIENT_SECRET=<secret>
|
|
|
|
# ──────────────────────────────────────────
|
|
# Email Notifications (Brevo SMTP)
|
|
# ──────────────────────────────────────────
|
|
BREVO_SMTP_HOST=smtp-relay.brevo.com
|
|
BREVO_SMTP_PORT=587
|
|
BREVO_SMTP_USER=<brevo-login>
|
|
BREVO_SMTP_PASS=<brevo-smtp-key>
|
|
NOTIFICATION_FROM_EMAIL=noreply@beletage.ro
|
|
NOTIFICATION_FROM_NAME=ArchiTools
|
|
NOTIFICATION_CRON_SECRET=<random-bearer-token>
|
|
```
|
|
|
|
> **N8N cron setup:** Create a workflow with Cron node (`0 8 * * 1-5`), HTTP Request node (POST `https://tools.beletage.ro/api/notifications/digest`, header `Authorization: Bearer <NOTIFICATION_CRON_SECRET>`). The endpoint returns `{ success, totalEmails, errors, companySummary }`.
|
|
|
|
### Variable Scoping Rules
|
|
|
|
| Prefix | Available In | Notes |
|
|
|---|---|---|
|
|
| `NEXT_PUBLIC_*` | Client + server | Inlined into the JavaScript bundle at build time. Visible to users in browser DevTools. Never put secrets here. |
|
|
| No prefix | Server only | Available in API routes, middleware, server components. Used for secrets, credentials, internal URLs. |
|
|
|
|
### Build-Time vs. Runtime
|
|
|
|
`NEXT_PUBLIC_*` variables are baked into the bundle during `npm run build`. Changing them requires a rebuild. Non-prefixed variables are read at runtime and can be changed by restarting the container.
|
|
|
|
For Docker, this means:
|
|
- `NEXT_PUBLIC_*` changes require rebuilding the image.
|
|
- Server-only variables can be changed via Portainer environment editor and restarting the container.
|
|
|
|
---
|
|
|
|
## Nginx Proxy Manager Setup
|
|
|
|
### Proxy Host Configuration
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| **Domain Names** | `tools.internal` (or `tools.beletage.internal`, etc.) |
|
|
| **Scheme** | `http` |
|
|
| **Forward Hostname / IP** | `architools` (Docker container name, resolved via `proxy-network`) |
|
|
| **Forward Port** | `3000` |
|
|
| **Block Common Exploits** | Enabled |
|
|
| **Websockets Support** | Enabled (for HMR in dev; harmless in production) |
|
|
|
|
### SSL Configuration
|
|
|
|
**Internal access (self-signed or internal CA):**
|
|
|
|
1. In Nginx Proxy Manager, go to SSL Certificates > Add SSL Certificate > Custom.
|
|
2. Upload the internal CA certificate and key.
|
|
3. Assign to the `tools.internal` proxy host.
|
|
4. Browsers on internal machines must trust the internal CA (deployed via group policy or manual install).
|
|
|
|
**External access (Let's Encrypt):**
|
|
|
|
1. When the domain becomes publicly resolvable (e.g., `tools.beletage.ro`), switch to Let's Encrypt.
|
|
2. In Nginx Proxy Manager, go to SSL Certificates > Add SSL Certificate > Let's Encrypt.
|
|
3. Enter the domain, email, and agree to ToS.
|
|
4. Nginx Proxy Manager handles renewal automatically.
|
|
|
|
### Security Headers
|
|
|
|
Add the following in the proxy host's Advanced tab (Custom Nginx Configuration):
|
|
|
|
```nginx
|
|
# Security headers
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
|
|
# Content Security Policy
|
|
add_header Content-Security-Policy "
|
|
default-src 'self';
|
|
script-src 'self' 'unsafe-inline' 'unsafe-eval';
|
|
style-src 'self' 'unsafe-inline';
|
|
img-src 'self' data: blob:;
|
|
font-src 'self' data:;
|
|
connect-src 'self' https://api.openai.com https://api.anthropic.com;
|
|
frame-ancestors 'self';
|
|
" always;
|
|
```
|
|
|
|
**Notes on CSP:**
|
|
- `'unsafe-inline'` and `'unsafe-eval'` are required by Next.js in production. Tighten with nonces if migrating to a stricter CSP in the future.
|
|
- `connect-src` includes AI provider API domains for the AI Chat and Prompt Generator modules. Adjust as providers are added or removed.
|
|
- `frame-ancestors 'self'` prevents clickjacking (equivalent to `X-Frame-Options: SAMEORIGIN`).
|
|
|
|
---
|
|
|
|
## Portainer Deployment
|
|
|
|
### Stack Deployment from Gitea
|
|
|
|
1. In Portainer, go to Stacks > Add Stack.
|
|
2. Select **Repository** as the build method.
|
|
3. Configure:
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| **Name** | `architools` |
|
|
| **Repository URL** | `https://gitea.internal/beletage/architools.git` |
|
|
| **Repository reference** | `refs/heads/main` |
|
|
| **Compose path** | `docker-compose.yml` |
|
|
| **Authentication** | Gitea access token or SSH key |
|
|
|
|
4. Under **Environment variables**, add all variables from the `.env` file. Portainer stores these securely and injects them at deploy time.
|
|
5. Enable **Auto update** with a webhook if desired.
|
|
|
|
### Environment Variable Management
|
|
|
|
Portainer provides a UI for managing environment variables per stack. Use this for:
|
|
|
|
- Toggling feature flags without touching the repository.
|
|
- Updating server-side secrets (MinIO keys, Authentik credentials) without rebuilding.
|
|
- Switching `NEXT_PUBLIC_*` values (requires stack redeploy to rebuild the image).
|
|
|
|
**Important:** `NEXT_PUBLIC_*` variables are build-time constants. Changing them in Portainer requires redeploying the stack (which triggers a rebuild), not just restarting the container.
|
|
|
|
### Container Monitoring
|
|
|
|
Portainer provides:
|
|
- **Container status:** running, stopped, restarting.
|
|
- **Resource usage:** CPU, memory, network I/O.
|
|
- **Logs:** stdout/stderr output (same as Dozzle, but accessible from the Portainer UI).
|
|
- **Console:** exec into the container for debugging (use sparingly; the container has minimal tooling).
|
|
- **Restart/Stop/Remove:** Manual container lifecycle controls.
|
|
|
|
---
|
|
|
|
## Watchtower Integration
|
|
|
|
Watchtower monitors Docker containers and automatically updates them when a new image is available.
|
|
|
|
### How It Works with ArchiTools
|
|
|
|
1. The `docker-compose.yml` includes the label `com.centurylinklabs.watchtower.enable=true`.
|
|
2. Watchtower periodically checks (default: every 24 hours, configurable) if the image has changed.
|
|
3. If a new image is detected, Watchtower:
|
|
- Pulls the new image.
|
|
- Stops the running container.
|
|
- Creates a new container with the same configuration.
|
|
- Starts the new container.
|
|
- Removes the old image (if configured).
|
|
|
|
### Triggering Updates
|
|
|
|
**Automatic (Watchtower polling):** Watchtower polls at a configured interval. Suitable for non-urgent updates.
|
|
|
|
**Manual (Portainer):** Redeploy the stack from Portainer. This pulls the latest code from Gitea, rebuilds the image, and restarts the container.
|
|
|
|
**Webhook (Portainer):** Configure a Portainer webhook URL. Add it as a webhook in Gitea (triggered on push to `main`). Gitea pushes, Portainer receives the webhook, and redeploys.
|
|
|
|
### Recommended Flow
|
|
|
|
For ArchiTools, the primary deployment trigger is the **Portainer webhook from Gitea**:
|
|
|
|
```
|
|
git push origin main
|
|
--> Gitea fires webhook to Portainer
|
|
--> Portainer redeploys the architools stack
|
|
--> Docker rebuilds the image (multi-stage build)
|
|
--> New container starts
|
|
--> Old container removed
|
|
```
|
|
|
|
Watchtower serves as a safety net for cases where the webhook fails or for updating the base `node:20-alpine` image.
|
|
|
|
---
|
|
|
|
## Health Check Endpoint
|
|
|
|
The application exposes a health check endpoint at `/api/health`.
|
|
|
|
```typescript
|
|
// src/app/api/health/route.ts
|
|
|
|
import { NextResponse } from 'next/server';
|
|
|
|
export async function GET() {
|
|
return NextResponse.json(
|
|
{
|
|
status: 'healthy',
|
|
timestamp: new Date().toISOString(),
|
|
version: process.env.npm_package_version ?? 'unknown',
|
|
environment: process.env.NODE_ENV ?? 'unknown',
|
|
},
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
```
|
|
|
|
### Usage
|
|
|
|
- **Uptime Kuma:** Add a monitor with type HTTP(s), URL `http://architools:3000/api/health`, expected status code `200`. Monitor interval: 60 seconds.
|
|
- **Docker health check (optional):** Add to `docker-compose.yml`:
|
|
|
|
```yaml
|
|
services:
|
|
architools:
|
|
# ... existing config ...
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 40s
|
|
```
|
|
|
|
The `start_period` gives Next.js time to start before Docker begins health checking.
|
|
|
|
---
|
|
|
|
## Logging
|
|
|
|
### Strategy
|
|
|
|
ArchiTools logs to stdout and stderr. No file-based logging, no log rotation configuration inside the container. Docker captures all stdout/stderr output and makes it available via:
|
|
|
|
- **Dozzle:** Real-time log viewer. Access at `dozzle.internal`. Filter by container name `architools`.
|
|
- **Portainer:** Logs tab on the container detail page.
|
|
- **CLI:** `docker logs architools` or `docker logs -f architools` for live tail.
|
|
|
|
### Log Levels
|
|
|
|
| Source | Output | Captured By |
|
|
|---|---|---|
|
|
| Next.js server | Request logs, compilation warnings | stdout |
|
|
| Application `console.log` | Debug information, state changes | stdout |
|
|
| Application `console.error` | Errors, stack traces | stderr |
|
|
| Unhandled exceptions | Crash traces | stderr |
|
|
|
|
### Structured Logging (Future)
|
|
|
|
When the application grows beyond simple console output, adopt a structured JSON logger (e.g., `pino`). This enables Dozzle or a future log aggregator to parse, filter, and search log entries by level, module, and context.
|
|
|
|
---
|
|
|
|
## Data Persistence Strategy
|
|
|
|
### Current Phase: localStorage
|
|
|
|
In the current phase, all module data lives in the browser's `localStorage`. The Docker container is stateless -- no server-side data storage. This means:
|
|
|
|
- **No data loss on container restart.** Data is in the browser, not the container.
|
|
- **No backup needed for the container.** The volume mount (`architools-data:/app/data`) is provisioned but empty.
|
|
- **No multi-user data sharing.** Each browser has its own isolated data set.
|
|
- **Export/import is the backup mechanism.** Modules provide export buttons that download JSON files.
|
|
|
|
### Future Phase: Server-Side Storage
|
|
|
|
When the storage adapter switches to `api` or a database backend:
|
|
|
|
| Concern | Implementation |
|
|
|---|---|
|
|
| **Database** | PostgreSQL container on the same Docker network. Volume-mounted for persistence. |
|
|
| **File storage** | MinIO (already running). ArchiTools stores file references in the database, binary objects in MinIO buckets. |
|
|
| **Backup** | Database dumps + MinIO bucket sync. Scheduled via N8N or cron. |
|
|
| **Volume mount** | `architools-data:/app/data` used for SQLite (if chosen as interim DB) or temp files. |
|
|
|
|
### Volume Mount
|
|
|
|
The `architools-data` volume is defined in `docker-compose.yml` and mounted at `/app/data`. It persists across container restarts and image rebuilds. Currently unused but ready for:
|
|
|
|
- SQLite database file (interim before PostgreSQL).
|
|
- Temporary file processing (document generation, PDF manipulation).
|
|
- Cache files if needed.
|
|
|
|
---
|
|
|
|
## Build and Deploy Workflow
|
|
|
|
### Full Lifecycle
|
|
|
|
```
|
|
1. Developer pushes to Gitea (main branch)
|
|
|
|
|
2. Gitea fires webhook to Portainer
|
|
|
|
|
3. Portainer pulls latest code from Gitea repository
|
|
|
|
|
4. Docker builds multi-stage image:
|
|
a. Stage 1 (deps): npm ci --only=production
|
|
b. Stage 2 (builder): npm run build (Next.js standalone)
|
|
c. Stage 3 (runner): minimal image with server.js
|
|
|
|
|
5. Portainer stops the running container
|
|
|
|
|
6. Portainer starts a new container from the fresh image
|
|
|
|
|
7. Health check passes (GET /api/health returns 200)
|
|
|
|
|
8. Nginx Proxy Manager routes traffic to the new container
|
|
|
|
|
9. Uptime Kuma confirms service is up
|
|
|
|
|
10. Old image is cleaned up (Watchtower or manual docker image prune)
|
|
```
|
|
|
|
### Build Time Expectations
|
|
|
|
| Stage | Typical Duration | Notes |
|
|
|---|---|---|
|
|
| `deps` (cached) | <5 seconds | Only re-runs if `package.json` or `package-lock.json` changes. |
|
|
| `deps` (fresh) | 30--60 seconds | Full `npm ci` with all dependencies. |
|
|
| `builder` | 30--90 seconds | Next.js build. Depends on module count and TypeScript compilation. |
|
|
| `runner` | <5 seconds | Just file copies. |
|
|
| **Total (cached deps)** | ~1--2 minutes | Typical deployment time. |
|
|
| **Total (fresh)** | ~2--3 minutes | After dependency changes. |
|
|
|
|
### Rollback
|
|
|
|
If a deployment introduces a bug:
|
|
|
|
1. In Portainer, stop the current container.
|
|
2. Redeploy the stack pointing to the previous Gitea commit (change the repository reference to a specific commit SHA or tag).
|
|
3. Alternatively, if the previous Docker image is still cached locally, restart the container from that image.
|
|
|
|
Tagging releases in Gitea (`v1.0.0`, `v1.1.0`) makes rollback straightforward.
|
|
|
|
---
|
|
|
|
## Development vs. Production Configuration
|
|
|
|
### Comparison
|
|
|
|
| Aspect | Development | Production |
|
|
|---|---|---|
|
|
| **Command** | `npm run dev` | `node server.js` (standalone) |
|
|
| **Hot reload** | Yes (Fast Refresh) | No |
|
|
| **Source maps** | Full | Minimal (production build) |
|
|
| **NODE_ENV** | `development` | `production` |
|
|
| **Storage adapter** | `localStorage` | `localStorage` (current), `api` (future) |
|
|
| **Feature flags** | All enabled for testing | Selective per `.env` |
|
|
| **Error display** | Full stack traces in browser | Generic error page |
|
|
| **CSP headers** | None (permissive) | Strict (via Nginx Proxy Manager) |
|
|
| **SSL** | None (`http://localhost:3000`) | Terminated at Nginx Proxy Manager |
|
|
| **Docker** | Not used (direct `npm run dev`) | Multi-stage build, containerized |
|
|
| **Port** | 3000 (direct) | 3000 (container) --> 443 (Nginx) |
|
|
|
|
### Running Development Locally
|
|
|
|
```bash
|
|
# Install dependencies
|
|
npm install
|
|
|
|
# Start dev server
|
|
npm run dev
|
|
|
|
# Access at http://localhost:3000
|
|
```
|
|
|
|
No Docker, no Nginx, no SSL. Just the Next.js dev server.
|
|
|
|
### Testing Production Build Locally
|
|
|
|
```bash
|
|
# Build the production bundle
|
|
npm run build
|
|
|
|
# Start the production server
|
|
npm start
|
|
|
|
# Or test the Docker build
|
|
docker build -t architools:local .
|
|
docker run -p 3000:3000 --env-file .env architools:local
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Container Fails to Start
|
|
|
|
**Symptom:** Container status shows `Restarting` in Portainer, or `docker ps` shows restart loop.
|
|
|
|
**Diagnosis:**
|
|
```bash
|
|
docker logs architools
|
|
```
|
|
|
|
**Common causes:**
|
|
|
|
| Error | Cause | Fix |
|
|
|---|---|---|
|
|
| `Error: Cannot find module './server.js'` | `output: 'standalone'` missing from `next.config.ts` | Add `output: 'standalone'` and rebuild. |
|
|
| `EACCES: permission denied` | File ownership mismatch | Verify the Dockerfile copies files before switching to `USER nextjs`. |
|
|
| `EADDRINUSE: port 3000` | Another container using port 3000 | Change the host port mapping in `docker-compose.yml` (e.g., `"3001:3000"`). |
|
|
| `MODULE_NOT_FOUND` | Dependency not in production deps | Move the dependency from `devDependencies` to `dependencies` in `package.json`. |
|
|
|
|
### Build Fails at `npm run build`
|
|
|
|
**Symptom:** Docker build exits at the `builder` stage.
|
|
|
|
**Common causes:**
|
|
|
|
| Error | Cause | Fix |
|
|
|---|---|---|
|
|
| TypeScript errors | Type mismatches in code | Fix TypeScript errors locally before pushing. |
|
|
| `ENOMEM` | Not enough memory for build | Increase Docker memory limit (Next.js build can use 1--2 GB). |
|
|
| Missing environment variables | `NEXT_PUBLIC_*` required at build time | Pass build args or set defaults in `next.config.ts`. |
|
|
|
|
### Application Returns 502 via Nginx
|
|
|
|
**Symptom:** Browser shows `502 Bad Gateway`.
|
|
|
|
**Checklist:**
|
|
1. Is the container running? `docker ps | grep architools`
|
|
2. Is the container healthy? `docker inspect architools | grep Health`
|
|
3. Can Nginx reach the container? Both must be on `proxy-network`.
|
|
4. Is the forward port correct (3000)?
|
|
5. Is the scheme `http` (not `https` -- SSL terminates at Nginx)?
|
|
|
|
### Static Assets Not Loading (CSS, JS, Images)
|
|
|
|
**Symptom:** Page loads but unstyled, or browser console shows 404 for `/_next/static/*`.
|
|
|
|
**Cause:** Missing `COPY --from=builder /app/.next/static ./.next/static` in the Dockerfile.
|
|
|
|
**Fix:** Verify both `public/` and `.next/static/` are copied in the runner stage.
|
|
|
|
### Environment Variables Not Taking Effect
|
|
|
|
**Symptom:** Feature flag change in Portainer does not change behavior.
|
|
|
|
**Diagnosis:**
|
|
- If the variable starts with `NEXT_PUBLIC_*`: it is baked in at build time. You must redeploy (rebuild the image), not just restart.
|
|
- If the variable has no prefix: restart the container. The value is read at runtime.
|
|
|
|
### High Memory Usage
|
|
|
|
**Symptom:** Container uses more than expected memory (check Portainer or Netdata).
|
|
|
|
**Typical usage:** 100--200 MB for a standalone Next.js server with moderate traffic.
|
|
|
|
**If higher:**
|
|
- Check for memory leaks in server-side code (API routes, middleware).
|
|
- Set a memory limit in `docker-compose.yml`:
|
|
|
|
```yaml
|
|
services:
|
|
architools:
|
|
# ... existing config ...
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 512M
|
|
```
|
|
|
|
### Logs Not Appearing in Dozzle
|
|
|
|
**Symptom:** Dozzle shows the container but no log output.
|
|
|
|
**Checklist:**
|
|
1. Is the container actually running (not in a restart loop)?
|
|
2. Is the application writing to stdout/stderr (not to a file)?
|
|
3. Is Dozzle configured to monitor all containers on the Docker socket?
|
|
|
|
### Container Networking Issues
|
|
|
|
**Symptom:** Container cannot reach other services (MinIO, Authentik, N8N).
|
|
|
|
**Checklist:**
|
|
1. All services must be on the same Docker network (`proxy-network`).
|
|
2. Use container names as hostnames (e.g., `http://minio:9000`), not `localhost`.
|
|
3. Verify DNS resolution: `docker exec architools wget -q -O- http://minio:9000/minio/health/live`
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
### Commands
|
|
|
|
```bash
|
|
# Build image
|
|
docker build -t architools .
|
|
|
|
# Run container
|
|
docker run -d --name architools -p 3000:3000 --env-file .env architools
|
|
|
|
# View logs
|
|
docker logs -f architools
|
|
|
|
# Exec into container
|
|
docker exec -it architools sh
|
|
|
|
# Rebuild and restart (compose)
|
|
docker compose down && docker compose up -d --build
|
|
|
|
# Check health
|
|
curl http://localhost:3000/api/health
|
|
|
|
# Prune old images
|
|
docker image prune -f
|
|
```
|
|
|
|
### File Checklist
|
|
|
|
| File | Required | Purpose |
|
|
|---|---|---|
|
|
| `Dockerfile` | Yes | Multi-stage build definition. |
|
|
| `docker-compose.yml` | Yes | Service orchestration, networking, volumes. |
|
|
| `.env` | Yes (not committed) | Environment variables. |
|
|
| `.dockerignore` | Recommended | Exclude `node_modules`, `.git`, `.next` from build context. |
|
|
| `next.config.ts` | Yes | Must include `output: 'standalone'`. |
|
|
| `src/app/api/health/route.ts` | Yes | Health check endpoint. |
|
|
|
|
### `.dockerignore`
|
|
|
|
```
|
|
node_modules
|
|
.next
|
|
.git
|
|
.gitignore
|
|
*.md
|
|
docs/
|
|
.env
|
|
.env.*
|
|
```
|
|
|
|
This reduces the Docker build context size and prevents leaking sensitive files into the image.
|