From bda41e413f5a368ce848509f4e9eeb388cb14648 Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:13:09 +0100 Subject: [PATCH 1/6] feat: docker compose support + env var config overrides (#19) docker compose up gets the API running against MariaDB 11 on node:22-trixie-slim. generate-env.sh creates .env with random passwords, same convention as NicTool 2's generate-env.sh. config.js now checks env vars (DB_HOST, DB_PASSWORD, HTTP_HOST, etc.) after parsing TOML -- fully backwards compatible, the TOML values are just defaults now. config tests save/clear/restore the env vars so they should pass in both CI and docker. init-mysql.sh picks up MYSQL_ROOT_PASSWORD when running inside the mariadb container, prefers the mariadb client over mysql, and supports DB_USER/DB_NAME/SQL_DIR env vars. quick start: ./generate-env.sh && docker compose up --build Co-Authored-By: Claude Opus 4.6 (1M context) --- .dockerignore | 9 +++++++ .env.example | 21 +++++++++++++++ Dockerfile | 7 +++++ docker-compose.yml | 45 ++++++++++++++++++++++++++++++++ generate-env.sh | 38 +++++++++++++++++++++++++++ lib/config.js | 16 ++++++++++++ lib/config.test.js | 22 +++++++++++++++- sql/init-mysql.sh | 64 +++++++++++++++++++++++++++++----------------- 8 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100755 generate-env.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cf55e70 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules/ +.git/ +.github/ +dist/ +*.md +.env +.env.example +.eslintrc* +eslint.config* diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7663081 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# NicTool API environment configuration +# Copy to .env, or run: ./dist/setup/generate-env.sh + +# --- Database (MariaDB/MySQL) --- +DB_ROOT_PASSWORD=changeme +NICTOOL_DB_NAME=nictool +NICTOOL_DB_USER=nictool +NICTOOL_DB_USER_PASSWORD=changeme + +# --- API config overrides (optional, override conf.d/*.toml defaults) --- +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_USER=nictool +# DB_PASSWORD= +# DB_NAME=nictool +# HTTP_HOST=localhost +# HTTP_PORT=3000 + +# --- Docker Compose port mapping --- +# DB_PORT=3307 +# API_PORT=3000 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8307fee --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-trixie-slim +WORKDIR /app +COPY package*.json . +RUN npm install --omit=dev +COPY . . +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6025330 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +services: + db: + image: mariadb:11 + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MYSQL_ROOT_HOST: '%' + MYSQL_DATABASE: ${NICTOOL_DB_NAME:-nictool} + MYSQL_USER: ${NICTOOL_DB_USER:-nictool} + MYSQL_PASSWORD: ${NICTOOL_DB_USER_PASSWORD} + SQL_DIR: /sql + volumes: + - db-data:/var/lib/mysql + - ./sql:/sql:ro + - ./sql/init-mysql.sh:/docker-entrypoint-initdb.d/init-mysql.sh:ro + ports: + - "${DB_PORT:-3307}:3306" + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: . + ports: + - "${API_PORT:-3000}:3000" + depends_on: + db: + condition: service_healthy + environment: + NODE_ENV: development + DB_HOST: db + DB_USER: ${NICTOOL_DB_USER:-nictool} + DB_PASSWORD: ${NICTOOL_DB_USER_PASSWORD} + DB_NAME: ${NICTOOL_DB_NAME:-nictool} + HTTP_HOST: "0.0.0.0" + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3000/documentation').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 15s + +volumes: + db-data: diff --git a/generate-env.sh b/generate-env.sh new file mode 100755 index 0000000..7bd53f6 --- /dev/null +++ b/generate-env.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +ENV_FILE="$(cd "$(dirname "$0")/../.." && pwd)/.env" + +if [ -f "$ENV_FILE" ]; then + echo ".env already exists, not overwriting." >&2 + exit 0 +fi + +DB_ROOT_PW=$(openssl rand -base64 24) +NT_DB_PW=$(openssl rand -base64 24) + +cat > "$ENV_FILE" < { + const savedEnv = {} + + before(() => { + for (const key of envOverrideKeys) { + savedEnv[key] = process.env[key] + delete process.env[key] + } + Config.cfg = {} + }) + + after(() => { + for (const key of envOverrideKeys) { + if (savedEnv[key] !== undefined) process.env[key] = savedEnv[key] + else delete process.env[key] + } + Config.cfg = {} + }) + describe('get', () => { it(`loads mysql config`, async () => { const cfg = await Config.get('mysql') diff --git a/sql/init-mysql.sh b/sql/init-mysql.sh index f3638f1..876a7a9 100755 --- a/sql/init-mysql.sh +++ b/sql/init-mysql.sh @@ -1,37 +1,55 @@ #!/bin/sh +DB_USER="${DB_USER:-root}" +DB_NAME="${DB_NAME:-nictool}" MYSQL_BIN="" -if [ "$MYSQL_PWD" = "" ]; -then - export MYSQL_PWD=root - - # configure MySQL in the GitHub workflow runners - case "$(uname -s)" in - Linux*) - ;; - Darwin*) - MYSQL_BIN=/opt/homebrew/opt/mysql@8.4/bin/ - ${MYSQL_BIN}mysqladmin --user=root --password='' --protocol=tcp password 'root' - ;; - CYGWIN*|MINGW*|MINGW32*|MSYS*) - mysqladmin --user=root --password='' --protocol=tcp password 'root' - # export MYSQL_PWD="" - ;; - esac +if [ "$MYSQL_PWD" = "" ]; then + if [ -n "$MYSQL_ROOT_PASSWORD" ]; then + # Docker: MYSQL_ROOT_PASSWORD is set by the MariaDB container + export MYSQL_PWD="$MYSQL_ROOT_PASSWORD" + else + export MYSQL_PWD=root + + # configure MySQL in the GitHub workflow runners + case "$(uname -s)" in + Linux*) + ;; + Darwin*) + MYSQL_BIN=/opt/homebrew/opt/mysql@8.4/bin/ + ${MYSQL_BIN}mysqladmin --user=root --password='' --protocol=tcp password 'root' + ;; + CYGWIN*|MINGW*|MINGW32*|MSYS*) + mysqladmin --user=root --password='' --protocol=tcp password 'root' + # export MYSQL_PWD="" + ;; + esac + fi fi -# AUTH="--defaults-extra-file=./sql/my-gha.cnf" +if [ -z "$MYSQL_CMD" ]; then + # prefer mariadb client if available (MariaDB 11+ dropped the mysql symlink) + if [ -z "$MYSQL_BIN" ] && command -v mariadb >/dev/null 2>&1; then + MYSQL_CMD="mariadb --user=$DB_USER" + elif [ -n "$MYSQL_BIN" ]; then + MYSQL_CMD="${MYSQL_BIN}mysql --user=$DB_USER" + else + MYSQL_CMD="mysql --user=$DB_USER" + fi +fi if [ "$1" = "drop" ]; then - ${MYSQL_BIN}mysql --user=root -e 'DROP DATABASE IF EXISTS nictool;' || exit 1 + $MYSQL_CMD -e "DROP DATABASE IF EXISTS $DB_NAME;" || exit 1 fi -${MYSQL_BIN}mysql --user=root -e 'CREATE DATABASE nictool;' || exit 1 -for f in ./sql/*.sql; +$MYSQL_CMD -e "CREATE DATABASE IF NOT EXISTS $DB_NAME;" || exit 1 + +SQL_DIR="${SQL_DIR:-./sql}" + +for f in "$SQL_DIR"/*.sql; do - echo "cat $f | ${MYSQL_BIN}mysql nictool" - cat $f | ${MYSQL_BIN}mysql --user=root nictool || exit 1 + echo "$f" + $MYSQL_CMD "$DB_NAME" < "$f" || exit 1 done exit 0 \ No newline at end of file From 1bcf896114b5b0a585496df63bf3dc3fdaafff45 Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:27:50 +0100 Subject: [PATCH 2/6] ci: add Docker Compose test job Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9054b6f..1ce3cdb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,23 @@ jobs: - run: npm install - run: npm test + test-docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Generate .env + run: ./generate-env.sh + - name: Build and start services + run: docker compose up --build -d --wait + - name: Run tests + run: docker compose exec api npm test + - name: Logs on failure + if: failure() + run: docker compose logs + - name: Tear down + if: always() + run: docker compose down -v + test-win: needs: [ get-lts ] runs-on: windows-latest From 3a2292f629571188ef969424f6a2b5efa85fc1c1 Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:52:32 +0100 Subject: [PATCH 3/6] address review: move docker files to docker/, namespace env vars Move Dockerfile, docker-compose.yml, generate-env.sh, .env.example into docker/ to keep the repo root clean. Namespace all config env var overrides with NICTOOL_ prefix to match NicTool 2 conventions (NICTOOL_DB_HOST, NICTOOL_DB_USER_PASSWORD, NICTOOL_HTTP_HOST, etc). DB_ROOT_PASSWORD stays unprefixed as it's a MariaDB convention shared with NicTool 2. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 10 +++++----- .env.example => docker/.env.example | 13 +++++-------- Dockerfile => docker/Dockerfile | 0 .../docker-compose.yml | 18 ++++++++++-------- generate-env.sh => docker/generate-env.sh | 15 ++++++--------- lib/config.js | 14 +++++++------- lib/config.test.js | 2 +- 7 files changed, 34 insertions(+), 38 deletions(-) rename .env.example => docker/.env.example (64%) rename Dockerfile => docker/Dockerfile (100%) rename docker-compose.yml => docker/docker-compose.yml (71%) rename generate-env.sh => docker/generate-env.sh (73%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ce3cdb..3acf056 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,17 +92,17 @@ jobs: steps: - uses: actions/checkout@v6 - name: Generate .env - run: ./generate-env.sh + run: ./docker/generate-env.sh - name: Build and start services - run: docker compose up --build -d --wait + run: docker compose -f docker/docker-compose.yml up --build -d --wait - name: Run tests - run: docker compose exec api npm test + run: docker compose -f docker/docker-compose.yml exec api npm test - name: Logs on failure if: failure() - run: docker compose logs + run: docker compose -f docker/docker-compose.yml logs - name: Tear down if: always() - run: docker compose down -v + run: docker compose -f docker/docker-compose.yml down -v test-win: needs: [ get-lts ] diff --git a/.env.example b/docker/.env.example similarity index 64% rename from .env.example rename to docker/.env.example index 7663081..e52f64e 100644 --- a/.env.example +++ b/docker/.env.example @@ -1,5 +1,5 @@ # NicTool API environment configuration -# Copy to .env, or run: ./dist/setup/generate-env.sh +# Copy to .env, or run: ./docker/generate-env.sh # --- Database (MariaDB/MySQL) --- DB_ROOT_PASSWORD=changeme @@ -8,13 +8,10 @@ NICTOOL_DB_USER=nictool NICTOOL_DB_USER_PASSWORD=changeme # --- API config overrides (optional, override conf.d/*.toml defaults) --- -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_USER=nictool -# DB_PASSWORD= -# DB_NAME=nictool -# HTTP_HOST=localhost -# HTTP_PORT=3000 +# NICTOOL_DB_HOST=127.0.0.1 +# NICTOOL_DB_PORT=3306 +# NICTOOL_HTTP_HOST=localhost +# NICTOOL_HTTP_PORT=3000 # --- Docker Compose port mapping --- # DB_PORT=3307 diff --git a/Dockerfile b/docker/Dockerfile similarity index 100% rename from Dockerfile rename to docker/Dockerfile diff --git a/docker-compose.yml b/docker/docker-compose.yml similarity index 71% rename from docker-compose.yml rename to docker/docker-compose.yml index 6025330..52b3893 100644 --- a/docker-compose.yml +++ b/docker/docker-compose.yml @@ -10,8 +10,8 @@ services: SQL_DIR: /sql volumes: - db-data:/var/lib/mysql - - ./sql:/sql:ro - - ./sql/init-mysql.sh:/docker-entrypoint-initdb.d/init-mysql.sh:ro + - ../sql:/sql:ro + - ../sql/init-mysql.sh:/docker-entrypoint-initdb.d/init-mysql.sh:ro ports: - "${DB_PORT:-3307}:3306" healthcheck: @@ -21,7 +21,9 @@ services: retries: 10 api: - build: . + build: + context: .. + dockerfile: docker/Dockerfile ports: - "${API_PORT:-3000}:3000" depends_on: @@ -29,11 +31,11 @@ services: condition: service_healthy environment: NODE_ENV: development - DB_HOST: db - DB_USER: ${NICTOOL_DB_USER:-nictool} - DB_PASSWORD: ${NICTOOL_DB_USER_PASSWORD} - DB_NAME: ${NICTOOL_DB_NAME:-nictool} - HTTP_HOST: "0.0.0.0" + NICTOOL_DB_HOST: db + NICTOOL_DB_USER: ${NICTOOL_DB_USER:-nictool} + NICTOOL_DB_USER_PASSWORD: ${NICTOOL_DB_USER_PASSWORD} + NICTOOL_DB_NAME: ${NICTOOL_DB_NAME:-nictool} + NICTOOL_HTTP_HOST: "0.0.0.0" healthcheck: test: ["CMD", "node", "-e", "fetch('http://localhost:3000/documentation').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] interval: 10s diff --git a/generate-env.sh b/docker/generate-env.sh similarity index 73% rename from generate-env.sh rename to docker/generate-env.sh index 7bd53f6..bee94f1 100755 --- a/generate-env.sh +++ b/docker/generate-env.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -ENV_FILE="$(cd "$(dirname "$0")/../.." && pwd)/.env" +ENV_FILE="$(cd "$(dirname "$0")" && pwd)/.env" if [ -f "$ENV_FILE" ]; then echo ".env already exists, not overwriting." >&2 @@ -13,7 +13,7 @@ NT_DB_PW=$(openssl rand -base64 24) cat > "$ENV_FILE" < { const savedEnv = {} From e2a881ab093ce48a38c6b32201e021964c63e825 Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:27:21 +0100 Subject: [PATCH 4/6] wip: add server (web UI) service to docker compose Adds an optional 'server' service behind the 'ui' and 'e2e' profiles. Builds from the sibling NicTool/server repo, configurable via NICTOOL_SERVER_PATH for CI checkouts. docker compose --profile ui up --build # full stack docker compose up --build # db + api only Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/docker-compose.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 52b3893..e5c78c9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -43,5 +43,25 @@ services: retries: 10 start_period: 15s + server: + profiles: [ui, e2e] + build: + context: ${NICTOOL_SERVER_PATH:-../../server} + dockerfile: docker/Dockerfile + hostname: localhost + ports: + - "${SERVER_PORT:-8443}:443" + depends_on: + api: + condition: service_healthy + environment: + NICTOOL_DB_HOST: db + NICTOOL_DB_USER: ${NICTOOL_DB_USER:-nictool} + NICTOOL_DB_USER_PASSWORD: ${NICTOOL_DB_USER_PASSWORD} + NICTOOL_DB_NAME: ${NICTOOL_DB_NAME:-nictool} + NICTOOL_API_HOST: api + NICTOOL_API_PORT: 3000 + NICTOOL_BIND_HOST: "0.0.0.0" + volumes: db-data: From dba982620c18b03492757a18a7db6c76a984223f Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:41:47 +0100 Subject: [PATCH 5/6] feat: server service gets HTTP mode, dev volume mounts - NICTOOL_TLS env var controls TLS (defaults to auto, which preserves existing cert discovery behavior). Set to false in local .env for plain HTTP dev. - Bind-mount server source (index.js, bin/, html/) so edits are live without rebuilding the image - Port mapping uses SERVER_PORT and SERVER_CONTAINER_PORT env vars for flexibility Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/docker-compose.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e5c78c9..2ab35d1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -50,11 +50,16 @@ services: dockerfile: docker/Dockerfile hostname: localhost ports: - - "${SERVER_PORT:-8443}:443" + - "${SERVER_PORT:-8080}:${SERVER_CONTAINER_PORT:-8080}" depends_on: api: condition: service_healthy + volumes: + - ${NICTOOL_SERVER_PATH:-../../server}/index.js:/app/index.js:ro + - ${NICTOOL_SERVER_PATH:-../../server}/bin:/app/bin:ro + - ${NICTOOL_SERVER_PATH:-../../server}/html:/app/html:ro environment: + NICTOOL_TLS: ${NICTOOL_TLS:-auto} NICTOOL_DB_HOST: db NICTOOL_DB_USER: ${NICTOOL_DB_USER:-nictool} NICTOOL_DB_USER_PASSWORD: ${NICTOOL_DB_USER_PASSWORD} From f3db7ce571c9eba4396368d641612796ee80265e Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:48:42 +0100 Subject: [PATCH 6/6] fix: address review feedback on docker/config changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate port env vars (NICTOOL_DB_PORT, NICTOOL_HTTP_PORT) via parsePort() — throws on non-numeric or out-of-range values instead of silently passing NaN to the connection layer. - Add comment to Dockerfile explaining why --omit=dev is safe for running tests (node:test is stdlib, devDeps are only eslint/prettier). - Add comment to init-mysql.sh explaining SQL_DIR default behavior in Docker vs. bare-metal contexts. - Add trailing newline to init-mysql.sh. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/Dockerfile | 1 + lib/config.js | 14 ++++++++++++-- sql/init-mysql.sh | 4 +++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8307fee..0fea311 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,7 @@ FROM node:22-trixie-slim WORKDIR /app COPY package*.json . +# --omit=dev is safe: tests use node:test (stdlib), devDeps are only eslint/prettier RUN npm install --omit=dev COPY . . EXPOSE 3000 diff --git a/lib/config.js b/lib/config.js index f4d122b..f517ea4 100644 --- a/lib/config.js +++ b/lib/config.js @@ -49,17 +49,27 @@ class Config { } } +function parsePort(envVar) { + const raw = process.env[envVar] + if (!raw) return undefined + const port = parseInt(raw, 10) + if (Number.isNaN(port) || port < 1 || port > 65535) { + throw new Error(`${envVar}="${raw}" is not a valid port (1-65535)`) + } + return port +} + function applyEnvOverrides(name, cfg) { if (name === 'mysql') { if (process.env.NICTOOL_DB_HOST) cfg.host = process.env.NICTOOL_DB_HOST - if (process.env.NICTOOL_DB_PORT) cfg.port = parseInt(process.env.NICTOOL_DB_PORT) + if (process.env.NICTOOL_DB_PORT) cfg.port = parsePort('NICTOOL_DB_PORT') if (process.env.NICTOOL_DB_USER) cfg.user = process.env.NICTOOL_DB_USER if (process.env.NICTOOL_DB_USER_PASSWORD) cfg.password = process.env.NICTOOL_DB_USER_PASSWORD if (process.env.NICTOOL_DB_NAME) cfg.database = process.env.NICTOOL_DB_NAME } if (name === 'http') { if (process.env.NICTOOL_HTTP_HOST) cfg.host = process.env.NICTOOL_HTTP_HOST - if (process.env.NICTOOL_HTTP_PORT) cfg.port = parseInt(process.env.NICTOOL_HTTP_PORT) + if (process.env.NICTOOL_HTTP_PORT) cfg.port = parsePort('NICTOOL_HTTP_PORT') } } diff --git a/sql/init-mysql.sh b/sql/init-mysql.sh index 876a7a9..9e10eac 100755 --- a/sql/init-mysql.sh +++ b/sql/init-mysql.sh @@ -44,6 +44,8 @@ fi $MYSQL_CMD -e "CREATE DATABASE IF NOT EXISTS $DB_NAME;" || exit 1 +# In Docker, SQL_DIR is set via compose env (e.g. /sql). +# Outside Docker, defaults to ./sql relative to CWD (typically the repo root). SQL_DIR="${SQL_DIR:-./sql}" for f in "$SQL_DIR"/*.sql; @@ -52,4 +54,4 @@ do $MYSQL_CMD "$DB_NAME" < "$f" || exit 1 done -exit 0 \ No newline at end of file +exit 0