Fess 15 on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Fess 15 Enterprise Search on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images.
Fess is an open-source enterprise search server from the CodeLibs project, published under the Apache License 2.0. It crawls content, extracts the text of every document with Apache Tika, indexes it into OpenSearch and serves the results through a search interface and a JSON API — with role-based filtering so users only ever see the documents they are allowed to see.
This image bundles the complete appliance on a single virtual machine:
- Fess 15.7.0 — the search server, crawler and administration UI on TCP 8080
- OpenSearch 3.7.0 — the search engine that stores the index, bound to loopback only
- The four CodeLibs engine plugins Fess requires —
analysis-fess,analysis-extension,configsyncandminhash
Fess crawls three kinds of source out of the box:
- Web — any HTTP site, with depth, thread and politeness controls
- File system — local directories, mounted NFS or SMB shares
- Data store — CSV files and JDBC databases
Over twenty languages are supported for tokenisation and stemming, and Apache Tika handles the file formats you would expect from a document repository: PDF, Microsoft Office, OpenDocument, HTML, plain text, and archives.
The appliance is self-proving. It ships with a small sample corpus and a self-test script that runs a real crawl, waits for it to finish, and then asserts the documents are genuinely searchable — the right number of hits for a term that should match, and zero for one that should not — through both the JSON API and the web UI. One command tells you the whole pipeline works on your own machine.
What is included:
-
Fess 15.7.0 (Apache-2.0), installed from the official CodeLibs
.deband SHA-256 verified against the digest published for the release asset -
OpenSearch 3.7.0 (Apache-2.0), the minimal distribution — the core engine with none of the bundled plugin suite, so no machine-learning, k-NN or performance-analyzer components are consuming memory on your VM
-
A per-VM administrator password and a per-VM admin API token, both generated on first boot
-
Two ready-made File Crawler configurations: Local Documents pointing at
/var/lib/fess/documents, and cloudimg Self-Test Corpus used by the shipped self-test -
A
fess-selftest.shcommand that proves crawl, index and search end to end
Security model
The appliance is built so that the only thing exposed to your network is the Fess web interface.
-
There is no default password. Fess normally ships an
admin/adminaccount. This image never creates it: the administrator password is generated on your VM at first boot and written into Fess's configuration before the search server is allowed to start for the first time, so the published default credential does not exist at any point in the machine's life. -
The search engine is not reachable from your network. OpenSearch holds every document you index and runs with no authentication plugin, so it is bound to
127.0.0.1and additionally fenced at the systemd level withIPAddressDeny=any/IPAddressAllow=localhost. Even a mistaken edit toopensearch.ymlcannot expose it. -
The public listener refuses to start with a stale secret. A guard runs before
fess.serviceevery time it starts and compares every live secret against a list of known upstream defaults extracted from the shipped package. If any of them still matches — or if the stored password hash for theadminaccount would accept a default — port 8080 stays closed. -
No swap. The three JVM heaps are sized to fit the VM rather than spilling to disk.
Prerequisites
- An Azure subscription with permission to create virtual machines
- The Azure CLI installed locally, or access to the Azure Portal
- An SSH key pair
- A network security group that allows inbound TCP 22 (SSH) and TCP 8080 (Fess) from your own address ranges
Sizing. The image is built and tested on Standard_B2s (2 vCPU, 4 GiB) and works there, but a search appliance benefits from memory for the OpenSearch page cache. For anything beyond evaluation, deploy on Standard_D2s_v3 (2 vCPU, 8 GiB) or larger and raise the heaps as described in Tuning memory below.
Step 1: Deploy from the Azure Portal
- In the Azure Portal, search the Marketplace for Fess 15 Enterprise Search on Ubuntu 24.04 and select Create.
- Choose your subscription, resource group and region.
- Set the VM size to
Standard_D2s_v3or larger. - Choose SSH public key authentication and set the username to
azureuser. - Under Inbound port rules, allow SSH (22) and add a custom rule for TCP 8080.
- On the Disks tab, choose Standard SSD or Premium SSD.
- Select Review + create, then Create.
Restrict both inbound rules to your own address ranges rather than leaving them open to the internet.
Step 2: Deploy from the Azure CLI
az group create --name fess-rg --location eastus
az vm create \
--resource-group fess-rg \
--name fess-vm \
--image cloudimg:fess-ubuntu-24-04:default:latest \
--size Standard_D2s_v3 \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port --resource-group fess-rg --name fess-vm --port 8080 --priority 1010
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
Step 4: Verify the Services
Two services make up the appliance: the search engine and the search server. Both should be active.
systemctl is-active opensearch fess
Expected output:
active
active
Confirm the versions actually installed, and which ports are bound:
dpkg-query -W -f='fess ${Version}\n' fess
curl -s http://127.0.0.1:9200 | jq -r '"opensearch " + .version.number'
ss -tln | grep -E ':(8080|9200|9300)'
Fess answers on 0.0.0.0:8080 — that is the interface you connect to. OpenSearch answers only on the loopback address.

Step 5: Confirm Only Fess Is Exposed
The search engine must not be reachable from your network. This check proves it, using the VM's own non-loopback address:
IP=$(hostname -I | awk '{print $1}')
curl -sS -m 5 "http://${IP}:9200/" 2>&1 | head -2
echo "--- and on loopback: ---"
curl -s http://127.0.0.1:9200/_cluster/health | jq -r '"cluster " + .status'
The first command must fail to connect. The second must report cluster green. If the first one ever succeeds, stop and investigate before indexing anything sensitive.
Step 6: Retrieve the Admin Credentials
Your per-VM administrator password and admin API token were generated on first boot and written to a root-only file:
sudo cat /stage/scripts/fess-credentials.log
The file contains FESS_ADMIN_USER, FESS_ADMIN_PASSWORD and FESS_API_TOKEN, along with the search and admin URLs for this machine. It is mode 0600, owned by root.
Store the password in your password manager, then change it from the admin UI (User → Users → admin).
Step 7: Run the Shipped Self-Test
This is the fastest way to confirm the whole pipeline works on your machine. It starts a real crawl, waits for it to complete, and then asserts the crawled documents come back from a search — through the API and the UI — with the right hit counts.
sudo /usr/local/sbin/fess-selftest.sh
Expected output (a single OK line):
OK fess zylotrimbek=3/3 brindlewick=1/1 qwixmarbleton=0/0 hits via api+ui, crawl completed, default admin/admin rejected, bogus api token rejected, opensearch loopback-only
The three counts are the point. zylotrimbek appears in exactly three of the four sample documents, brindlewick in exactly one, and qwixmarbleton in none — so the test proves search discriminates rather than proving the service merely responded. It also confirms the upstream admin/admin credential is refused and the search engine is loopback-only.

Anything other than a line starting OK is a failure, and the suffix names the failing condition.
Step 8: Search from the JSON API
Fess exposes a JSON search API at /api/v2/search. It needs no credentials for public documents, which is what makes Fess easy to embed in another application.
curl -s --get --data-urlencode 'q=zylotrimbek' \
http://127.0.0.1:8080/api/v2/search | jq '.response.record_count'
Expected output:
3
Retrieve the matching documents themselves:
curl -s --get --data-urlencode 'q=zylotrimbek' --data 'num=10' \
http://127.0.0.1:8080/api/v2/search \
| jq -r '.response.data[] | "\(.title) <- \(.url)"'
And confirm a term that appears in no document returns nothing:
curl -s --get --data-urlencode 'q=qwixmarbleton' \
http://127.0.0.1:8080/api/v2/search | jq '.response.record_count'

The response envelope carries record_count, the paging fields, and a data array of documents with title, url, content_description and last_modified.
Step 9: First Login
Open http://<vm-ip>:8080/ in a browser. You get the search interface immediately — no login is required to search.

To administer the appliance, go to http://<vm-ip>:8080/admin/ and sign in as admin with the password from Step 6.

The dashboard shows the state of the search engine behind Fess — node count, index count, document count and total index size.

Step 10: Index Your Own Documents
The image ships a File Crawler configuration named Local Documents pointing at /var/lib/fess/documents. Drop files there and crawl.
sudo install -d -o fess -g fess -m 0755 /var/lib/fess/documents
printf 'Quarterly revenue rose in the Northern region.\nProject codename: albatross.\n' \
| sudo tee /var/lib/fess/documents/quarterly-notes.txt >/dev/null
sudo chown fess:fess /var/lib/fess/documents/quarterly-notes.txt
ls -l /var/lib/fess/documents/
Start the crawler using your admin API token:
TOKEN=$(sudo sed -n 's/^FESS_API_TOKEN=//p' /stage/scripts/fess-credentials.log | head -1)
curl -s -X PUT -H "Authorization: Bearer ${TOKEN}" \
http://127.0.0.1:8080/api/admin/scheduler/default_crawler/start | jq -r '.response.status'
A 0 means the job was accepted. Wait for it to finish, then search for a word from your document:
TOKEN=$(sudo sed -n 's/^FESS_API_TOKEN=//p' /stage/scripts/fess-credentials.log | head -1)
for i in $(seq 1 60); do
sleep 5
R=$(curl -s -H "Authorization: Bearer ${TOKEN}" \
http://127.0.0.1:8080/api/admin/scheduler/setting/default_crawler \
| jq -r '.response.setting.running')
[ "$R" = "false" ] && break
done
echo "crawler running: ${R}"
curl -s --get --data-urlencode 'q=albatross' \
http://127.0.0.1:8080/api/v2/search | jq '.response.record_count'
The count should be 1. Your document is now searchable from the web interface too.
The Default Crawler job also runs on a schedule (daily at midnight by default), which you can change under System → Scheduler.
Step 11: Crawl Configuration
Under Crawler in the admin menu you will find the three crawler types. The appliance ships with two File Crawler configurations already created.

List them from the API:
TOKEN=$(sudo sed -n 's/^FESS_API_TOKEN=//p' /stage/scripts/fess-credentials.log | head -1)
curl -s -H "Authorization: Bearer ${TOKEN}" \
http://127.0.0.1:8080/api/admin/fileconfig/settings \
| jq -r '.response.settings[] | "\(.name) -> \(.paths) [permissions: \(.permissions)]"'
The permissions field matters. Fess filters every search result by the searching user's roles. A crawl configuration with an empty permissions value will index documents that nobody can see — the crawl succeeds, the index fills, and every query returns zero hits. Both shipped configurations use {role}guest, which makes the documents visible to anonymous searchers. Keep that value unless you are deliberately building a restricted collection.
To crawl a web site instead, add a Web configuration under Crawler → Web with a start URL, a crawl depth and an included-URL pattern. To crawl a database, use Crawler → Data Store with a JDBC URL and a SELECT statement.
Once you no longer need the sample documents, delete the cloudimg Self-Test Corpus configuration and remove /var/lib/fess/selftest-documents.
Step 12: Server Components
| Component | Version | Path | Port |
|---|---|---|---|
| Fess | 15.7.0 | /usr/share/fess |
8080 |
| OpenSearch | 3.7.0 | /usr/share/opensearch |
9200 / 9300 (loopback only) |
| Fess configuration | — | /etc/fess/fess_config.properties |
— |
| Fess service environment | — | /etc/default/fess |
— |
| OpenSearch configuration | — | /etc/opensearch/opensearch.yml |
— |
| Index data | — | /var/lib/opensearch |
— |
| Documents to index | — | /var/lib/fess/documents |
— |
| Fess logs | — | /var/log/fess |
— |
| OpenSearch logs | — | /var/log/opensearch |
— |
| Credentials | — | /stage/scripts/fess-credentials.log |
— |
ls -l /etc/fess/ | head -12
ls -ld /var/lib/opensearch /var/lib/fess/documents /var/log/fess
Step 13: Tuning Memory
The appliance runs two Java processes continuously and forks a third while a crawl is in progress. All three heaps are capped so the stack fits a 4 GiB machine with headroom:
| Process | Heap | Where it is set |
|---|---|---|
| OpenSearch | 1 GiB | /etc/opensearch/jvm.options |
| Fess | 512 MiB | FESS_HEAP_SIZE in /etc/default/fess |
| Crawler (forked per crawl) | 384 MiB | jvm.crawler.options in /etc/fess/fess_config.properties |
Check what is actually in force:
grep -E '^-Xm[sx]' /etc/opensearch/jvm.options
grep '^FESS_HEAP_SIZE' /etc/default/fess
grep -o '\-Xmx[0-9]*m' /etc/fess/fess_config.properties | head -1
free -m
On a larger VM, raise them together — a common mistake is to give OpenSearch more heap without leaving room for the Fess and crawler JVMs. On Standard_D2s_v3 (8 GiB) a good starting point is OpenSearch 2 GiB, Fess 1 GiB, crawler 512 MiB. Never set the OpenSearch heap above half of physical memory: the engine relies on the operating system's file cache for the rest.
Apply changes with a restart:
echo "Edit the files above, then run:"
echo " sudo systemctl restart opensearch && sudo systemctl restart fess"
There is deliberately no swap on this image. If a crawl is running out of memory, increase the VM size rather than adding a swap file.
Step 14: Managing the Services
systemctl status opensearch --no-pager -n 0
systemctl status fess --no-pager -n 0
Order matters: fess.service requires opensearch.service, so start the engine first and stop it last. Systemd already encodes that dependency, so systemctl restart fess alone is safe.
Recent log lines:
journalctl -u fess --no-pager -n 15
tail -n 5 /var/log/opensearch/cloudimg-fess.log
Log growth is bounded out of the box: OpenSearch rolls its log at 32 MB keeping three generations, /var/log/fess is rotated daily with a seven-generation limit, and the systemd journal is capped at 256 MB. Crawling writes continuously, so leave those limits in place unless you have a reason to change them.
Step 15: Put TLS in Front (Production)
Fess serves plain HTTP on 8080. For production, terminate TLS upstream with Azure Application Gateway, Azure Front Door, or an nginx or Caddy reverse proxy on the same VM, and restrict 8080 to the proxy.
Step 16: Troubleshooting
Fess does not start and port 8080 is closed. The secret guard refuses to open the listener if a secret still matches an upstream default. Check why:
journalctl -u fess --no-pager -n 30 | grep -i 'secret-guard' || echo "no guard refusal recorded"
The crawl finishes but search returns nothing. Almost always the crawl configuration's permissions field is empty — see Step 11. Confirm documents did reach the index:
curl -s 'http://127.0.0.1:9200/_cat/indices/fess.*?h=index,docs.count'
A crawl is slow or the machine is under memory pressure. Reduce the crawler's thread count on the crawl configuration, or move to a larger VM size — see Step 13.
Check the crawl history. Every run is recorded:
TOKEN=$(sudo sed -n 's/^FESS_API_TOKEN=//p' /stage/scripts/fess-credentials.log | head -1)
curl -s -H "Authorization: Bearer ${TOKEN}" http://127.0.0.1:8080/api/admin/joblog/logs \
| jq -r '.response.logs[] | "\(.job_name): \(.job_status)"' | head -5
Step 17: Security Recommendations
- Restrict inbound 8080 to the networks that need to search, and 22 to your administrative range.
- Change the administrator password from the value generated at first boot, and store it in a password manager.
- Treat the admin API token as a secret. Rotate it under System → Access Token if it is ever exposed.
- Leave OpenSearch on loopback. Nothing in normal operation requires reaching it from off the machine.
- Put TLS in front of Fess before indexing anything confidential — Step 15.
- Use role and group permissions on crawl configurations to keep restricted collections away from anonymous searchers.
- Keep the machine patched. Unattended security upgrades are enabled by default.

Step 18: Support and Licensing
Fess is published by the CodeLibs project under the Apache License 2.0. OpenSearch is also Apache-2.0. Both are free to use commercially; you pay only for the Azure compute and storage you consume.
head -3 /usr/share/doc/fess/copyright 2>/dev/null || echo "Fess is licensed under the Apache License 2.0"
Upstream documentation lives at fess.codelibs.org. cloudimg provides 24/7 support for the image itself — deployment, configuration and the appliance components.
Deploy on Azure
Find Fess 15 Enterprise Search on Ubuntu 24.04 in the Azure Marketplace.
Need Help?
Contact cloudimg support at cloudimg.co.uk for assistance with deployment, tuning or crawl configuration.