Skip to content

Commit e3f4f5b

Browse files
authored
feat: docker compose support + env var config overrides (#40)
1 parent fb7fce0 commit e3f4f5b

File tree

9 files changed

+250
-25
lines changed

9 files changed

+250
-25
lines changed

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules/
2+
.git/
3+
.github/
4+
dist/
5+
*.md
6+
.env
7+
.env.example
8+
.eslintrc*
9+
eslint.config*

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ jobs:
9797
- run: npm install
9898
- run: npm test
9999

100+
test-docker:
101+
runs-on: ubuntu-latest
102+
steps:
103+
- uses: actions/checkout@v6
104+
- name: Generate .env
105+
run: ./docker/generate-env.sh
106+
- name: Build and start services
107+
run: docker compose -f docker/docker-compose.yml up --build -d --wait
108+
- name: Run tests
109+
run: docker compose -f docker/docker-compose.yml exec api npm test
110+
- name: Logs on failure
111+
if: failure()
112+
run: docker compose -f docker/docker-compose.yml logs
113+
- name: Tear down
114+
if: always()
115+
run: docker compose -f docker/docker-compose.yml down -v
116+
100117
test-win:
101118
permissions:
102119
contents: read

docker/.env.example

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# NicTool API environment configuration
2+
# Copy to .env, or run: ./docker/generate-env.sh
3+
4+
# --- Database (MariaDB/MySQL) ---
5+
DB_ROOT_PASSWORD=changeme
6+
NICTOOL_DB_NAME=nictool
7+
NICTOOL_DB_USER=nictool
8+
NICTOOL_DB_USER_PASSWORD=changeme
9+
10+
# --- API config overrides (optional, override conf.d/*.toml defaults) ---
11+
# NICTOOL_DB_HOST=127.0.0.1
12+
# NICTOOL_DB_PORT=3306
13+
# NICTOOL_HTTP_HOST=localhost
14+
# NICTOOL_HTTP_PORT=3000
15+
16+
# --- Docker Compose port mapping ---
17+
# DB_PORT=3307
18+
# API_PORT=3000

docker/Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM node:22-trixie-slim
2+
WORKDIR /app
3+
COPY package*.json .
4+
# --omit=dev is safe: tests use node:test (stdlib), devDeps are only eslint/prettier
5+
RUN npm install --omit=dev
6+
COPY . .
7+
EXPOSE 3000
8+
CMD ["node", "server.js"]

docker/docker-compose.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
services:
2+
db:
3+
image: mariadb:11
4+
environment:
5+
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
6+
MYSQL_ROOT_HOST: '%'
7+
MYSQL_DATABASE: ${NICTOOL_DB_NAME:-nictool}
8+
MYSQL_USER: ${NICTOOL_DB_USER:-nictool}
9+
MYSQL_PASSWORD: ${NICTOOL_DB_USER_PASSWORD}
10+
SQL_DIR: /sql
11+
volumes:
12+
- db-data:/var/lib/mysql
13+
- ../sql:/sql:ro
14+
- ../sql/init-mysql.sh:/docker-entrypoint-initdb.d/init-mysql.sh:ro
15+
ports:
16+
- "${DB_PORT:-3307}:3306"
17+
healthcheck:
18+
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
19+
interval: 5s
20+
timeout: 5s
21+
retries: 10
22+
23+
api:
24+
build:
25+
context: ..
26+
dockerfile: docker/Dockerfile
27+
ports:
28+
- "${API_PORT:-3000}:3000"
29+
depends_on:
30+
db:
31+
condition: service_healthy
32+
environment:
33+
NODE_ENV: development
34+
NICTOOL_DB_HOST: db
35+
NICTOOL_DB_USER: ${NICTOOL_DB_USER:-nictool}
36+
NICTOOL_DB_USER_PASSWORD: ${NICTOOL_DB_USER_PASSWORD}
37+
NICTOOL_DB_NAME: ${NICTOOL_DB_NAME:-nictool}
38+
NICTOOL_HTTP_HOST: "0.0.0.0"
39+
healthcheck:
40+
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/documentation').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
41+
interval: 10s
42+
timeout: 5s
43+
retries: 10
44+
start_period: 15s
45+
46+
server:
47+
profiles: [ui, e2e]
48+
build:
49+
context: ${NICTOOL_SERVER_PATH:-../../server}
50+
dockerfile: docker/Dockerfile
51+
hostname: localhost
52+
ports:
53+
- "${SERVER_PORT:-8080}:${SERVER_CONTAINER_PORT:-8080}"
54+
depends_on:
55+
api:
56+
condition: service_healthy
57+
volumes:
58+
- ${NICTOOL_SERVER_PATH:-../../server}/index.js:/app/index.js:ro
59+
- ${NICTOOL_SERVER_PATH:-../../server}/bin:/app/bin:ro
60+
- ${NICTOOL_SERVER_PATH:-../../server}/html:/app/html:ro
61+
environment:
62+
NICTOOL_TLS: ${NICTOOL_TLS:-auto}
63+
NICTOOL_DB_HOST: db
64+
NICTOOL_DB_USER: ${NICTOOL_DB_USER:-nictool}
65+
NICTOOL_DB_USER_PASSWORD: ${NICTOOL_DB_USER_PASSWORD}
66+
NICTOOL_DB_NAME: ${NICTOOL_DB_NAME:-nictool}
67+
NICTOOL_API_HOST: api
68+
NICTOOL_API_PORT: 3000
69+
NICTOOL_BIND_HOST: "0.0.0.0"
70+
71+
volumes:
72+
db-data:

docker/generate-env.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
set -e
3+
4+
ENV_FILE="$(cd "$(dirname "$0")" && pwd)/.env"
5+
6+
if [ -f "$ENV_FILE" ]; then
7+
echo ".env already exists, not overwriting." >&2
8+
exit 0
9+
fi
10+
11+
DB_ROOT_PW=$(openssl rand -base64 24)
12+
NT_DB_PW=$(openssl rand -base64 24)
13+
14+
cat > "$ENV_FILE" <<EOF
15+
# NicTool API environment configuration
16+
# Generated by docker/generate-env.sh
17+
18+
# --- Database (MariaDB/MySQL) ---
19+
DB_ROOT_PASSWORD=$DB_ROOT_PW
20+
NICTOOL_DB_NAME=nictool
21+
NICTOOL_DB_USER=nictool
22+
NICTOOL_DB_USER_PASSWORD=$NT_DB_PW
23+
24+
# --- API config overrides (optional, override conf.d/*.toml defaults) ---
25+
# NICTOOL_DB_HOST=127.0.0.1
26+
# NICTOOL_DB_PORT=3306
27+
# NICTOOL_HTTP_HOST=localhost
28+
# NICTOOL_HTTP_PORT=3000
29+
30+
# --- Docker Compose port mapping ---
31+
# DB_PORT=3307
32+
# API_PORT=3000
33+
EOF
34+
35+
echo "Generated $ENV_FILE with random passwords."

lib/config.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Config {
1717

1818
const str = await fs.readFile(`./conf.d/${name}.toml`, 'utf8')
1919
const cfg = parse(str)
20+
applyEnvOverrides(name, cfg)
2021
if (this.debug) console.debug(cfg)
2122

2223
if (name === 'http') {
@@ -35,6 +36,7 @@ class Config {
3536

3637
const str = fsSync.readFileSync(`./conf.d/${name}.toml`, 'utf8')
3738
const cfg = parse(str)
39+
applyEnvOverrides(name, cfg)
3840
if (this.debug) console.debug(cfg)
3941

4042
if (name === 'http') {
@@ -47,6 +49,30 @@ class Config {
4749
}
4850
}
4951

52+
function parsePort(envVar) {
53+
const raw = process.env[envVar]
54+
if (!raw) return undefined
55+
const port = parseInt(raw, 10)
56+
if (Number.isNaN(port) || port < 1 || port > 65535) {
57+
throw new Error(`${envVar}="${raw}" is not a valid port (1-65535)`)
58+
}
59+
return port
60+
}
61+
62+
function applyEnvOverrides(name, cfg) {
63+
if (name === 'mysql') {
64+
if (process.env.NICTOOL_DB_HOST) cfg.host = process.env.NICTOOL_DB_HOST
65+
if (process.env.NICTOOL_DB_PORT) cfg.port = parsePort('NICTOOL_DB_PORT')
66+
if (process.env.NICTOOL_DB_USER) cfg.user = process.env.NICTOOL_DB_USER
67+
if (process.env.NICTOOL_DB_USER_PASSWORD) cfg.password = process.env.NICTOOL_DB_USER_PASSWORD
68+
if (process.env.NICTOOL_DB_NAME) cfg.database = process.env.NICTOOL_DB_NAME
69+
}
70+
if (name === 'http') {
71+
if (process.env.NICTOOL_HTTP_HOST) cfg.host = process.env.NICTOOL_HTTP_HOST
72+
if (process.env.NICTOOL_HTTP_PORT) cfg.port = parsePort('NICTOOL_HTTP_PORT')
73+
}
74+
}
75+
5076
async function loadPEM(dir) {
5177
let entries
5278
try {

lib/config.test.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
import assert from 'node:assert/strict'
2-
import { describe, it } from 'node:test'
2+
import { describe, it, before, after } from 'node:test'
33

44
import Config from './config.js'
55

6+
const envOverrideKeys = ['NICTOOL_DB_HOST', 'NICTOOL_DB_PORT', 'NICTOOL_DB_USER', 'NICTOOL_DB_USER_PASSWORD', 'NICTOOL_DB_NAME', 'NICTOOL_HTTP_HOST', 'NICTOOL_HTTP_PORT']
7+
68
describe('config', () => {
9+
const savedEnv = {}
10+
11+
before(() => {
12+
for (const key of envOverrideKeys) {
13+
savedEnv[key] = process.env[key]
14+
delete process.env[key]
15+
}
16+
Config.cfg = {}
17+
})
18+
19+
after(() => {
20+
for (const key of envOverrideKeys) {
21+
if (savedEnv[key] !== undefined) process.env[key] = savedEnv[key]
22+
else delete process.env[key]
23+
}
24+
Config.cfg = {}
25+
})
26+
727
describe('get', () => {
828
it(`loads mysql config`, async () => {
929
const cfg = await Config.get('mysql')

sql/init-mysql.sh

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,57 @@
11
#!/bin/sh
22

3+
DB_USER="${DB_USER:-root}"
4+
DB_NAME="${DB_NAME:-nictool}"
35
MYSQL_BIN=""
46

5-
if [ "$MYSQL_PWD" = "" ];
6-
then
7-
export MYSQL_PWD=root
8-
9-
# configure MySQL in the GitHub workflow runners
10-
case "$(uname -s)" in
11-
Linux*)
12-
;;
13-
Darwin*)
14-
MYSQL_BIN=/opt/homebrew/opt/mysql@8.4/bin/
15-
${MYSQL_BIN}mysqladmin --user=root --password='' --protocol=tcp password 'root'
16-
;;
17-
CYGWIN*|MINGW*|MINGW32*|MSYS*)
18-
mysqladmin --user=root --password='' --protocol=tcp password 'root'
19-
# export MYSQL_PWD=""
20-
;;
21-
esac
7+
if [ "$MYSQL_PWD" = "" ]; then
8+
if [ -n "$MYSQL_ROOT_PASSWORD" ]; then
9+
# Docker: MYSQL_ROOT_PASSWORD is set by the MariaDB container
10+
export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"
11+
else
12+
export MYSQL_PWD=root
13+
14+
# configure MySQL in the GitHub workflow runners
15+
case "$(uname -s)" in
16+
Linux*)
17+
;;
18+
Darwin*)
19+
MYSQL_BIN=/opt/homebrew/opt/mysql@8.4/bin/
20+
${MYSQL_BIN}mysqladmin --user=root --password='' --protocol=tcp password 'root'
21+
;;
22+
CYGWIN*|MINGW*|MINGW32*|MSYS*)
23+
mysqladmin --user=root --password='' --protocol=tcp password 'root'
24+
# export MYSQL_PWD=""
25+
;;
26+
esac
27+
fi
2228
fi
2329

24-
# AUTH="--defaults-extra-file=./sql/my-gha.cnf"
30+
if [ -z "$MYSQL_CMD" ]; then
31+
# prefer mariadb client if available (MariaDB 11+ dropped the mysql symlink)
32+
if [ -z "$MYSQL_BIN" ] && command -v mariadb >/dev/null 2>&1; then
33+
MYSQL_CMD="mariadb --user=$DB_USER"
34+
elif [ -n "$MYSQL_BIN" ]; then
35+
MYSQL_CMD="${MYSQL_BIN}mysql --user=$DB_USER"
36+
else
37+
MYSQL_CMD="mysql --user=$DB_USER"
38+
fi
39+
fi
2540

2641
if [ "$1" = "drop" ]; then
27-
${MYSQL_BIN}mysql --user=root -e 'DROP DATABASE IF EXISTS nictool;' || exit 1
42+
$MYSQL_CMD -e "DROP DATABASE IF EXISTS $DB_NAME;" || exit 1
2843
fi
29-
${MYSQL_BIN}mysql --user=root -e 'CREATE DATABASE nictool;' || exit 1
3044

31-
for f in ./sql/*.sql;
45+
$MYSQL_CMD -e "CREATE DATABASE IF NOT EXISTS $DB_NAME;" || exit 1
46+
47+
# In Docker, SQL_DIR is set via compose env (e.g. /sql).
48+
# Outside Docker, defaults to ./sql relative to CWD (typically the repo root).
49+
SQL_DIR="${SQL_DIR:-./sql}"
50+
51+
for f in "$SQL_DIR"/*.sql;
3252
do
33-
echo "cat $f | ${MYSQL_BIN}mysql nictool"
34-
cat $f | ${MYSQL_BIN}mysql --user=root nictool || exit 1
53+
echo "$f"
54+
$MYSQL_CMD "$DB_NAME" < "$f" || exit 1
3555
done
3656

37-
exit 0
57+
exit 0

0 commit comments

Comments
 (0)