Install the server
One command takes a fresh Linux host to a working Antelao server. It adds an apt repository,
installs the antelao-ohm package, and hands over to ohm install, which lays everything else.
The whole walk takes about fifteen minutes, most of it waiting for images to download. Everything here runs on the server, with sudo where it needs it.
Running a hypervisor or a cloud that takes a disk image? A ready-made image installs itself on its first boot instead, and joins this page at step 4.
Requirements
| Operating system | A Debian-family host with systemd: Debian 12 or 13, or Ubuntu 24.04. The package repository has a suite for each of those three and no other, and the installer refuses a host that is not Debian-family |
| Architecture | amd64 only. There is no arm build, and the installer refuses one |
| Privileges | sudo, for anything run on the server itself: the installer and every local ohm command refuse to run unelevated. A remote command needs no privileges at all |
| Hardware | 2 vCPU, 4 GB RAM and 40 GB of disk as a floor. Odoo instances grow with their filestores, so give the disk room |
| Docker | Nothing to do. Docker Engine is a dependency of the antelao-ohm package, so apt installs it |
| DNS | A name that resolves to the server's public address. A wildcard such as *.pelmo.example.com, set as the server's provisioning domain, names instances created without a name of their own |
| Network in | TCP 80 and 443 from the internet, if you want publicly trusted certificates for the instances |
| Network out | HTTPS to packages.antelao.io (the package), ghcr.io (the images) and the license server. A network that only lets traffic out through a proxy: see The outbound proxy, which also lists every address the server reaches |
| Credentials | None: the package and the images are public |
| License | Your *.lic file, if you have one. It is optional at install time and can be installed later in the console |
Windows and macOS are not supported as hosts.
The console (5322) and its browser editor (5323), the API (5312), the agent (5202), the GeoIP and Fail2Ban services (5411 and 5421) and, when you install it, Portainer (9000 and 9443) listen on the host directly; pgAdmin (8081) answers on the server itself only. Only the instances themselves are proxied. Keep these ports closed at your provider's firewall and reach them over SSH or a VPN, or allow your own addresses only. There is a ready-made ruleset below.
1. Prepare the host
Create the virtual machine from a stock Debian 13 image, add your SSH key, and open inbound TCP 22, 80 and 443 at your provider's firewall. Leave everything else closed.
At your DNS provider, add an A record for the server's name (pelmo.example.com ->
203.0.113.10) and an AAAA record if the provider assigned an IPv6 address.
Add the same pair for the wildcard *.pelmo.example.com while you are there, and make
pelmo.example.com the server's provisioning domain (provisioningDomain in the bootstrap document, or later under
Configuration > Network). It costs nothing now and buys you the fast path later: an instance
created without a name of its own is then named under that wildcard and needs no DNS change at all,
which is what turns standing up a throwaway test instance into a one-minute job. Instances that
matter still get names of their own. See
Host names and DNS.
Wait until the name resolves before installing:
nslookup pelmo.example.com
Let's Encrypt certificates for the instances need the name to resolve from the internet and port 80 to be open. The console's own certificate does not: it is self-signed until you replace it.
Then sign in and bring the image up to date:
ssh admin@pelmo.example.com
sudo apt-get update && sudo apt-get -y upgrade
Sign in with an account that can use sudo; the commands in this walk carry it where they need it, and only there. Reboot if the upgrade replaced the kernel.
If you have a license file, copy it over now. Without one the server runs at the unregistered tier, which is a supported state rather than a broken one:
scp customer.lic admin@pelmo.example.com:customer.lic
2. Decide anything you want decided up front (optional)
The install is unattended. Everything it is not told it installs with a sensible default, all of it is finishable in the console afterwards, and the only question it may ask is whether you accept the license agreement - and only when it runs in a terminal.
To decide something in advance - the server's name, a license file, the mail profile, the addresses
that may reach the management API - write a bootstrap document to /etc/antelao/bootstrap.json
before you run the installer. A short one:
sudo install -d -m 0755 /etc/antelao
sudo tee /etc/antelao/bootstrap.json > /dev/null <<'EOF'
{
"hostname": "pelmo.example.com",
"provisioningDomain": "pelmo.example.com",
"apiAllow": ["203.0.113.7"]
}
EOF
sudo chmod 0600 /etc/antelao/bootstrap.json
The bootstrap document lists every key, how the file is checked,
and how to give the admin account a password and a second factor of your own. Skip this step and
the install still completes; nothing here is required.
3. Run the installer
With or without that file, one command:
curl -fsSL https://packages.antelao.io/raw/antelao-ohm-installer.sh | sudo bash
To check the installer's signature before it runs, download it and its signature, verify, and run the file instead: see Verify a download.
It adds the Antelao signing key and your suite's apt repository, installs the antelao-ohm package,
and hands over to ohm install. Run from a terminal, it first shows the license agreement and asks
you to accept it, unless the bootstrap document already says so (see
The license agreement). Then it runs on its
own for several minutes:
- apt installs
antelao-ohmand, as dependencies, Docker Engine, the Docker client and ICU. The package lays the tool and the systemd units and does nothing else. - The script execs
ohm install, outside apt, so nothing holds the dpkg lock and a failure never wedges the package system. The recovery is alwaysohm installagain.- Host -
/etc/antelao/agent.envis written with fresh credentials, the hostname is set, the Docker daemon logs in to the registry, the license is placed. - Agent - the three systemd units are installed and the
ohm-agentcontainer starts. - Server - the Docker networks, PostgreSQL, the master database, pgAdmin, the
GeoIP and fail2ban services, nginx, and the management containers
ohm-apiandohm-web. This is the long part: the PostgreSQL image is built on the host. Portainer is not installed unless you choose it later (Portainer). - Management API - the installer waits until the API reports its configuration loaded.
- Administrator - the
adminaccount is created, with a generated password unless you gave credentials of your own.
- Host -
The hand-off:
── Installation complete ─────────────────────────────────────────────
Console https://pelmo.example.com:5322
(the certificate is self-signed until one is installed)
Sign in admin
Password f7v9270MjF4bmuhN!nM1 (shown once; change it under Account)
License customer.lic
Continue in the browser:
1. Sign in. The setup page opens with the hostname and the addresses this installer wrote;
review the mail profile, the intrusion prevention and the access rules, then Finish.
2. Create an instance (Instances > New), or restore one from a backup archive.
To repair this installation later, run ohm install again (the environment is offline meanwhile).
A generated password is shown once and stored nowhere. If you lose it, run
ohm user password-reset admin on the server. A password you supplied is never shown: the line names
where it came from instead.
The hand-off also prints the agent's operator key, which has full authority over the server.
Keys and credentials lists it with every other key the server uses. Both
it and a generated password are printed only when the installer writes to a terminal. When its
output goes to a file, a pipe or an automated job - a cloud-init log, a CI job - those lines say the
value was not shown and name the command that gets it on the server: sudo ohm server agentkey show
for the key, sudo ohm user password-reset admin for a password. Pass --show-secrets to
ohm install to print them anyway. Where an earlier install's output was kept, rotate the key with
sudo ohm server agentkey rotate.
4. Check the server
systemctl status ohm-agent ohm-agent-upgrade.path --no-pager
sudo docker ps --format '{{.Names}}\t{{.Status}}'
curl -s http://127.0.0.1:5201/health # the agent
curl -s http://127.0.0.1:5311/health # the API: "serverConfigLoaded":true
Every managed container carries the ohm- prefix. Expect ohm-agent, ohm-api, ohm-web,
ohm-nginx, ohm-postgres, ohm-pgadmin, ohm-geoip and ohm-fail2ban, all healthy, and
ohm-portainer when you chose it.
$ sudo ohm server infoserver info masks credentials. Add --show-secrets to reveal them. A remote ohm or a script
does not use the console key shown there but an API key of its own: see
Remote administration.
5. Reach the console
The console listens on 5322 (HTTPS) on every interface and on 5321 (HTTP) on the server itself only, not behind nginx. Two ways in:
- Open the port for your own address only and browse to
https://pelmo.example.com:5322/. - Or tunnel it over SSH, which opens nothing:
then browse tossh -N -L 5322:127.0.0.1:5322 admin@pelmo.example.com
https://localhost:5322/.
An instance's Code tab opens the browser editor on port
5323 of the same server, so open or tunnel 5323 beside 5322 when you use it
(-L 5323:127.0.0.1:5323 on the same ssh line).
The certificate is self-signed for the hostname, so the browser warns once. Accept it. This is not cosmetic: passkeys only work in a secure context, and plain HTTP is not one.
Reach the console by name, not by address. A passkey's relying-party id must be a domain, and the certificate carries DNS names only. The authenticator app, offered under My Account, is not address-bound.
6. Sign in and finish the setup
Sign in as admin with the printed password.
The license agreement is shown once, for this account. Accepting it records the version, the
digest of the exact text shown, your user name, the time and the address you connected from. Every
user of the console is asked the first time they sign in, and again whenever a release carries a new
version. Read it at any time with ohm license agreement show, and see who accepted what with
ohm license agreement status.
Then the Server setup page opens. Until you finish it, the menu offers this one page.
The setup page shows what the installer wrote, and links to the policy pages that are yours to
decide. To change any of what the installer wrote, run ohm install again on the host; the host's
own half is read back on the bootstrap agent's
dashboard. None of them is required: a server with no mail profile, no intrusion prevention and no
access rules is a complete server that notifies nobody and admits everyone.
| Page | What it decides |
|---|---|
| Mail profile | Where notifications and fail2ban reports go. See Mail |
| Intrusion prevention | Fail2Ban on the instances' and the console's logins. See Fail2Ban |
| Firewall | Which addresses, countries or continents may reach the console and the API. Leave it open until your own address is in the allow-list |
| Certificate | Replace the self-signed management certificate |
| License | Upload the license if you skipped it at install time. See Installing a license |
Click Finish setup. The menu opens up and the console lands on the dashboard.
Finally, on the Security tab of My Account, change the password and set up an authenticator app. See MFA and passkeys.
7. Create your first instance
Instances > New opens the New instance wizard: five tabs, ending on Review + create, and nothing is created before you press Create. See Creating an instance for what each tab decides.
An unregistered server hosts one instance with one database, which is enough to try the product out. It does not back up, restore or send mail, though: those need an edition, and the Community edition costs nothing. A server whose license later ends runs unregistered in the same way; see When the license changes. How many instances you may run is a quota rather than a feature:
| Instances | Unregistered | Community | Developer | Professional | Provider |
|---|---|---|---|---|---|
| Limit | 1 | 2 | Unlimited | 5 | Unlimited |
Repairing an installation
Run ohm install again on an installed server and it repairs whatever it finds, keeping your
credentials. See Repairing an installation for what it keeps, how to
replace the credentials, and how to rename the server.
Restricting the management ports
A ready-made UFW ruleset that opens 80 and 443 to the world and restricts every management port to an allow-list is published beside the installer:
curl -fsSLO https://packages.antelao.io/raw/antelao-ohm-ufw.sh
sudo bash antelao-ohm-ufw.sh --dry-run --ips "203.0.113.10|Alice,198.51.100.0/24|Office"
The script's signature is published beside it; check it before the first run, as Verify a download shows.
--ips takes a comma-separated list of ADDRESS|LABEL entries, IPv4 or IPv6, hosts or CIDRs, the
label optional. Run it with --dry-run first and check your own address is in the list: every
management port ends up restricted to it.
Some management ports are published by a container rather than opened by the host: pgAdmin, the
analytics service, the instance debuggers and the SSH gateway. Docker routes those past UFW's own
rules, so the script writes the same allow-list a second time into Docker's DOCKER-USER chain,
which Docker checks first. Docker installed before or after the script makes no difference.
From 0.24.0 the script also opens the browser editor's port 5323 to the same addresses, and its
DOCKER-USER block carries the server's cloud metadata block (Configuration > Network > Cloud
metadata) while that block is on, so reloading the firewall never lets the instances reach the
cloud's metadata service. If you ran the script on this server before 0.24.0, run it once more after
updating; until then the server puts the metadata block back within a minute of any firewall reload.
The script is safe to run again: it replaces its own rules rather than adding a second set. Run it
again, with the same --ips list, whenever the list changes and after an update whose release notes
ask for it. Updating the product never changes the server's firewall by itself.
Administering it from your own machine
ohm on your own machine manages the server over its API. The API is closed to the outside after a
fresh install, so your address has to be let in first. See
Administering it from your own machine.
If the install stops part-way
| Symptom | Look at |
|---|---|
| It stops at "Waiting for the agent" | systemctl status ohm-agent, journalctl -u ohm-agent -n 100 |
| It stops at "Management API" | docker logs ohm-api |
| An image pull is refused | Whether this host may reach the registry at all, then the registry credential in /etc/antelao/agent.env - the only place it lives after the install |
| The console says "server configuration is not composed" | The agent is down: systemctl restart ohm-agent |
| Lost the admin password | ohm user password-reset admin |
| Anything else | ohm install again. It repairs whatever it finds |
More in Troubleshooting.