Zammad installs from one apt repository, but the package refuses to install until Elasticsearch is already there. The database it needs is PostgreSQL, not the MariaDB that older guides set up. Zammad 7.0 removed MySQL and MariaDB support, so a guide that starts with mysql_secure_installation now leads to an install that cannot run.
This guide installs Zammad helpdesk on Ubuntu 26.04 and 24.04 in the order the package expects: Elasticsearch 9 first, then Zammad with PostgreSQL and Redis, then nginx with HTTPS on your own domain. It finishes with the setup wizard, a real ticket and a search that proves the index works, plus the settings and backups you want before agents log in. We ran it in October 2026 on Ubuntu 26.04 and 24.04 with Zammad 7.2.2 and Elasticsearch 9.5.5, so you get the 7.2.2 security release that fixed a local privilege escalation in the packages.
What the server needs
Zammad runs four services of its own next to PostgreSQL, Redis and Elasticsearch, all on one machine. Elasticsearch is the heavy part, which is why the memory figure below is higher than the bare Zammad minimum:
- Ubuntu 26.04 LTS or 24.04 LTS, amd64 (Zammad publishes no arm64 builds for Ubuntu)
- 4 CPU cores and at least 8 GB RAM, with the Elasticsearch heap capped as shown below (Zammad’s docs ask for 10 GB with no cap)
- About 3 GB of disk for the packages, plus room for attachments and backups
- A domain name with an A record pointing at the server, for example
helpdesk.example.com - Ports 80 and 443 reachable from the internet for the Let’s Encrypt certificate
Zammad’s docs ask for 2 cores and 6 GB, plus 4 GB more when Elasticsearch runs on the same host, so 10 GB in total. The reason is Elasticsearch’s default heap: it sizes itself to half the RAM, which on our 8 GB VM meant a 3,968 MB heap, a 4.6 GB process and about 6 GB in use on the whole server with Zammad running and nobody logged in. Capping the heap at 2 GB, as the Elasticsearch section shows, brought the server down to about 4 GB in use and search kept working. Go with 8 GB and the cap for a small team, or 10 GB and up without it; the docs’ larger example, for up to 40 agents, uses 6 cores and 12 GB.
Set the domain and admin email
Two values repeat through the nginx, certificate and Zammad steps. Export them once at the top of your SSH session, using your real domain and an address that should receive certificate expiry notices:
export ZAMMAD_DOMAIN="helpdesk.example.com"
export ADMIN_EMAIL="[email protected]"
If you reconnect later, run the two export lines again, because every command below reads them. Zammad also needs a UTF-8 locale. Check what the server uses:
locale | grep "LANG="
Both our Ubuntu servers reported LANG=C.UTF-8, which works. If yours shows a non-UTF-8 locale, install the locales package, run sudo locale-gen en_US.UTF-8 and sudo update-locale LANG=en_US.UTF-8, then log out and back in before you continue.
Install Elasticsearch 9
The Zammad package lists Elasticsearch as a hard dependency, so apt install zammad fails unless Elastic’s repository is already set up. Zammad 7.2 needs Elasticsearch 8.15 or newer and below 10, and the 9.x repository fits that range. Install the tools for adding repositories:
sudo apt update
sudo apt install -y curl apt-transport-https gnupg
Add Elastic’s signing key and the 9.x repository:
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor | sudo tee /usr/share/keyrings/elasticsearch-keyring.gpg > /dev/null && sudo chmod 644 /usr/share/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/9.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-9.x.list
Install the package. It bundles its own Java runtime, needs 1.5 GB of disk, and took 77 seconds on our 26.04 VM:
sudo apt update
sudo apt install -y elasticsearch
The installer prints a password for the elastic superuser. You do not need to copy it, because the connection step below generates a fresh one. Before you start the service, make two changes to /etc/elasticsearch/elasticsearch.yml. Zammad recommends a 400 MB request limit so large attachments index, and the package’s auto-configuration binds the HTTP port to 0.0.0.0, which on our 26.04 VM meant port 9200 listening on every interface. Zammad talks to it over localhost, so bind it there:
echo "http.max_content_length: 400mb" | sudo tee -a /etc/elasticsearch/elasticsearch.yml
sudo sed -i 's/^http.host: 0.0.0.0/http.host: 127.0.0.1/' /etc/elasticsearch/elasticsearch.yml
Cap the Java heap too, unless the server has 10 GB or more. Without a cap Elasticsearch takes half the RAM for itself, and a 2 GB heap is plenty for the index of a small helpdesk:
printf -- '-Xms2g\n-Xmx2g\n' | sudo tee /etc/elasticsearch/jvm.options.d/heap.options
Enable and start Elasticsearch:
sudo systemctl enable elasticsearch.service --now
Confirm it now listens on loopback only:
sudo ss -tlnp | grep 9200
The address column should read 127.0.0.1, not *:
LISTEN 0 4096 [::ffff:127.0.0.1]:9200 *:* users:(("java",pid=38514,fd=886))
The [::ffff:127.0.0.1] form is Java listening on the IPv4 loopback through an IPv6 socket, so nothing outside the server can reach port 9200.
Install Zammad
Zammad’s packages moved to go.packager.io in 2026, and the change trips up anyone following an older guide:
- The old
dl.packager.iorepository stopped at 7.0.2 - It has no 26.04 suite at all, and returned 404 when we tried
- Old keys at
/etc/apt/keyrings/pkgr-zammad.gpgbelong to that repository and can go
Add the signing key and the source file for your Ubuntu release from the new host:
sudo curl -fsSL "https://go.packager.io/srv/deb/zammad/zammad/gpg-key.gpg" -o /usr/share/keyrings/zammad.gpg && sudo chmod 644 /usr/share/keyrings/zammad.gpg
sudo curl -fsSL "https://go.packager.io/srv/zammad/zammad/stable/installer/ubuntu/$(lsb_release -rs).list" -o /etc/apt/sources.list.d/zammad.list
The lsb_release -rs part fills in 26.04 or 24.04, which is how Zammad names its suites. Install the package:
sudo apt update
sudo apt install -y zammad
The install takes about four minutes (250 seconds on 26.04, 230 on 24.04), because it pulls in PostgreSQL, Redis and nginx, creates the database and runs every migration. Ubuntu’s own packages supply the services, so the versions differ between the two releases:
| Installed by apt | Ubuntu 26.04 | Ubuntu 24.04 |
|---|---|---|
| Zammad | 7.2.2 (ubuntu26 build) | 7.2.2 (ubuntu24 build) |
| PostgreSQL | 18 | 16 |
| Redis | 8.0.5 | 7.0.15 |
| nginx | 1.28.3 | 1.24.0 |
| New packages | 89, 1028 MB | 74, 1115 MB |
Both meet Zammad’s floor of PostgreSQL 15 and Redis 7, and the PostgreSQL 18 guide covers tuning that database if your helpdesk grows. Check that the four Zammad services came up:
systemctl is-active zammad zammad-web zammad-worker zammad-websocket
All four should print active. zammad is the parent unit, and restarting it restarts the other three. The web server listens on 127.0.0.1:3000 and the websocket server on 127.0.0.1:6042, so nothing but nginx is reachable from outside. One habit to pick up now: every zammad run command needs sudo, or it stops with Error: You must be executing as the user 'zammad' or with root privileges.
Connect Zammad to Elasticsearch
The package does not touch Elasticsearch, so search stays off until you give Zammad the URL, a user and the cluster’s CA certificate. Generate a new password for the elastic user and keep it in a shell variable, so it never lands in your shell history (it is briefly visible in the process list while the next command runs, so do this on a server you do not share):
ES_PASS=$(sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -b -s)
Store the URL, user and password in Zammad’s settings. The password goes in as an argument rather than inside the Ruby string, so a quote or $ in it can’t break the command:
sudo zammad run rails r "Setting.set('es_url', 'https://localhost:9200')"
sudo zammad run rails r "Setting.set('es_user', 'elastic')"
sudo zammad run rails r "Setting.set('es_password', ARGV[0])" "$ES_PASS"
Elasticsearch 9 serves HTTPS with its own CA, so import that CA into Zammad, then build the search index:
sudo cat /etc/elasticsearch/certs/http_ca.crt | sudo zammad run rails r 'SSLCertificate.create!(certificate: STDIN.read)'
sudo zammad run rake zammad:searchindex:rebuild
On a fresh install the rebuild takes about 15 seconds and ends with a done line per object type. A quick check that Zammad now uses the index:
sudo zammad run rails r "puts SearchIndexBackend.enabled?"
It prints true. The URL says localhost, which resolves to ::1 first on Ubuntu while Elasticsearch now listens on 127.0.0.1 only. That worked on both our servers because the client falls back to IPv4, and you can write https://127.0.0.1:9200 instead if you prefer it explicit.
Point nginx at your domain
The package drops a plain HTTP config into /etc/nginx/sites-available/zammad.conf with server_name localhost, and leaves Ubuntu’s default site enabled. Until you change that, opening the server by IP shows the “Welcome to nginx!” page instead of Zammad, which is what our first test returned. Remove the default site and set your domain:
sudo rm /etc/nginx/sites-enabled/default
sudo sed -i "s/server_name localhost;/server_name ${ZAMMAD_DOMAIN};/" /etc/nginx/sites-available/zammad.conf
sudo nginx -t && sudo systemctl reload nginx
From this point the setup wizard is reachable on your domain, and whoever opens it first creates the admin account. Keep the next steps moving and create the admin as soon as HTTPS is up, because the certificate request publishes your domain in Certificate Transparency logs that scanners watch.
Open the firewall for SSH, HTTP and HTTPS. UFW was inactive on both our servers, so the enable line matters, and allowing OpenSSH first keeps your session alive when it turns on. OpenSSH means port 22, so if your SSH daemon listens elsewhere, allow that port instead or the enable line locks you out:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Nothing else needs a port. PostgreSQL, Redis, Elasticsearch and the two Zammad listeners are all on loopback, and the UFW guide for Ubuntu 26.04 covers rate limiting SSH if you want it.
Get a Let’s Encrypt certificate
Zammad’s TLS config expects Diffie-Hellman parameters. Generate them first, since they do not depend on the certificate; 4096-bit ones took 3 minutes 3 seconds on our 4-core VM:
sudo openssl dhparam -out /etc/ssl/dhparam.pem 4096
Zammad’s docs say HTTPS is required in production, and its session cookies only work properly once Zammad knows it runs on HTTPS. With the A record in place and port 80 open, install Certbot with the nginx plugin:
sudo apt install -y certbot python3-certbot-nginx
Request the certificate with certonly, so Certbot proves domain ownership through nginx but does not rewrite Zammad’s config. The next step swaps in Zammad’s own SSL config, which already has the right proxy headers for the websocket:
sudo certbot certonly --nginx -d "${ZAMMAD_DOMAIN}" -m "${ADMIN_EMAIL}" --agree-tos --non-interactive
Certbot saves the files under /etc/letsencrypt/live/ followed by your domain, and installs a timer that renews them. sudo certbot certificates lists the certificate and its expiry date. Our lab VMs had no public DNS, so this one command is the step we could not run; we tested the rest of the HTTPS setup with a certificate placed at the same paths. If the request fails, check that the A record resolves to this server and that port 80 is open at your cloud provider as well as in UFW.
Switch nginx to HTTPS
Zammad ships a TLS config for nginx in /opt/zammad/contrib/nginx/zammad_ssl.conf. Copy it over the HTTP one, then point it at your domain and the Let’s Encrypt files:
sudo cp /opt/zammad/contrib/nginx/zammad_ssl.conf /etc/nginx/sites-available/zammad.conf
sudo sed -i -e "s/server_name example.com;/server_name ${ZAMMAD_DOMAIN};/" \
-e "s#/etc/ssl/certs/example.com.pem#/etc/letsencrypt/live/${ZAMMAD_DOMAIN}/fullchain.pem#" \
-e "s#/etc/ssl/private/example.com.key#/etc/letsencrypt/live/${ZAMMAD_DOMAIN}/privkey.pem#" \
-e "s#/etc/ssl/certs/root-ca-plus-intermediates.pem#/etc/letsencrypt/live/${ZAMMAD_DOMAIN}/chain.pem#" \
/etc/nginx/sites-available/zammad.conf
Two things in the shipped file need fixing before you rely on it. The port-80 block puts return 301 at server level, which nginx runs before it looks at any location, so the /.well-known/ location meant for certificate challenges is never used. We confirmed it: a test file under /.well-known/acme-challenge/ came back as a 301 to HTTPS. Certbot’s nginx plugin works around that on renewal, but webroot-based clients such as acme.sh depend on the location. The file also turns on OCSP stapling, but Let’s Encrypt removed OCSP URLs from its certificates in May 2025, so nginx only warns "ssl_stapling" ignored, no OCSP responder URL in the certificate. Move the redirect into a location and turn stapling off:
sudo sed -i 's|^ return 301 https://$server_name$request_uri;| location / {\n return 301 https://$server_name$request_uri;\n }|' /etc/nginx/sites-available/zammad.conf
sudo sed -i -e 's/ssl_stapling on;/ssl_stapling off;/' -e 's/ssl_stapling_verify on;/ssl_stapling_verify off;/' /etc/nginx/sites-available/zammad.conf
On Ubuntu 26.04 only, also switch to the newer HTTP/2 syntax. nginx 1.28 warns that listen ... http2 is deprecated, while nginx 1.24 on Ubuntu 24.04 rejects the replacement with unknown directive "http2", so skip this command on 24.04:
sudo sed -i -e 's/listen 443 ssl http2;/listen 443 ssl;/' -e 's/listen \[::\]:443 ssl http2;/listen [::]:443 ssl;\n http2 on;/' /etc/nginx/sites-available/zammad.conf
Test the config and reload nginx:
sudo nginx -t && sudo systemctl reload nginx
With all the edits in place, nginx -t printed only the two “syntax is ok” and “test is successful” lines on both releases. Check both ports from the server:
curl -sI "http://${ZAMMAD_DOMAIN}/" | grep -iE "^HTTP|^location"
curl -sI "https://${ZAMMAD_DOMAIN}/" | grep -iE "^HTTP|strict-transport"
Plain HTTP answers with a redirect, and HTTPS with HTTP/2 and an HSTS header carrying a two-year max-age (our lab used a self-signed certificate, so we added -k to the second command; with the Let’s Encrypt certificate you do not need it):
HTTP/1.1 301 Moved Permanently
Location: https://helpdesk.example.com/
HTTP/2 200
strict-transport-security: max-age=63072000
Then run sudo certbot renew --dry-run once to confirm renewals work through the edited config, because a renewal that fails silently only shows up 90 days later.
Tell Zammad its URL
Zammad builds links, email footers and its websocket origin check from two settings. If http_type says http while users arrive over HTTPS, or the other way round, logins fail with CSRF token verification failed!, so set both now:
sudo zammad run rails r "Setting.set('fqdn', ARGV[0]); Setting.set('http_type', 'https')" "${ZAMMAD_DOMAIN}"
If the CSRF error shows up later anyway, the usual cause is another proxy or load balancer in front of nginx that doesn’t pass the HTTPS scheme through. Our CSRF login fix for Zammad walks through the header change for that case.
Here is how the pieces fit together now. Only nginx is reachable from the network, and everything behind it listens on localhost:

The worker never takes requests from nginx. It processes background jobs, fetches mail once you add a mailbox, and keeps the Elasticsearch index up to date as tickets change.
Finish the setup wizard
Open https://helpdesk.example.com (your domain) in a browser. A fresh install offers two choices, and you want the new system:

The first step creates the administrator. The password needs at least 10 characters with two upper-case letters, two lower-case letters and a digit, and this account owns every setting, so use a password manager:

The following steps ask for your organization name, logo and email settings, and they only run in the browser session that created the admin. We closed the browser after the admin step, and when we signed in again Zammad went straight to the dashboard and skipped the rest of the wizard. If that happens to you, set the organization name under the gear icon at the bottom left, in Settings > Branding, where we found the field still empty:

Email is optional for a first test. Agents can log phone calls and create tickets in the web UI without it, and you can connect a mailbox later under the gear icon in Channels > Email, which is where Zammad starts turning incoming mail into tickets.
Create a ticket and test search
A ticket proves the database and the web workers, and searching for it proves Elasticsearch. Click the plus icon at the bottom left, pick Received Call, and fill in a title, a customer and the text. Zammad seeds a sample customer, Nicole Braun, so you can test before you add your own. After Create, the ticket opens with its number:

Now search for a word that only appears in the ticket body. We typed duplex into the search box, and the ticket came back under the Ticket tab:

If the search returns nothing for a ticket you just created, check systemctl status elasticsearch first, then run the index rebuild from the Elasticsearch section again.
Turn off public sign-up
A new Zammad lets anyone register a customer account from the login page. The login screen showed a Register as a new customer link, and the gear icon’s Settings > Security > Base page had New User Accounts set to yes:

That suits a public support desk. For an internal IT helpdesk, where you create users yourself or sync them from LDAP, set it to no in that screen and press Submit. The same page holds the session timeout, which defaulted to four weeks, and the Two-factor Authentication tab next to it is worth enabling for agents and admins.
Back up Zammad
Zammad ships backup and restore scripts in /opt/zammad/contrib/backup/. The script reads a config file that doesn’t exist yet, so create it from the template:
sudo cp /opt/zammad/contrib/backup/config.dist /opt/zammad/contrib/backup/config
The defaults write to /var/tmp/zammad_backup and keep 10 days. Change BACKUP_DIR in that file if you want the dumps on another disk, then run a backup:
sudo /opt/zammad/contrib/backup/zammad_backup.sh
On our fresh system it finished in about one second and wrote a 6.4 MB database dump plus a tarball of stored files, with latest_ symlinks pointing at the newest pair:
-rw------- 1 root root 6.4M Oct 17 18:20 20261017182023_zammad_db.psql.gz
-rw------- 1 root root 125 Oct 17 18:20 20261017182023_zammad_files.tar.gz
lrwxrwxrwx 1 root root 55 Oct 17 18:20 latest_zammad_db.psql.gz -> /var/tmp/zammad_backup/20261017182023_zammad_db.psql.gz
lrwxrwxrwx 1 root root 57 Oct 17 18:20 latest_zammad_files.tar.gz -> /var/tmp/zammad_backup/20261017182023_zammad_files.tar.gz
Ignore the warning about having no attachments in the file system on a new install, because the script creates the empty directory and carries on. Schedule it nightly with sudo crontab -e and a line such as 0 2 * * * /opt/zammad/contrib/backup/zammad_backup.sh, and copy the dumps off the server, since a backup on the same disk does not survive losing the disk. Two limits from the docs: a backup cannot be restored into an older Zammad version, and it doesn’t include environment variables you set with zammad config:set.
Upgrade Zammad without surprises
Zammad only ships security fixes for the current stable release, so falling behind is the real risk. Two 7.2 point releases arrived in the two weeks before we tested, 7.2.1 for a critical MFA bypass and 7.2.2 for a high-severity privilege escalation. The upgrade routine from Zammad’s docs stops the services, holds Zammad back while everything else updates, then upgrades it on its own:
- Stop Zammad with
sudo systemctl stop zammadand run a backup - Run
sudo apt updateandsudo apt-mark hold zammad - Run
sudo apt upgradefor the rest of the system - Run
sudo apt-mark unhold zammadandsudo apt upgradeagain - Rebuild the search index if the release notes ask for it, as they do for 7.2
The package runs the database migrations itself, but it never overwrites your nginx config, so compare /opt/zammad/contrib/nginx/zammad_ssl.conf with your copy after each upgrade and merge proxy-header changes by hand. Upgrade one major version at a time, which matters if you’re moving an old 6.x install that also needs its MariaDB data migrated to PostgreSQL first. Zammad also packages the same release for Rocky Linux and AlmaLinux and for Debian; on any distro, use the go.packager.io repository rather than the old host. If you want a PHP-based helpdesk that needs no Elasticsearch, osTicket is the usual alternative.
Thank you! I tried several different installation guides, and this was the only one that worked. There was a step I had to do to install the public key, but that was an easy google of the error fix.
We’re happy to learn this worked for you.
Muchas gracias!! de varias guias, me paso igual que al Knuson, esta fue la unica que me funciรณn!!
Gracias y bienvenido siempre!