This is a first version of modulo's configuration reference, and it will move into modulo's own repository as it grows. It describes modulo as it is today, including a few rough edges that are marked as such. For installing modulo in the first place, see the Pure modulo deployment guide.
modulo reads one configuration file, /opt/webobjects/modulo.toml. It holds four kinds of thing: how modulo itself listens ([frontend]), who may use its admin pages ([admin]), where it learns about your applications ([wotaskd]), and the sites it serves ([acme], include and [[sites]]). Everything else has a built-in default, and the few settings that can be changed from the command line are listed at the end.
1. The file
The file is TOML. Two features of TOML matter here. Keys that stand on their own, like include, have to come before the first table header; after [frontend] every key belongs to that table. And strings in single quotes are literal, with no escaping, which is how regular expressions in rewrite rules stay readable.
modulo parses the file strictly. An unknown field, a misspelled table, a hostname claimed by two sites or a malformed entry stops the front end from starting, with an error naming the file and the problem, rather than starting with half a configuration. The file is the source of truth, so a typo should be loud.
Some settings take effect on reload, the others need a restart:
| Settings | A change takes effect |
|---|---|
| include, [acme], [[sites]] | On reload, without dropping a connection: curl -X POST -u :password https://your-admin-host/reload, or the button on the admin overview. |
| [frontend], [admin], [wotaskd] | On restart: systemctl restart modulo. A reload that finds one of these changed says so explicitly instead of ignoring the edit. |
A reload validates first. A file that doesn't parse is refused with the exact error and HTTP 422, and the configuration already running is untouched.
2. A complete, minimal configuration
include = [ "/opt/apps/*/conf/site.toml" ]
[frontend]
httpPort = 80
httpsPort = 443
accessLogDir = "/opt/webobjects/log/access"
[admin]
password = "…"
[wotaskd]
host = "server.example.com"
port = 1085
password = "…"
[acme]
email = "you@example.com"
storage = "/opt/webobjects/acme"
[[sites]]
hostnames = [ "server.example.com" ]
app = "Modulo"
[[sites]]
hostnames = [ "www.example.com", "example.com" ]
app = "MyApp"
That is a server whose admin pages are at https://server.example.com/, with one application served at www.example.com, example.com redirecting to it, certificates from Let's Encrypt for both, and more sites picked up from files next to the applications. The sections below go through it table by table.
3. [frontend]
How modulo faces the internet. Restart to change.
| Field | Default | Meaning |
|---|---|---|
| httpPort | 80 | The plain-HTTP port. It redirects to HTTPS and answers Let's Encrypt's challenges, so for certificates to work it has to be reachable from the internet on port 80. |
| httpsPort | 443 | The TLS port, speaking HTTP/2 and HTTP/1.1. Certificates are chosen per hostname from the sites. |
| http3 | false | Also listen for HTTP/3 on the same port number over UDP. Leave it off: it works, but Jetty's HTTP/3 has an open bug that makes it unreliable with many sites, and it needs one certificate covering every hostname on the server. |
| accessLogDir | none | A directory for access logs: one subdirectory per site, one file per day. Without it, nothing is logged. Logs are kept indefinitely; deleting them is your decision. |
| acmeWebroot | none | Only for moving from certbot: a directory whose .well-known/acme-challenge/ files modulo serves on port 80, so certbot can keep renewing while its sites move over. Not needed once modulo manages the certificates itself. |
Without a [frontend] table the defaults apply. If the file has an error, modulo logs it and starts without the front end, so ports 80 and 443 stay closed until the file is fixed and modulo restarted. It still needs [wotaskd] to start at all.
4. [admin]
[admin]
password = "…"
The password for modulo's admin pages and its /reload endpoint, using HTTP Basic authentication with any username. When a password is set it is always required. When none is set, the admin pages are open in development and switched off in production, so on a server set one. Restart to change.
The admin pages are an application like any other as far as the sites are concerned: modulo itself answers as the application Modulo, so a site with app = "Modulo" serves them. They show the sites and applications modulo knows about, recent events, request statistics and, under /config, every setting modulo is running with, where it came from, and whether it can be configured yet.
5. [wotaskd]
Where modulo learns which applications exist and where their instances listen. modulo asks wotaskd every ten seconds, and sooner when an instance stops answering. Restart to change.
| Field | Meaning |
|---|---|
| host | The host wotaskd runs on, as wotaskd knows itself. If wotaskd was started with -WOHost, it only listens on that address, so use the same name here rather than localhost. |
| port | wotaskd's port, normally 1085. |
| password | The password wotaskd's configuration is protected with. wotaskd expects it in the hashed form stored in SiteConfig.xml, not the password you type into JavaMonitor. |
All three are required. The instance timeouts you set in JavaMonitor come along with the configuration: an instance's receive timeout becomes the time modulo waits for that instance's response.
6. [acme]
Certificates from Let's Encrypt, obtained and renewed by modulo. Required as soon as any site uses automatic certificates, which is the default. Changes take effect on reload.
| Field | Meaning |
|---|---|
| Required. The account's contact address; Let's Encrypt writes here about problems with your certificates. | |
| storage | Required. A directory modulo owns, holding the account key and every certificate. See what modulo keeps on disk. |
| directory | Optional. letsencrypt (the default), letsencrypt-staging for experimenting without hitting Let's Encrypt's rate limits, or the address of another ACME directory. |
A new site starts with a self-signed placeholder certificate, so its port works immediately, and the real certificate arrives seconds later once Let's Encrypt has checked the domain. A visitor who arrives in those seconds sees a certificate warning. Renewal is checked every twelve hours; a certificate is renewed thirty days before it expires, or at once if a hostname was added to the site.
7. include: sites in their own files
include = [ "/opt/apps/*/conf/site.toml" ]
A list of files, or glob patterns, whose [[sites]] are added to the main file's. Relative paths are resolved against the main file's directory. A pattern without wildcards must name an existing file; a wildcard pattern that matches nothing is only a warning, so a pattern can be in place before the first application arrives.
This keeps each application's site next to the application, where whoever deploys it can find it:
# /opt/apps/myapp/conf/site.toml
[[sites]]
hostnames = [ "www.example.com", "example.com" ]
app = "MyApp"
An included file may only contain sites. include, [acme] and the startup tables belong to the main file, and the strict parser rejects them anywhere else.
8. [[sites]]
A site is a set of hostnames and what to do with requests for them. Each [[sites]] header adds one.
| Field | Default | Meaning |
|---|---|---|
| hostnames | required | Every hostname the site answers to. The first is the canonical hostname, the rest are aliases. A hostname can belong to one site only. |
| app | none | The application to send requests to, by the name wotaskd and JavaMonitor know it by. A site without one still terminates TLS and redirects, but has nothing to proxy to, which modulo warns about at startup. |
| canonicalRedirect | true | Redirect requests for an alias to the canonical hostname, with a 301 that keeps the path and query. |
| httpsRedirect | true | Redirect plain HTTP to HTTPS with a 301. |
| tls | automatic | Where the certificate comes from. See below. |
| rewrites | none | URL rewrite and redirect rules, tried in order. See rewrite rules. |
| woa | none | Path to the application's .woa on this server. modulo then serves /WebObjects/<App>.woa/… from the bundle's WebServerResources directories, the job Apache's document root used to do for classic applications. Never anything outside those directories. Applications that serve their own resources don't need it. |
Canonical hostnames and aliases
With the defaults, the order of hostnames is the whole story: the first one is where visitors end up, and every other one redirects there. To make the apex the canonical name instead of www, put it first.
To serve every listed hostname directly, without redirects, set canonicalRedirect = false. The request then reaches the application with the hostname the visitor used, which is what you want when the application decides for itself what each domain shows, for example a multi-tenant application keyed on the hostname.
[[sites]]
hostnames = [ "shop.example.com", "shop.example.net", "brand-a.example.org" ]
app = "Shop"
canonicalRedirect = false
A rough edge: today the first hostname also names the site's certificate directory and access log directory. Reordering the list, to change which domain is canonical, therefore moves both: a new certificate is ordered under the new name and the access log continues in a new directory. Separating a site's name from its canonical hostname is modulo issue #13.
Certificates
Leave tls out and modulo obtains the site's certificate from Let's Encrypt, covering all of its hostnames. That needs the [acme] table, and DNS for every hostname pointing at the server, since Let's Encrypt checks each name over port 80.
For a certificate you manage yourself, give the files:
tls = { mode = "manual", cert = "/etc/ssl/example/fullchain.pem", key = "/etc/ssl/example/privkey.pem" }
The certificate file holds the full chain in PEM; the key is unencrypted PEM. modulo checks the files every five minutes and loads a replaced certificate without a restart, so an external renewal job only has to write the files.
9. Rewrite rules
For friendly URLs that an application's own routing doesn't handle, and for redirects. The rules replace Apache's RewriteRule:
[[sites]]
hostnames = [ "www.example.com" ]
app = "MyApp"
rewrites = [
{ match = '^/$', to = '/Apps/WebObjects/MyApp.woa/wa/default' },
{ match = '^/things/([^/]+)$', to = '/Apps/WebObjects/MyApp.woa/wa/thing?id=$1', encodeCaptures = true },
{ match = '^/old-name$', to = '/new-name', redirect = "permanent" },
{ match = '^/elsewhere$', to = 'https://www.example.org/', redirect = "temporary" },
]
| Field | Default | Meaning |
|---|---|---|
| match | required | A Java regular expression, matched against the request path. It isn't anchored, so write ^ and $ yourself. |
| to | required | The new URL, with $1 to $9 for the parts in parentheses and $$ for a literal dollar sign. A path for a rewrite; a path or a full URL for a redirect. |
| redirect | none | Without it the request goes on to the application under the new path, and the visitor never sees it. "temporary" answers 302 and "permanent" 301 instead. |
| appendQuery | false | When to has a query of its own, add the request's query after it. Without it the target's query replaces the original. A target without a query always keeps the original. |
| encodeCaptures | false | Encode each captured part as a query value, for captures that end up after the ?. A literal & or + in the path then arrives as part of the value rather than splitting it. |
Three things to know about how rules are applied:
- The first matching rule wins, like Apache with
[L]on every rule. - URLs already in the adaptor's space are never rewritten. Anything starting with
/Apps/WebObjects/goes straight to the application, so the links an application generates pass through, and a catch-all rule can't loop. - Rules see the path as the browser sent it, percent-encoded. To match
æ, write%C3%A6. This is the opposite of Apache, which decoded first, and it means a captured path segment can never smuggle a?or/into the target.
Applications using wonder-slim's routes, or ng-objects, usually need no rewrite rules at all: they answer / and their own clean URLs directly.
10. What modulo keeps on disk
Two directories are modulo's own. You never have to edit either, but you can read both.
<acme storage>/account-key.pem the Let's Encrypt account key
<acme storage>/sites/<host>/cert.pem a site's certificate, full chain, PEM
<acme storage>/sites/<host>/key.pem its private key, PEM
<accessLogDir>/<host>/2026_09_24.log a site's access log for one day
<accessLogDir>/_unmatched/… requests for hostnames no site claims
<host> is the site's first hostname. Access log lines are the combined log format with the hostname the visitor actually used in front and the response time at the end, so one site's aliases can be told apart:
example.com 203.0.113.9 - - [24/Sep/2026:08:12:44 +0000] "GET /about HTTP/2" 301 0 "-" "Mozilla/5.0 …" 1ms
Deleting a site's directory under sites/ makes modulo order a fresh certificate at the next check or restart, which is the way out of a certificate that is somehow wrong.
11. Command-line overrides
A few settings are given as -D properties when modulo starts, for example in its systemd unit. Most are for testing and unusual installations.
| Property | Default | Meaning |
|---|---|---|
| modulo.config-file | /opt/webobjects/modulo.toml | Read the configuration from somewhere else. |
| modulo.proxy-port | 1400 | The plain reverse-proxy port, which always runs alongside the front end. Useful for putting modulo behind another web server. |
| modulo.adaptor-url | /Apps/WebObjects/ | The URL prefix of the adaptor space. Change it only if your applications generate URLs under a different prefix. |
| modulo.wotaskd.host, .port, .password | from the file | Override the [wotaskd] table. |
The admin pages listen on port 45678, given as -WOPort 45678 in the unit. That port is internal: reach the pages through a site with app = "Modulo".
12. What isn't configurable yet
Timeouts towards the applications (apart from the per-instance ones from JavaMonitor), thread pools, compression, how instances are chosen, certificate renewal timing and a handful of others are built in. modulo's /config admin page lists every one of them with its current value, so you can see exactly what a server runs with. They will move into the file as the need arises.