Files
ArchiTools/docs/guides/DOCKER-DEPLOYMENT.md
T
AI Assistant 0958238b25 Update docs: compact numbers, Alerte Termene sender, notifications in repo structure
- CLAUDE.md: add compact registry numbers feature, sender name, test mode
- ROADMAP.md: expand 8.03 with compact numbers, icon-only toolbar, test mode
- REPO-STRUCTURE.md: add src/core/notifications/ directory + description
- SYSTEM-ARCHITECTURE.md: add sender name, test mode, group company behavior
- CONFIGURATION.md + DOCKER-DEPLOYMENT.md: NOTIFICATION_FROM_NAME=Alerte Termene

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:14:39 +02:00

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=Alerte Termene
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 }`. Add `?test=true` query param to send a test digest with sample data.
### 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.