# 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:-:@tcp(mariadb-dev:3306)/?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=:@tcp(mariadb:3306)/?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_.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_
.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 Set backend version explicitly -f 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`).