www in docker support

This commit is contained in:
MaddoScientisto 2026-04-22 18:41:37 +02:00
commit c227fce036
2145 changed files with 399596 additions and 58 deletions

View file

@ -0,0 +1,54 @@
# Local JSP Docker Stack
This stack boots the `www` Tomcat webapp locally, seeds MySQL from the checked-in `pg` dump, overrides environment-sensitive `PARM` rows, and captures outgoing email into files.
The default local model seed is `db/pg-model-seed-trimmed-20260421.sql`.
## Services
- `tomcat-www`: Tomcat 9 on Java 11 serving a runtime copy of `www`
- `mysql`: MySQL 8 with first-boot import of `db/pg-model-seed-trimmed-20260421.sql` by default
- `maildump`: local SMTP sink writing `.eml`, `.txt`, and `.html` payloads to disk
## What Gets Mocked
- `PARM.DOCBASE` points to `/data/docbase/`
- temp, help, and mailer paths are redirected under `/data/docbase/`
- SMTP points to the local `maildump` service
- common external-storage style paths are mapped to local fixture directories from `test_pkl`
## Start
From this folder:
```powershell
docker compose up --build
```
The public site is exposed on `http://localhost:8080`.
Mail output is written under `local-jsp-docker/runtime/maildump/out`.
## First Boot
The first MySQL start imports the file named by `LOCAL_DB_SEED_DUMP`, which defaults to `db/pg-model-seed-trimmed-20260421.sql`.
After the import finishes, the local override script updates `PARM` with local-safe values.
The Tomcat runtime also patches the deployed `WEB-INF/web.xml` so the `instance` context-param is `local-model` instead of `main`. That keeps the startup DB updater servlets from mutating the model database during local boot.
## Restart Workflow
JSP edits are picked up by restarting `tomcat-www`:
```powershell
docker compose restart tomcat-www
```
The Tomcat container copies `www` into a runtime directory on each start, patches the deployed `WEB-INF/web.xml`, and then runs Tomcat.
## Notes
- This stack currently targets the `www` runtime only.
- PHP files in `www` are not executed by this stack.
- If the app still references production-only paths from DB data, add them to `mysql/init/20-local-overrides.sh` and `tomcat/bootstrap-docbase.sh`.

View file

@ -0,0 +1,91 @@
services:
tomcat-www:
build:
context: .
dockerfile: tomcat/Dockerfile
container_name: regalami-local-jsp-tomcat
restart: unless-stopped
environment:
LOCAL_DB_HOST: mysql
LOCAL_DB_PORT: 3306
LOCAL_DB_NAME: pg
LOCAL_DB_USER: root
LOCAL_DB_PASSWORD: root
FACEAI_HANDOFF_URL: ${FACEAI_HANDOFF_URL:-http://localhost:8081/faceai_handoff.php}
FACEAI_DEV_USER_ID: ${FACEAI_DEV_USER_ID:-1}
FACEAI_DEV_DISPLAY_NAME: ${FACEAI_DEV_DISPLAY_NAME:-Local Model User}
FACEAI_DEV_EMAIL: ${FACEAI_DEV_EMAIL:-local.model.user@example.invalid}
FACEAI_DEV_MEMBERSHIP_STATUS: ${FACEAI_DEV_MEMBERSHIP_STATUS:-active}
LOCAL_APP_INSTANCE: ${LOCAL_APP_INSTANCE:-local-model}
LOCAL_DOCBASE: /data/docbase/
FACEAI_FEATURE_ENABLED: ${FACEAI_FEATURE_ENABLED:-1}
FACEAI_FRONTEND_URL: ${FACEAI_FRONTEND_URL:-http://localhost:3001}
FACEAI_BACKEND_INTERNAL_URL: ${FACEAI_BACKEND_INTERNAL_URL:-http://host.docker.internal:3001}
FACEAI_SHARED_SECRET: ${FACEAI_SHARED_SECRET:-change-me}
FACEAI_ALLOW_DEV_HANDOFF: ${FACEAI_ALLOW_DEV_HANDOFF:-1}
FACEAI_IDENTITY_COOKIE: ${FACEAI_IDENTITY_COOKIE:-rus_faceai_identity}
volumes:
- ../www:/workspace/www:ro
- ../test_pkl:/workspace/test_pkl:ro
- ./runtime/docbase:/data/docbase
- ./runtime/tomcat-work:/usr/local/tomcat/work
- ./runtime/tomcat-logs:/usr/local/tomcat/logs
ports:
- "8080:8080"
depends_on:
mysql:
condition: service_healthy
maildump:
condition: service_started
mysql:
image: mysql:8.4
container_name: regalami-local-jsp-mysql
restart: unless-stopped
command:
- --max_allowed_packet=1G
- --net_read_timeout=600
- --net_write_timeout=600
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: pg
MYSQL_ROOT_HOST: '%'
LOCAL_DB_SEED_DUMP: ${LOCAL_DB_SEED_DUMP:-pg-model-seed-trimmed-20260421.sql}
LOCAL_DOCBASE: /data/docbase/
LOCAL_MAIL_SMTP_HOST: maildump
LOCAL_MAIL_SMTP_PORT: 1025
LOCAL_SOURCE_DIR: /workspace/www/
volumes:
- mysql-data:/var/lib/mysql
- ../db:/seed:ro
- ../www:/workspace/www:ro
- ./mysql/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test:
- CMD
- mysqladmin
- ping
- -h
- 127.0.0.1
- -proot
interval: 10s
timeout: 5s
retries: 30
start_period: 60s
maildump:
build:
context: .
dockerfile: maildump/Dockerfile
container_name: regalami-local-jsp-maildump
restart: unless-stopped
environment:
MAILDUMP_OUTPUT_DIR: /out
MAILDUMP_SMTP_PORT: 1025
volumes:
- ./runtime/maildump/out:/out
ports:
- "8025:8025"
volumes:
mysql-data:

View file

@ -0,0 +1,11 @@
FROM python:3.12-slim
RUN pip install --no-cache-dir aiosmtpd
COPY maildump/maildump.py /app/maildump.py
WORKDIR /app
EXPOSE 1025 8025
CMD ["python", "/app/maildump.py"]

View file

@ -0,0 +1,101 @@
from __future__ import annotations
import asyncio
import email
import os
import re
from datetime import datetime, timezone
from email import policy
from email.message import Message
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from threading import Thread
from aiosmtpd.controller import Controller
OUTPUT_DIR = Path(os.environ.get("MAILDUMP_OUTPUT_DIR", "/out"))
SMTP_PORT = int(os.environ.get("MAILDUMP_SMTP_PORT", "1025"))
HTTP_PORT = int(os.environ.get("MAILDUMP_HTTP_PORT", "8025"))
def safe_name(value: str) -> str:
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip())
return cleaned.strip("-.") or "message"
def extract_part(message: Message, wanted: str) -> str:
if message.is_multipart():
for part in message.walk():
if part.get_content_type() == wanted:
payload = part.get_payload(decode=True) or b""
charset = part.get_content_charset() or "utf-8"
return payload.decode(charset, errors="replace")
return ""
if message.get_content_type() != wanted:
return ""
payload = message.get_payload(decode=True) or b""
charset = message.get_content_charset() or "utf-8"
return payload.decode(charset, errors="replace")
class MailDumpHandler:
async def handle_DATA(self, server, session, envelope):
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
parsed = email.message_from_bytes(envelope.content, policy=policy.default)
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
subject = safe_name(parsed.get("subject", "no-subject"))
message_root = OUTPUT_DIR / f"{timestamp}-{subject}"
message_root.mkdir(parents=True, exist_ok=True)
(message_root / "message.eml").write_bytes(envelope.content)
(message_root / "metadata.txt").write_text(
"\n".join(
[
f"mail_from: {envelope.mail_from}",
f"rcpt_tos: {', '.join(envelope.rcpt_tos)}",
f"subject: {parsed.get('subject', '')}",
f"date: {parsed.get('date', '')}",
]
)
+ "\n",
encoding="utf-8",
)
text_body = extract_part(parsed, "text/plain")
html_body = extract_part(parsed, "text/html")
if text_body:
(message_root / "body.txt").write_text(text_body, encoding="utf-8")
if html_body:
(message_root / "body.html").write_text(html_body, encoding="utf-8")
return "250 OK"
def serve_http() -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
os.chdir(OUTPUT_DIR)
server = ThreadingHTTPServer(("0.0.0.0", HTTP_PORT), SimpleHTTPRequestHandler)
server.serve_forever()
async def main() -> None:
http_thread = Thread(target=serve_http, daemon=True)
http_thread.start()
controller = Controller(MailDumpHandler(), hostname="0.0.0.0", port=SMTP_PORT)
controller.start()
try:
while True:
await asyncio.sleep(3600)
finally:
controller.stop()
if __name__ == "__main__":
asyncio.run(main())

View file

@ -0,0 +1,12 @@
#!/bin/sh
set -eu
seed_dump="${LOCAL_DB_SEED_DUMP:-pg-model-seed-trimmed-20260421.sql}"
if [ ! -f "/seed/${seed_dump}" ]; then
echo "Seed dump not found: /seed/${seed_dump}" >&2
exit 1
fi
echo "Importing seed dump: ${seed_dump}"
mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" < "/seed/${seed_dump}"

View file

@ -0,0 +1,33 @@
#!/bin/sh
set -eu
mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" <<SQL
INSERT INTO parm (codice, descrizione, testo, numero, tipoParm, flgTipo, flgAdmin, createTmst, lastUpdTmst)
VALUES
('DOCBASE', 'Local Docker docbase root', '${LOCAL_DOCBASE}', NULL, 'STRING', 0, 1, NOW(), NOW()),
('PATH_TMP', 'Local Docker tmp path', '_tmp/', NULL, 'STRING', 0, 1, NOW(), NOW()),
('MAIL_MSG_PATH_MAILER', 'Mail template directory', 'mailMessage/', NULL, 'STRING', 0, 1, NOW(), NOW()),
('SMTP', 'Local Docker SMTP host', '${LOCAL_MAIL_SMTP_HOST}', NULL, 'STRING', 0, 1, NOW(), NOW()),
('SMTP_PORT', 'Local Docker SMTP port', '${LOCAL_MAIL_SMTP_PORT}', ${LOCAL_MAIL_SMTP_PORT}, 'NUMBER', 0, 1, NOW(), NOW()),
('SMTP_USE_AUTH', 'Disable SMTP auth locally', '0', 0, 'NUMBER', 0, 1, NOW(), NOW()),
('SMTP_STARTTLS', 'Disable STARTTLS locally', '0', 0, 'NUMBER', 0, 1, NOW(), NOW()),
('FROM', 'Local Docker sender', 'local-dev@regalamiunsorriso.test', NULL, 'STRING', 0, 1, NOW(), NOW()),
('TO_TEST', 'Force all mail to local test inbox', 'mail-capture@regalamiunsorriso.test', NULL, 'STRING', 0, 1, NOW(), NOW()),
('BCC', 'Disable default BCC locally', '', NULL, 'STRING', 0, 1, NOW(), NOW()),
('CC', 'Disable default CC locally', '', NULL, 'STRING', 0, 1, NOW(), NOW()),
('PATH_IMG_ART', 'Local image path', '_img/', NULL, 'STRING', 0, 1, NOW(), NOW()),
('LOG_MAIL_ATTACH', 'Local mail attachment log path', '_logs/mail-attach/', NULL, 'STRING', 0, 1, NOW(), NOW()),
('DAILY_CRONTAB_MAIN_LOG_FILE', 'Local crontab log file', '${LOCAL_DOCBASE}_logs/daily-crontab.log', NULL, 'STRING', 0, 1, NOW(), NOW()),
('HELP_ATTACH_PATH', 'Local help attachment path', '_help/attach/', NULL, 'STRING', 0, 1, NOW(), NOW()),
('HELP_EXPORT_PATH', 'Local help export path', '_help/export/', NULL, 'STRING', 0, 1, NOW(), NOW()),
('HELP_IMG_PATH', 'Local help image path', '_help/img/', NULL, 'STRING', 0, 1, NOW(), NOW()),
('SOURCE_DIR', 'Workspace source path', '${LOCAL_SOURCE_DIR}', NULL, 'STRING', 0, 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE
descrizione = VALUES(descrizione),
testo = VALUES(testo),
numero = VALUES(numero),
tipoParm = VALUES(tipoParm),
flgTipo = VALUES(flgTipo),
flgAdmin = VALUES(flgAdmin),
lastUpdTmst = NOW();
SQL

View file

@ -0,0 +1,14 @@
FROM tomcat:9.0.102-jdk11-temurin
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bash perl rsync \
&& rm -rf /var/lib/apt/lists/*
COPY tomcat/bootstrap-docbase.sh /usr/local/bin/bootstrap-docbase.sh
COPY tomcat/entrypoint.sh /usr/local/bin/local-jsp-entrypoint.sh
RUN chmod +x /usr/local/bin/bootstrap-docbase.sh /usr/local/bin/local-jsp-entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/local-jsp-entrypoint.sh"]

View file

@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
docbase="${LOCAL_DOCBASE:-/data/docbase/}"
workspace_www="/workspace/www"
workspace_pkl="/workspace/test_pkl"
mkdir -p \
"${docbase}" \
"${docbase}_tmp" \
"${docbase}_logs" \
"${docbase}admin/csv" \
"${docbase}_help/attach" \
"${docbase}_help/export" \
"${docbase}_help/img"
link_into_docbase() {
local source_path="$1"
local target_path="$2"
rm -rf "$target_path"
ln -s "$source_path" "$target_path"
}
if [ -d "${workspace_www}/mailMessage" ]; then
link_into_docbase "${workspace_www}/mailMessage" "${docbase}mailMessage"
fi
for subdir in _attach _csv _docs _img _imgMsg csv pics images pdf Templates tmp; do
if [ -e "${workspace_www}/${subdir}" ]; then
link_into_docbase "${workspace_www}/${subdir}" "${docbase}${subdir}"
fi
done
if [ -d "${workspace_pkl}" ]; then
link_into_docbase "${workspace_pkl}" "${docbase}test_pkl"
for subdir in 2026 live test_images; do
if [ -e "${workspace_pkl}/${subdir}" ]; then
link_into_docbase "${workspace_pkl}/${subdir}" "${docbase}${subdir}"
fi
done
# Common generic aliases for code that expects a mounted external storage root.
for alias_name in storage nas archive pkl RUS; do
link_into_docbase "${workspace_pkl}" "${docbase}${alias_name}"
done
fi

View file

@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
export CATALINA_TMPDIR=/usr/local/tomcat/temp
runtime_root="/usr/local/tomcat/webapps/ROOT"
workspace_www="/workspace/www"
rm -rf "$runtime_root"
mkdir -p "$runtime_root"
shopt -s dotglob nullglob
for source_path in "$workspace_www"/*; do
entry_name="$(basename "$source_path")"
if [ "$entry_name" = "WEB-INF" ]; then
continue
fi
ln -s "$source_path" "$runtime_root/$entry_name"
done
mkdir -p "$runtime_root/WEB-INF"
cp -a "$workspace_www/WEB-INF/." "$runtime_root/WEB-INF/"
mkdir -p /usr/local/tomcat/conf/Catalina/localhost
cat > /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml <<'EOF'
<Context>
<Resources allowLinking="true" />
</Context>
EOF
/usr/local/bin/bootstrap-docbase.sh
patch_context_param() {
local param_name="$1"
local param_value="$2"
PARAM_NAME="$param_name" PARAM_VALUE="$param_value" perl -0pi -e 's#(<param-name>\Q$ENV{PARAM_NAME}\E</param-name>\s*<param-value>)[^<]*(</param-value>)#$1 . $ENV{PARAM_VALUE} . $2#egs' "$runtime_root/WEB-INF/web.xml"
}
patch_context_param database "//${LOCAL_DB_HOST:-mysql}/${LOCAL_DB_NAME:-pg}"
patch_context_param catalog "${LOCAL_DB_NAME:-pg}"
patch_context_param user "${LOCAL_DB_USER:-root}"
patch_context_param password "${LOCAL_DB_PASSWORD:-root}"
patch_context_param instance "${LOCAL_APP_INSTANCE:-local-model}"
exec catalina.sh run