Use case · Writing tools

LanguageTool self hosted: the official HTTP server on a dedicated machine

A self-hosted LanguageTool is the project's own embedded HTTP server, the class org.languagetool.server.HTTPServer from the daily snapshot ZIP, running on Java 17 to 21 under a service account and answering on port 8081. Below: what it needs from a machine, the install, the optional n-gram data and its real disk footprint, a systemd unit, and how to keep the API off the open internet, because the server has no login of its own.

See the machines

What the self-hosted server is

LanguageTool ships an embedded HTTP server so that a local instance can check text sent to it, and it speaks the JSON API documented for the public service (/v2/check). Per the change log, 6.6 (27 March 2025) was the last versioned ZIP; releases now come as daily snapshots, each holding languagetool-server.jar.

  • You get a basic server without the AI-based rules, which the docs reserve for the cloud.
  • The desktop app for Windows cannot use your own server, and synonyms in the browser add-on are unsupported against it.
  • There is no official Docker image. The GitHub README points to three community-contributed projects (meyayl/docker-languagetool, Erikvl87/docker-languagetool and silvio/docker-languagetool); they are unofficial.

If you run one of those community images anyway, publish its port on 127.0.0.1 only. A mapping without an address listens on every host address, 0.0.0.0 and [::] alike; that traffic bypasses ufw's rules on IPv4, and also on IPv6 if Docker's own IPv6 networking is on. On Docker Engine 28 or later, a 127.0.0.1 mapping is reachable from the machine itself only.

Sizing: disk first, then memory

The base server is small. The optional n-gram data, which catches confused words such as their/there, is large and wants an SSD. The project publishes no RAM or CPU figure.

ItemFigureSource
Java17 up to 21Change log on GitHub, 6.9 (unreleased); the 6.6 log on languagetool.org says Java 17 or later
Snapshot ZIPabout 263 MB; about 420 MB unpackedSnapshot folder on languagetool.org; the unpacked total is our reading of the archive's file list
English n-gram ZIPabout 9 GB (8,961,853,713 bytes); the docs round it to ~8GBngram-data folder on languagetool.org
English n-grams unpackedabout 15.4 GBOur reading of the ZIP's file list
German, Spanish, French n-gram ZIPsabout 1.7, 1.7 and 1.8 GBngram-data folder
Disk type for n-gramsSSDn-gram page: without an SSD, LanguageTool can get much slower

This part is our own arithmetic: unpacking English while the ZIP is still on disk needs roughly 24.4 GB free at the peak. For memory, the README's one piece of advice (written for the command-line tool) is to raise the Java heap with -Xmx if Java runs out of memory; the same flag goes in the ExecStart line below.

Choosing the machine

Read the Storage column on the dedicated server lines before the processor column. A line listed with SATA-SSD or NVMe-SSD storage matches the n-gram advice; a line listed with plain SATA disks, such as the Intel Atom C2750 cartridge as it stands, does not. Without n-grams, the smallest SSD cartridge has disk to spare.

  • Whole machine, root, no second tenant on the hardware: the checker and whatever runs next to it get every core and all the memory.
  • Every racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC). Of the cartridges, only the m510 and m710x carry one; the rest offer power control alone.
  • Not the X-Gene ARM cartridge: it installs only Ubuntu 16.04 and 18.04, and the steps here rely on Ubuntu 24.04 packages for Java 21.
  • A racked machine is handed over in about 30 minutes once payment is approved; a built-to-order line takes 4 to 24 hours.

The same machine can carry the documents you are checking. See BookStack on a dedicated server or the Outline wiki setup.

Installing the snapshot under a service user

On Ubuntu 24.04, as a sudo user: install a Java inside the 17-21 range, create a no-login languagetool account, unpack the latest snapshot into a dated release directory and point a current symlink at it. The empty server.properties is what the official page tells you to create when you skip fastText.

sudo apt update
sudo apt install -y openjdk-21-jre-headless unzip curl
sudo useradd --system --user-group --home-dir /opt/languagetool --shell /usr/sbin/nologin languagetool
sudo mkdir -p /opt/languagetool/releases /etc/languagetool
sudo touch /etc/languagetool/server.properties
cd /tmp
curl -fLO https://languagetool.org/download/snapshots/LanguageTool-latest-snapshot.zip
REL=/opt/languagetool/releases/$(date +%F)
sudo mkdir -p "$REL"
sudo unzip -q LanguageTool-latest-snapshot.zip -d "$REL"
sudo ln -sfn "$(ls -d "$REL"/LanguageTool-*)" /opt/languagetool/current
ls /opt/languagetool/current/languagetool-server.jar

The folder inside the ZIP carries the version (today LanguageTool-6.9-SNAPSHOT), hence ls -d for the symlink. Files stay owned by root; the service only reads them.

The official page strongly recommends fastText on Linux for good language detection. Follow its two fastText links to build the binary and fetch lid.176.bin, then put fasttextModel= and fasttextBinary= lines with those paths in /etc/languagetool/server.properties.

Running it under systemd

The unit file goes in /etc/systemd/system/languagetool.service. The command line is the one from the official page, with absolute paths. The explicit Java 21 path keeps the server on a supported Java even if a newer default is installed later. On the Ampere Altra ARM line the same package lives under /usr/lib/jvm/java-21-openjdk-arm64/bin/java.

[Unit]
Description=LanguageTool HTTP server
After=network.target

[Service]
User=languagetool
Group=languagetool
WorkingDirectory=/opt/languagetool/current
ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java -cp /opt/languagetool/current/languagetool-server.jar org.languagetool.server.HTTPServer --config /etc/languagetool/server.properties --port 8081 --allow-origin
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
  1. Load and start it: sudo systemctl daemon-reload && sudo systemctl enable --now languagetool.
  2. Test with the documented call: curl -d "language=en-US" -d "text=a simple test" http://localhost:8081/v2/check. A JSON reply means it works.
  3. Read the log with journalctl -u languagetool. The server prints a warning when it runs as root (seen in its source, Server.java); under this unit it should not.

--allow-origin with no value sets the CORS header to *, i.e. any website, per the server's own help text and HTTPServerConfig.java. The docs say it is needed for the browser add-on; drop it if you only call the API from scripts. Leave --verbose off if the texts are private: when a check hits an exception, it logs up to 500 characters of the input.

Adding the n-gram data

The docs list n-gram data for English, German, French and Spanish (the download folder also holds a Dutch file). Each ZIP unpacks to a folder named for its language, and the setting points at the folder that contains it.

  1. sudo mkdir -p /opt/languagetool/ngrams && cd /opt/languagetool/ngrams
  2. sudo curl -fL -o en.zip https://languagetool.org/download/ngram-data/ngrams-en-20150817.zip
  3. sudo unzip -q en.zip && sudo rm en.zip leaves en/1grams, en/2grams and en/3grams.
  4. echo 'languageModel=/opt/languagetool/ngrams' | sudo tee -a /etc/languagetool/server.properties, then sudo systemctl restart languagetool.

The docs give a test sentence that only the n-gram rule flags: "Don't forget to put on the breaks."

Keeping the API private

Started without --public, the server binds to localhost and accepts requests only from 127.0.0.1 and ::1 (the address list is in Server.java). With --public it prints that the API can be accessed without restrictions (the warning is in HTTPServer.java). The basic server asks callers for no login, so anything that can reach the port can use it. To reach it from elsewhere without --public, use a tunnel (step 1) or a proxy (steps 2 to 4):

  1. Just for you: a tunnel over your own SSH account, ssh -N -L 8081:127.0.0.1:8081 you@your-server. Your laptop's port 8081 is now the server's.
  2. For a team: nginx on the same host with a password and HTTPS, as the official page recommends for TLS. Firewall first: sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw, then sudo ufw allow OpenSSH, sudo ufw allow 80,443/tcp and sudo ufw --force enable. nginx is an ordinary process on the host, so without that rule it is blocked on IPv4 and IPv6 alike.
  3. Install nginx, apache2-utils, certbot and python3-certbot-nginx, create users with sudo htpasswd -c /etc/nginx/languagetool.htpasswd alice, then sudo chown root:www-data and sudo chmod 640 that file.
  4. Save the site below in /etc/nginx/sites-available/languagetool, link it into sites-enabled, run sudo nginx -t && sudo systemctl reload nginx, then sudo certbot --nginx -d lt.example.com before you hand out any password.
server {
    listen 80;
    listen [::]:80;
    server_name lt.example.com;

    location / {
        auth_basic "LanguageTool";
        auth_basic_user_file /etc/nginx/languagetool.htpasswd;
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Because nginx connects from 127.0.0.1, the server keeps its default local-only mode. If you set requestLimit in server.properties (with requestLimitPeriodInSeconds, without which no limit applies, per Server.java), also set trustXForwardForHeader=true so the limit counts the callers' addresses rather than the proxy's.

Add-on, updates, licence

  • Browser add-on: the cog icon leads to its options; under Experimental settings, choose Local server. With the tunnel above running, that means your server. The docs note that Safari needs HTTPS, set as Other server with a URL ending in /v2.
  • Updates: unpack a newer snapshot into a new dated directory, move the current symlink with ln -sfn, then sudo systemctl restart languagetool. The old release stays for a rollback.
  • Backups: copy /etc/languagetool, the unit and the nginx files off the machine yourself; the n-gram data can be fetched again.
  • Licence: LGPL 2.1 or later for the core (README and COPYING.txt). Some bundled libraries and data carry their own terms; the README in the download points to them.

For a notes app that pairs with it, see Joplin Server on bare metal.

Questions

Is there an official LanguageTool Docker image?

No. The GitHub README points to three community-contributed Docker projects instead. The official route is the snapshot ZIP run with Java.

Does a self-hosted server include the AI-based rules?

No. Per the official server page, the download is a basic server and the AI-based rules stay cloud-only. Rule-based checks, spelling and the optional n-gram rules run locally.

Which Java version should I install?

The current snapshot's change log says Java 17 up to 21. The server page still mentions Java 8 or later, which is older text. Install Java 21 and name it in the unit.

Can other machines reach the server on port 8081?

Not by default: without --public it accepts only 127.0.0.1 and ::1. Use an SSH tunnel for yourself, or a password-protected HTTPS proxy on the same host for a team.

How much disk do the English n-grams take?

The ZIP is about 9 GB and its file list adds up to about 15.4 GB unpacked, so keep around 25 GB free while unpacking, on SSD.

Put the checker on your own hardware

Pick a line with SSD or NVMe storage, root and an out-of-band console where the line has one, month to month.

See the machines

Related on Server Room

Other use cases