ai documentation

This commit is contained in:
2026-04-01 15:16:12 +02:00
parent deaf004891
commit 724ee77710
4 changed files with 4715 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+1362
View File
File diff suppressed because it is too large Load Diff
+1563
View File
File diff suppressed because it is too large Load Diff
+703
View File
@@ -0,0 +1,703 @@
# Bamort Infrastructure Findings
> Generated: 2026-03-13
> Scope: Docker infrastructure, scripts, networking, persistence, dev/prod differences
---
## Table of Contents
1. [Docker Compose Configurations](#1-docker-compose-configurations)
2. [Dockerfiles](#2-dockerfiles)
3. [Nginx Configuration](#3-nginx-configuration)
4. [Scripts](#4-scripts)
5. [Database Initialization](#5-database-initialization)
6. [Network Architecture](#6-network-architecture)
7. [Data Persistence](#7-data-persistence)
8. [Development vs Production Differences](#8-development-vs-production-differences)
---
## 1. Docker Compose Configurations
### 1.1 `docker/docker-compose.dev.yml` — Development Stack
**Compose project name**: `bamort` (set via `COMPOSE_PROJECT_NAME` env var)
#### Service: `backend-dev`
| Property | Value |
|----------|-------|
| Container name | `bamort-backend-dev` |
| Build context | `../backend` |
| Dockerfile | `../docker/Dockerfile.backend.dev` |
| Host port → Container port | `8180 → 8180` |
| Working directory | `/app` |
| Restart policy | `unless-stopped` |
| Depends on | `mariadb-dev` (condition: `service_healthy`) |
**Environment variables:**
```
GO_ENV=development
CGO_ENABLED=1
DATABASE_TYPE=${DATABASE_TYPE:-mysql}
DATABASE_URL=${DATABASE_URL:-<user>:<pass>@tcp(mariadb-dev:3306)/<db>?charset=utf8mb4&parseTime=True&loc=Local}
API_PORT=${API_PORT:-8180}
TEMPLATES_DIR=${TEMPLATES_DIR:-./templatesx}
EXPORT_TEMP_DIR=${EXPORT_TEMP_DIR:-./export_tempx}
GIT_COMMIT=${GIT_COMMIT:-unknown}
```
> Note: `TEMPLATES_DIR` defaults to `./templatesx` (with trailing 'x') in Docker — this means the container uses the source-mounted `../backend:/app` volume and the actual templates directory at `/app/templates`.
**Volumes:**
- `../backend:/app` — full backend source mounted for live-reload via Air
- `go-mod-cache:/go/pkg/mod` — named volume to cache Go module downloads
---
#### Service: `frontend-dev`
| Property | Value |
|----------|-------|
| Container name | `bamort-frontend-dev` |
| Build context | `../frontend` |
| Dockerfile | `../docker/Dockerfile.frontend.dev` |
| Host port → Container port | `5173 → 5173` |
| Restart policy | `unless-stopped` |
| Depends on | `backend-dev` |
**Environment variables:**
```
NODE_ENV=development
VITE_API_URL=${API_URL:-http://192.168.0.1:8180}
VITE_BASE_URL=${BASE_URL:-http://bamort.trokan.de}
VITE_API_PORT=${API_PORT:-8180}
```
> The default `VITE_API_URL` points to `192.168.0.1` — a LAN IP. In practice this is overridden via `.env.dev` to `http://localhost:8180`.
**Volumes:**
- `../frontend:/app` — full frontend source mounted for Vite HMR
- `/app/node_modules` — anonymous volume to prevent host from overwriting container's installed `node_modules`
---
#### Service: `mariadb-dev`
| Property | Value |
|----------|-------|
| Container name | `bamort-mariadb-dev` |
| Image | `mariadb:11.4` |
| Host port → Container port | `3306 → 3306` |
| Restart policy | `unless-stopped` |
**Environment variables:**
```
MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD:-secure_root_password}
MARIADB_DATABASE=${MARIADB_DATABASE:-bamort}
MARIADB_USER=${MARIADB_USER:-bamort}
MARIADB_PASSWORD=${MARIADB_PASSWORD:-secure_user_password}
MARIADB_CHARSET=utf8mb4
MARIADB_COLLATION=utf8mb4_unicode_ci
```
**Volumes:**
- `./bamort-db-dev:/var/lib/mysql` — persistent database files stored in `docker/bamort-db-dev/`
- `./init-db:/docker-entrypoint-initdb.d` — SQL initialization scripts (run only on first container creation)
**Health check:**
```
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 10s
timeout: 5s
retries: 3
```
---
#### Service: `phpmyadmin-dev`
| Property | Value |
|----------|-------|
| Container name | `bamort-phpmyadmin-dev` |
| Image | `phpmyadmin/phpmyadmin:5.2` |
| Host port → Container port | `8081 → 80` |
| Restart policy | `unless-stopped` |
| Depends on | `mariadb-dev` (condition: `service_healthy`) |
**Environment variables:**
```
PMA_HOST=mariadb-dev
PMA_PORT=3306
PMA_USER=root
PMA_PASSWORD=${MARIADB_ROOT_PASSWORD:-secure_root_password}
MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD:-secure_root_password}
PMA_ARBITRARY=1
```
**Named volumes declared:**
```yaml
volumes:
go-mod-cache:
```
---
### 1.2 `docker/docker-compose.yml` — Production Stack
#### Service: `backend`
| Property | Value |
|----------|-------|
| Container name | `bamort-backend` |
| Build context | `../backend` |
| Dockerfile | `../docker/Dockerfile.backend` |
| Host port → Container port | `8182 → 8180` |
| Working directory | `/app` |
| Restart policy | `unless-stopped` |
| Depends on | `mariadb` (condition: `service_healthy`) |
**Environment variables:**
```
GO_ENV=production
CGO_ENABLED=1
DATABASE_TYPE=${DATABASE_TYPE:-mysql}
DATABASE_URL=<user>:<pass>@tcp(mariadb:3306)/<db>?charset=utf8mb4&parseTime=True&loc=Local
BASE_URL=${BASE_URL:-https://bamort.trokan.de}
API_PORT=${API_PORT:-8180}
```
> Note: `DATABASE_URL` in production is NOT wrapped in `${}` with a fallback — it's constructed inline, which means any unset env vars will result in empty credentials.
**Volumes:**
- `./templates:/app/templates` — mounts host templates directory (no source code mounted)
---
#### Service: `frontend`
| Property | Value |
|----------|-------|
| Container name | `bamort-frontend` |
| Build context | `../frontend` |
| Dockerfile | `../docker/Dockerfile.frontend` |
| Host port → Container port | `8181 → 80` |
| Restart policy | `unless-stopped` |
| Depends on | `backend` |
**Build args (baked into the image at build time):**
```
VITE_API_URL: ${API_URL:-https://bamort-api.trokan.de}
VITE_BASE_URL: ${BASE_URL:-https://bamort.trokan.de}
VITE_API_PORT: ${API_PORT:-443}
```
> These are compile-time args embedded into the Vite bundle — runtime `environment:` vars have no effect on the built static bundle.
**Environment variables (runtime — informational only):**
```
NODE_ENV=production
VITE_API_URL=${API_URL:-https://bamort.trokan.de:8180}
VITE_BASE_URL=${BASE_URL:-https://bamort.trokan.de}
VITE_API_PORT=${API_PORT:-8180}
```
---
#### Service: `mariadb`
| Property | Value |
|----------|-------|
| Container name | `bamort-mariadb` |
| Image | `mariadb:11.4` |
| Host port | **NOT EXPOSED** (ports section commented out) |
| Restart policy | `unless-stopped` |
**Environment variables:** Same as dev
**Volumes:**
- `./bamort-db:/var/lib/mysql` — persistent data in `docker/bamort-db/`
- `./init-db:/docker-entrypoint-initdb.d`
**Health check:** Same, but with longer timeouts (`start_period: 20s`, `timeout: 10s`, `retries: 3`)
---
#### Service: `phpmyadmin` (COMMENTED OUT in production)
The service definition exists in the file but is fully commented out for security. Can be re-enabled via:
```bash
sed -i 's/^ # phpmyadmin:/ phpmyadmin:/' docker-compose.yml
docker-compose up -d phpmyadmin
```
If re-enabled: port `8081 → 80`
---
## 2. Dockerfiles
### 2.1 `docker/Dockerfile.backend` — Production Backend (Multi-stage)
**Stage 1: `builder`** — `golang:1.25-alpine`
```dockerfile
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache gcc musl-dev sqlite-dev
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -v -o server cmd/main.go
```
- Installs GCC (required for CGO), musl-dev, and sqlite-dev (CGO is enabled for SQLite support)
- Builds a static binary named `server` from `cmd/main.go`
**Stage 2: Runtime** — `alpine:3.23`
```dockerfile
FROM alpine:3.23
RUN apk add --no-cache \
chromium chromium-chromedriver \
nss freetype harfbuzz ca-certificates ttf-freefont
ENV CHROME_BIN=/usr/bin/chromium-browser \
CHROME_PATH=/usr/bin/chromium-browser
WORKDIR /app
COPY --from=builder /app/server /app
COPY --from=builder /app/templates /app/default_templates
EXPOSE 8180
CMD ["./server"]
```
- Installs Chromium for PDF rendering via chromedp
- Copies compiled binary and templates from builder stage
- Templates copied to `default_templates/` (runtime templates come from volume mount `./templates:/app/templates`)
- Result: minimal Alpine image with only Chromium and the binary
---
### 2.2 `docker/Dockerfile.backend.dev` — Development Backend (Single-stage)
**Base**: `golang:1.25-alpine` (single stage — no build separation)
```dockerfile
FROM golang:1.25-alpine
RUN apk add --no-cache gcc musl-dev sqlite-dev
RUN apk add --no-cache chromium chromium-chromedriver nss freetype harfbuzz ca-certificates ttf-freefont
ENV CHROME_BIN=/usr/bin/chromium-browser \
CHROME_PATH=/usr/bin/chromium-browser
RUN go install github.com/air-verse/air@latest
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
EXPOSE 8180
CMD ["air", "-c", ".air.toml"]
```
**Key differences from production:**
- Includes the full Go toolchain (for recompilation on file changes)
- Installs `air` (live-reload tool from `github.com/air-verse/air@latest`)
- Full source code is mounted at runtime via volume, not copied into image
- Starts with `air -c .air.toml` instead of the compiled binary
**Air configuration** (`.air.toml`):
```toml
root = "."
tmp_dir = "tmp"
[build]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/main.go"
delay = 1000 # 1 second delay before rebuild
exclude_dir = ["assets", "tmp", "vendor", "testdata", "uploads", "transfer/xporttemp", "export_temp"]
exclude_regex = ["_test.go"] # Test files excluded from watch
include_ext = ["go", "tpl", "tmpl", "html"]
log = "build-errors.log"
```
Air watches `.go`, `.tpl`, `.tmpl`, `.html` files, excludes test files, and rebuilds into `./tmp/main` on any change.
---
### 2.3 `docker/Dockerfile.frontend` — Production Frontend (Multi-stage)
**Stage 1: `build`** — `node:22-alpine`
```dockerfile
FROM node:22-alpine AS build
ARG VITE_API_URL
ARG VITE_BASE_URL
ARG VITE_API_PORT
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_BASE_URL=$VITE_BASE_URL
ENV VITE_API_PORT=$VITE_API_PORT
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
```
- Accepts `VITE_*` values as build ARGs → embedded into the static bundle by Vite
- Runs `npm run build` which produces `dist/`
**Stage 2: `serve`** — `nginx:alpine`
```dockerfile
FROM nginx:alpine
COPY --from=build /usr/src/app/dist /usr/share/nginx/html
COPY --from=build /usr/src/app/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
- Serves the static Vue bundle via Nginx
- Uses `frontend/nginx.conf` for SPA routing (see section 3)
---
### 2.4 `docker/Dockerfile.frontend.dev` — Development Frontend (Single-stage)
**Base**: `node:22-alpine`
```dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
```
- No build step — Vite dev server handles hot module replacement (HMR)
- `--host 0.0.0.0` binds Vite to all interfaces (required for Docker port exposure)
- Full source mounted at `/app` via volume at runtime
- `node_modules` protected inside container via anonymous volume in compose
---
## 3. Nginx Configuration
**Note:** Both `docker/nginx.conf` and `frontend/nginx.conf` are **identical** in content. The `frontend/nginx.conf` is the one copied into the production Docker image.
### Configuration (both files are the same):
```nginx
server {
listen 80;
listen [::]:80;
server_name localhost;
# SPA routing: try files, fallback to index.html
location / {
try_files $uri $uri/ /index.html;
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
```
**Key behavior:**
- **SPA routing**: `try_files $uri $uri/ /index.html` — any URL that doesn't match a static file falls through to `index.html`, enabling Vue Router to handle client-side navigation
- **No backend proxy**: Nginx does NOT proxy `/api` requests to the backend. The frontend communicates with the backend API directly via the `VITE_API_URL` environment variable baked into the bundle (e.g., `https://bamort-api.trokan.de`)
- **Static file serving**: All files served from `/usr/share/nginx/html`
- **No HTTPS**: The Nginx container handles only HTTP on port 80; TLS termination is expected to happen at an external reverse proxy (e.g., Traefik, HAProxy, or a hosting provider)
- **No API proxy block**: Unlike many SPA setups, there is no `location /api` proxy directive — the API is on a separate domain/port
---
## 4. Scripts
### 4.1 `docker/start-dev.sh`
```bash
#!/bin/bash
# Verifies Docker is running
# Changes to docker/ directory
# Exports GIT_COMMIT=$(git rev-parse --short HEAD)
# Runs: docker-compose -f docker-compose.dev.yml down
# Runs: docker-compose -f docker-compose.dev.yml up --build -d
```
**What it does:**
1. Checks `docker info` — exits with error if Docker daemon is not running
2. `cd`s to the script's own directory (so paths in compose file work)
3. Exports current short git commit hash as `GIT_COMMIT`
4. Stops any existing dev containers first (`down`)
5. Rebuilds images and starts all dev services in detached mode (`up --build -d`)
---
### 4.2 `docker/stop-dev.sh`
```bash
#!/bin/bash
# Changes to docker/ directory
# Runs: docker-compose -f docker-compose.dev.yml down
```
Stops and removes all dev containers (but preserves volumes and images).
---
### 4.3 `docker/start-prd.sh`
```bash
#!/bin/bash
# Verifies Docker is running
# Changes to docker/ directory
# Runs: docker-compose -f docker-compose.yml build (pre-build while old containers run)
# Runs: docker-compose -f docker-compose.yml down (stop old containers)
# Runs: docker-compose -f docker-compose.yml up --build -d
```
**Key difference from dev:** Runs `build` before `down` to minimize downtime. Old containers continue running while new images are being built, then a quick switch happens.
---
### 4.4 `docker/stop-prd.sh`
```bash
#!/bin/bash
# Changes to docker/ directory
# Runs: docker-compose -f docker-compose.yml down
```
Stops and removes production containers. Note: the script message says "Stopping Development Environment" — this is a copy-paste bug in the echo message.
---
### 4.5 `backend/startserver.sh`
This file is **not a standard shell script** — it is actually a `.env`-style configuration file that also contains the final command to start the server locally (outside Docker):
```bash
# Environment variables for BaMoRT development environment
ENVIRONMENT=development
DATABASE_TYPE=mysql
DATABASE_URL="bamort:bG4)efozrc@tcp(192.168.0.36:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local"
MARIADB_ROOT_PASSWORD=root_password_dev
MARIADB_PASSWORD="bG4)efozrc"
MARIADB_DATABASE=bamort
MARIADB_USER=bamort
API_URL="http://localhost:8180"
VITE_API_URL="http://localhost:8180"
API_PORT=8180
BASE_URL="http://localhost:5173"
TEMPLATES_DIR=./templates
EXPORT_TEMP_DIR=./export_temp
GIT_COMMIT=d0c177b
LOG_LEVEL=debug
CHROME_BIN="/usr/bin/chromium"
echo $DATABASE_URL
/home/de31a2/.local/bin/go/bin/go run ./cmd/main.go
```
Used as a local dev helper to set env vars and launch the Go backend directly (no Docker). Points to a MariaDB at `192.168.0.36:3306` (a LAN server).
> **Security note**: Contains hardcoded credentials (`bG4)efozrc` for MariaDB user) — this is development-only but should be in `.gitignore`.
---
### 4.6 `backend/transfer_sqlite_to_mariadb.sh`
```bash
BACKEND_URL="http://localhost:8180"
ENDPOINT="/api/maintenance/transfer-sqlite-to-mariadb"
```
**What it does:**
1. Optionally accepts argument `clear` to wipe MariaDB data before transfer (prompts for confirmation)
2. Calls `POST http://localhost:8180/api/maintenance/transfer-sqlite-to-mariadb[?clear=true]` via `curl`
3. Displays JSON response (formatted via `jq` if available) or raw output
**Purpose:** One-time data migration from the SQLite test database (`testdata/prepared_test_data.db`) to the MariaDB development database. The actual transfer logic is implemented in the backend's `/maintenance` module.
---
### 4.7 `scripts/export-databases.sh`
**What it does:**
1. Loads environment variables from `./docker/.env.dev`
2. **SQLite export:**
- Dumps `backend/testdata/prepared_test_data.db` to `backend/testdata/exports/sqlite_dump.sql`
- Exports each table as a CSV file: `backend/testdata/exports/sqlite_<table>.csv`
3. **MariaDB export** (requires `bamort-mariadb-dev` container running):
- Runs `mariadb-dump` inside the container → `backend/testdata/exports/mysql_dump.sql`
- Exports each table as CSV: `backend/testdata/exports/mysql_<table>.csv`
4. Lists exported files
**Purpose:** Side-by-side export of SQLite (test data) and MariaDB (dev data) for comparison/debugging during data migration. The `TIMESTAMP` variable is intentionally empty (left blank in the script), so files are overwritten on each run.
---
### 4.8 `scripts/update-version.sh`
**Files updated by this script:**
| File | What changes |
|------|--------------|
| `backend/appsystem/version.go` | `const Version = "X.Y.Z"` |
| `backend/VERSION.md` | `## Current Version: X.Y.Z` |
| `frontend/src/version.js` | `export const VERSION = 'X.Y.Z'` |
| `frontend/package.json` | `"version": "X.Y.Z"` |
| `frontend/VERSION.md` | `## Current Version: X.Y.Z` |
**CLI flags:**
```
-b <version> Set backend version explicitly
-f <version> Set frontend version explicitly
-n Auto-bump patch version on both (reads current, increments Z in X.Y.Z)
-c Create a git commit after version update
-t Create a git tag after commit
```
**Tag strategy:**
- If both versions are equal: creates single tag `vX.Y.Z`
- If they differ: creates `backend-vX.Y.Z` and `frontend-vX.Y.Z` separately
**Example workflows:**
```bash
./scripts/update-version.sh -b 0.1.31 -f 0.2.0 # Set explicit versions
./scripts/update-version.sh -b 0.1.31 -c -t # Set + commit + tag
./scripts/update-version.sh -n -c # Auto-bump patch + commit
./scripts/update-version.sh -c -t # Commit + tag from current files
```
---
## 5. Database Initialization
### Init directory: `docker/init-db/`
| File | Status | Description |
|------|--------|-------------|
| `00-init.sql` | **Active** | Full schema + seed data (~3,872 lines) |
| `01-init.sql.bak` | Backup | Earlier version (user data focus) |
| `02-init.sql.bak` | Backup | Skill tables variant |
| `09-init.sql.bak` | Backup | Indexes and auto-increments |
### `00-init.sql` — Active Initialization Script
Generated by phpMyAdmin from `mariadb-dev:3306` on 2025-12-30.
Contains **78 CREATE TABLE + INSERT INTO** statements covering the full database schema.
**Tables created (sampled):**
- `audit_log_entries` — character field change history
- `char_bennies` — character blessings/bennies
- `char_characteristics` — physical appearance (eye color, hair, size, etc.)
- Full game character schema (all character sub-tables)
**Settings applied on init:**
```sql
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
SET NAMES utf8mb4;
```
**Character set:** `utf8mb4` with `utf8mb4_unicode_ci` collation throughout.
**Execution timing:** Scripts in `init-db/` run only once — when the MariaDB container is created for the first time. If `./bamort-db-dev/` (dev) or `./bamort-db/` (prod) already contain data, init scripts are skipped.
---
## 6. Network Architecture
### Development Network Topology
```
Host Machine
├── :5173 ──→ bamort-frontend-dev (Vite HMR, Vue SPA)
├── :8180 ──→ bamort-backend-dev (Go/Gin REST API, Air live-reload)
├── :3306 ──→ bamort-mariadb-dev (MariaDB 11.4)
└── :8081 ──→ bamort-phpmyadmin-dev (phpMyAdmin 5.2)
Docker Internal Network (docker-compose default bridge)
├── bamort-frontend-dev → bamort-backend-dev:8180 (via VITE_API_URL on host)
├── bamort-backend-dev → mariadb-dev:3306 (internal DNS resolution)
└── bamort-phpmyadmin-dev → mariadb-dev:3306 (internal DNS resolution)
```
**Inter-container communication:**
- Backend connects to MariaDB using the Docker service name `mariadb-dev` as hostname (resolved via Docker internal DNS)
- Frontend in dev calls the backend via `VITE_API_URL` which is set to the **host's** IP (e.g., `http://localhost:8180`), not an internal Docker name — meaning the browser makes direct calls to the exposed host port, not through Docker networking
- phpMyAdmin connects to `mariadb-dev:3306` internally
### Production Network Topology
```
Host Machine
├── :8181 ──→ bamort-frontend (Nginx serving built Vue SPA)
├── :8182 ──→ bamort-backend (Go/Gin REST API, compiled binary)
│ bamort-mariadb (NOT exposed — internal only)
│ [phpmyadmin] (disabled by default)
Docker Internal Network
├── bamort-frontend → bamort-backend [via external VITE_API_URL baked into bundle]
├── bamort-backend → mariadb:3306 (internal DNS)
└── bamort-mariadb (no host port binding — not accessible from outside)
```
**External reverse proxy (implicit):** The production setup is designed to sit behind an external reverse proxy (e.g., Nginx, Traefik, or HAProxy) that terminates TLS and routes:
- `bamort.trokan.de` → container port `8181` (frontend)
- `bamort-api.trokan.de` → container port `8182` (backend API)
### CORS Configuration
The backend reads `FRONTEND_URL` from config (defaults to `http://localhost:5173`) and uses it for CORS origin allow-listing. Production backend receives `BASE_URL=https://bamort.trokan.de` for this purpose.
---
## 7. Data Persistence
### Volume Mounts
| Environment | Service | Host path | Container path | Purpose |
|-------------|---------|-----------|----------------|---------|
| Dev | MariaDB | `docker/bamort-db-dev/` | `/var/lib/mysql` | Database files |
| Dev | Backend | `../backend/` | `/app` | Source code (live-reload) |
| Dev | Frontend | `../frontend/` | `/app` | Source code (HMR) |
| Dev | Backend | `go-mod-cache` (named) | `/go/pkg/mod` | Go module cache |
| Prod | MariaDB | `docker/bamort-db/` | `/var/lib/mysql` | Database files |
| Prod | Backend | `docker/templates/` | `/app/templates` | PDF templates |
### Database Persistence Strategy
**Dev:** `docker/bamort-db-dev/` — a host directory mount. Survives container restarts and recreations. Wiped only if you remove the directory manually or run `docker-compose down -v` (though `-v` only removes named volumes, not bind mounts).
**Prod:** `docker/bamort-db/` — same strategy. MariaDB port is NOT exposed to the host, so direct external access requires `docker exec`.
### SQLite (Test Data)
The backend also has a SQLite database at `backend/testdata/prepared_test_data.db` used for automated tests. This is **not** a Docker volume — it's part of the source tree.
### Backup Strategy
**`scripts/export-databases.sh`:** Manual backup script that:
1. Dumps SQLite test DB to SQL + per-table CSV files
2. Dumps MariaDB dev DB to SQL + per-table CSV files
3. Output to `backend/testdata/exports/`
**No automated backup:** There is no cron job or automated backup mechanism in the Docker setup. Backups are manual only. The `docker/bamort_20260123_v0.1.37.sql` and `docker/bamort.sql_backup` files are manual one-off snapshots.
---
## 8. Development vs Production Differences
| Aspect | Development | Production |
|--------|-------------|------------|
| **Backend image** | `Dockerfile.backend.dev` — full Go toolchain + Air | `Dockerfile.backend` — multi-stage, Alpine runtime only |
| **Backend start** | `air -c .air.toml` (live reloads on `.go` file changes) | `./server` (pre-compiled binary) |
| **Backend source** | Volume-mounted from host (`../backend:/app`) | Compiled into image at build time |
| **Frontend image** | `Dockerfile.frontend.dev` — Node + Vite dev server | `Dockerfile.frontend` — multi-stage, Nginx serving `dist/` |
| **Frontend start** | `npm run dev -- --host 0.0.0.0` (Vite HMR) | `nginx -g 'daemon off;'` |
| **Frontend port** | `5173` | `8181` (maps to container `80`) |
| **Backend port** | `8180` | `8182` (maps to container `8180`) |
| **MariaDB port** | `3306` (exposed to host) | NOT exposed externally |
| **phpMyAdmin** | Enabled on port `8081` | Disabled (commented out) |
| **VITE_API_URL** | Runtime env var (set in compose) | Build-time ARG (baked into bundle) |
| **Template source** | Mounted from `../backend` volume (part of source) | Mounted from `docker/templates/` |
| **DB data dir** | `docker/bamort-db-dev/` | `docker/bamort-db/` |
| **Go env** | `GO_ENV=development` | `GO_ENV=production` |
| **MariaDB hostname** | `mariadb-dev` | `mariadb` |
| **Health check timing** | `start_period: 10s` | `start_period: 20s` |
| **Image size** | Large (Go toolchain + Chromium) | Smaller (Alpine + Chromium only) |
| **Hot reload** | Yes (Air for Go, Vite HMR for Vue) | No |
| **Startup script** | `start-dev.sh` (includes git commit export) | `start-prd.sh` (pre-builds before stopping) |
### Notable Configuration Quirks
1. **`TEMPLATES_DIR=./templatesx`** in dev compose — has a trailing `x` suffix, which means Air/server falls back to default `./templates`. The actual templates come from the bind-mounted source directory.
2. **Production DATABASE_URL** is constructed inline without `${}` fallback syntax, meaning if `MARIADB_USER` or `MARIADB_PASSWORD` are unset, the URL will contain empty strings rather than defaults.
3. **Frontend VITE_API_URL defaults** differ between dev (`192.168.0.1:8180`) and prod (`bamort-api.trokan.de`). The dev default is a LAN IP that only works in the author's local network — other developers must set their own `.env.dev`.
4. **No Docker network** is explicitly defined — both compose files rely on Docker Compose's default bridge network where all services in the same file can reach each other by service name.
5. **`startserver.sh`** is misleadingly named — it is actually a combined `.env` file + startup command for running outside Docker, not a proper shell script header (no `#!/bin/bash`).