From bdafd3f2c33557129e20ccec517541133d66222a Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 24 May 2026 21:43:43 -0400 Subject: [PATCH] Self-host LanguageTool in Docker Compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add erikvl87/languagetool service on the internal network (port 8010, English only by default — change langsToLoad to add more) - 256 MB min / 512 MB max heap; healthcheck waits up to ~3 min for first startup while Java initialises - Server LANGUAGETOOL_URL hardwired to http://languagetool:8010/v2 — no character limits, no rate limits, fully private - Remove 40 000-char cap from lint route (not needed self-hosted) - Bump fetch timeout to 30 s for cold-start requests Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 18 ++++++++++++++++++ server/routes/lint.js | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9e583c3..6c903f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - OLLAMA_URL=${OLLAMA_URL:-https://ai.binarygnome.com} - OLLAMA_MODEL=${OLLAMA_MODEL:-gemma4:latest} - OLLAMA_API_KEY=${OLLAMA_API_KEY:-} + - LANGUAGETOOL_URL=http://languagetool:8010/v2 extra_hosts: - "host-gateway:host-gateway" networks: @@ -22,6 +23,23 @@ services: retries: 10 start_period: 15s + languagetool: + image: erikvl87/languagetool + restart: unless-stopped + environment: + - Java_Xms=256m + - Java_Xmx=512m + # Add more languages (comma-separated) or remove this line to load all + - langsToLoad=en + networks: + - internal + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8010/v2/languages > /dev/null"] + interval: 10s + timeout: 5s + retries: 18 # LT takes ~30–60 s to start on first run + start_period: 90s + app: build: ./frontend restart: unless-stopped diff --git a/server/routes/lint.js b/server/routes/lint.js index 1a71d90..71bdf50 100644 --- a/server/routes/lint.js +++ b/server/routes/lint.js @@ -11,7 +11,6 @@ router.use(auth) router.post('/check', async (req, res) => { const { text, language = 'en-US' } = req.body || {} if (!text || typeof text !== 'string') return res.status(400).json({ error: 'text required' }) - if (text.length > 40000) return res.status(400).json({ error: 'text too long (max 40 000 chars)' }) try { const body = new URLSearchParams({ @@ -25,7 +24,7 @@ router.post('/check', async (req, res) => { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }, body: body.toString(), - signal: AbortSignal.timeout(20000), + signal: AbortSignal.timeout(30000), }) if (!r.ok) {