This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Docker image for LanguageTool built on Alpine Linux with a custom minimal JRE, fasttext support, and automatic ngram model downloads. Published to Docker Hub as meyay/languagetool.
# Build the standard image (uses Alpine fasttext package)
docker build -t meyay/languagetool:latest .
# Build with compiled-from-source fasttext (for CPU compatibility issues)
sudo docker build -t meyay/languagetool:latest -f Dockerfile.fasttext .
sudo make docker_build # same as above via Makefile
# Start the container locally
docker compose up -d
# Test the API after startup
curl --data "language=en-US&text=a simple test" http://127.0.0.1:8081/v2/checkBoth Dockerfile and Dockerfile.fasttext use the same multi-stage pattern:
base— pinned Alpine base imagejava_base— base + locale/timezone packagesprepare— downloads Eclipse Temurin JDK + Apache Maven, clones LanguageTool from GitHub at the tag matchingLT_VERSION, patchespom.xmlfor CVE fixes (logback, Jackson), builds with Maven, then usesjlinkto produce a minimal custom JRE (/opt/java/customjre) with only the modules LanguageTool actually needsfasttext(Dockerfile.fasttext only) — compiles fasttext from source with gcc13 patch + UPX compressiongo_build— compiles theentrypointGo binary (see below);ARG GO_VERSIONmust be declared in global scope (before the firstFROM) so it can be referenced in theFROM golang:${GO_VERSION}line- final — copies
/languagetooland/opt/java/customjrefromprepare, installs runtime Alpine packages, sets uplanguagetooluser (UID/GID 783)
Every pinned version in the Dockerfiles has a # renovate: comment above it. Renovate bot uses regular expression matching to track and auto-update these versions. When bumping any package or tool version, always update the corresponding ARG line and its renovate comment.
Direct CVE fixes are applied two ways:
pom.xmlpatches viaxmlstarlet(logback, Jackson) during the Maven build- Direct JAR replacements via
wgetinto/languagetool/libs/after extraction (netty, opennlp)
The container entry point is a statically-linked Go binary (CGO_ENABLED=0 GOOS=linux, cross-compiled per target architecture — amd64 and arm64) that replaces the former entrypoint.sh. It requires no shell, su-exec, shadow, or xmlstarlet in the runtime image. See entrypoint/README.md for the full startup sequence and environment variable reference.
Key implementation rules:
- No hardcoded UID/GID numbers. Effective UID/GID is derived once at startup:
MAP_UID/MAP_GIDenv vars when root,os.Getuid()/os.Getgid()when unprivileged. Every downstream operation (ownership fix, download subprocess, privilege drop) uses this single computed value. - Download URLs/filenames live in
entrypoint/internal/download/downloads.yaml(embedded via//go:embed). Edit the YAML to update URLs; no Go recompile is needed. - Read-only filesystem detection uses
syscall.Statfs("/", &stat)checkingstat.Flags & 0x1(ST_RDONLY). Do not parse/proc/mounts. - Privilege drop uses
runtime.LockOSThread()+syscall.Setgid+syscall.Setuid+syscall.Exec. Do not usesu-execor similar wrappers. - Download isolation: when root, downloads are run by re-invoking the binary via
--_internal-runwithexec.Cmd.SysProcAttr.Credential{Uid, Gid}so downloaded files are owned by the target user. go mod tidyruns inside the Dockergo_buildstage — there is no local Go install requirement. Thego.sumfile is committed and kept up to date.go.summust be committed. Rungo mod tidyinentrypoint/after any dependency change.
Tags follow the pattern {LT_VERSION}-{sequential_number} (e.g., 6.8-0). The CI pipeline auto-increments the number by querying existing GitHub tags. IMAGE_VERSION and IMAGE_CREATED are build args set at build time.
- super-linter — runs on PRs and feature branches only
- build-test-image — builds and pushes to GHCR with the run ID as tag
- integration-test-image — runs 6 scenarios × 2 platforms (ubuntu-latest, ubuntu-24.04-arm) from
.github/tests/(privileged/unprivileged × rw/ro/no-volumes) - scan-image — Grype CVE scan (non-blocking on PRs/branches)
- cve-check-image-and-report — blocking CVE scan, runs only on tags
- retag-and-push-final-image — retags GHCR image and pushes to Docker Hub with versioned +
latesttags; runs only on tags - create-release-tag — creates and pushes the next
{LT_VERSION}-{sequential_number}git tag; only runs on a manualworkflow_dispatchrun onmain. Pushing that tag re-triggers the pipeline, which then runs the CVE check, retag/push, and release steps above. The tag is created using a short-lived GitHub App installation token (actions/create-github-app-token, app ID/private key in theRELEASE_APP_ID/RELEASE_APP_PRIVATE_KEYsecrets), not the defaultGITHUB_TOKEN— refs created withGITHUB_TOKENdo not trigger new workflow runs, which would silently break the re-trigger this step depends on. - create-github-release — creates the GitHub release (with auto-generated notes) for the pushed tag; runs only on tags
Branch naming: the pipeline triggers on feature/* branches (not feat/*). Always use feature/ as the branch prefix for development branches.
Files in .github/tests/ each define a specific runtime scenario. They all require the IMAGE environment variable to be set. The privileged-*.yml variants run as root with Linux capabilities; unprivileged-*.yml variants use user: "1001:1001".
- LanguageTool is built from GitHub tags (not release zips, which were discontinued after v6.6)
- The
patches/directory contains version-specific patches:gcc13.patch(fasttext),no-march-native.patch(fasttext),lt6_7_memory_leak_fix.patch(applied conditionally in the Dockerfile only whenLT_VERSION == 6.7) /tmpmust be mounted astmpfswithexecpermissions — JNA extracts native libs there- Default listen port is
8081(changed from8010in version 6.6-0)
When working on any .go file in this repository:
- Use LSP instead of grep for all code navigation and symbol lookup (finding definitions, references, implementations). Prefer
mcp__ide__getDiagnosticsand LSP-based tools overgrep/ripgrepfor Go source. - Follow Effective Go conventions: https://go.dev/doc/effective_go — idiomatic naming, error handling, interfaces, and concurrency patterns.
- Lint after every edit. After modifying any Go file, run
golangci-lint run ./...fromentrypoint/before considering the task done. Fix all reported issues. - Format on save. Run
gofmt -w(orgoimports -w) on every modified.gofile.
Prettier is the authoritative formatter for YAML, Markdown, and JSON files in this repository (enforced by YAML_PRETTIER, MARKDOWN_PRETTIER, and JSON_PRETTIER in super-linter). Always run it before committing to avoid CI failures.
After modifying any .yaml file under .github/workflows/:
prettier --write ".github/workflows/*.yaml"After modifying any .md file:
prettier --write "*.md"After modifying any .json file:
prettier --write "*.json"CHANGELOG.md follows Keep a Changelog format. Each release is a ## [tag] - date section with ### Added, ### Changed, ### Fixed, and ### Security subsections (omit any that have no entries).
CHANGELOG.md is updated manually. When editing, follow the same structure.
.markdownlint.json sets MD024: siblings_only: true to allow the repeated category headings across version sections — do not remove this rule.
- Commit messages follow Conventional Commits:
type(scope): description(e.g.feat(download): …,fix(entrypoint): …,chore(deps): …,refactor(mount): …,docs(entrypoint): …). - Feature branches must be named
feature/<name>— the CI pipeline only triggers onfeature/*, notfeat/*.