Initial commit: ArchiTools modular dashboard platform

Complete Next.js 16 application with 13 fully implemented modules:
Email Signature, Word XML Generator, Registratura, Dashboard,
Tag Manager, IT Inventory, Address Book, Password Vault,
Mini Utilities, Prompt Generator, Digital Signatures,
Word Templates, and AI Chat.

Includes core platform systems (module registry, feature flags,
storage abstraction, i18n, theming, auth stub, tagging),
16 technical documentation files, Docker deployment config,
and legacy HTML tool reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marius Tarau
2026-02-17 12:50:25 +02:00
commit 4c46e8bcdd
189 changed files with 33780 additions and 0 deletions

View File

@@ -0,0 +1,717 @@
# 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 (future: Authentik SSO)
# ──────────────────────────────────────────
# AUTHENTIK_ISSUER=https://auth.internal
# AUTHENTIK_CLIENT_ID=architools
# AUTHENTIK_CLIENT_SECRET=<secret>
```
### 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.