Files
bamort/infra_findings.md
T
2026-04-01 15:16:12 +02:00

26 KiB

Bamort Infrastructure Findings

Generated: 2026-03-13
Scope: Docker infrastructure, scripts, networking, persistence, dev/prod differences


Table of Contents

  1. Docker Compose Configurations
  2. Dockerfiles
  3. Nginx Configuration
  4. Scripts
  5. Database Initialization
  6. Network Architecture
  7. Data Persistence
  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:

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:

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: buildergolang:1.25-alpine

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: Runtimealpine:3.23

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)

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):

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: buildnode:22-alpine

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: servenginx:alpine

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

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):

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

#!/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. cds 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

#!/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

#!/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

#!/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):

# 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

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:

./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:

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).