A single install_odoo.sh that takes a fresh Ubuntu server to a working,
TLS-terminated, reverse-proxied, hardened Odoo Community Edition deployment.
A Davenport Software asset.
The Odoo config and nginx vhost are derived from a working production deployment — the proxy headers, buffer sizes, timeouts,
dbfilter/list_dbsetup, and caching blocks are all real.
- Dependencies — build toolchain, Python 3 + venv libs, image/XML/LDAP
dev headers,
node-less+rtlcss, PostgreSQL 16 from the official PGDG repo, and the patched wkhtmltopdf 0.12.6.1 (the Qt build Odoo needs for PDF headers/footers — the distro package is not patched). - System user + DB role — a system
odoouser, and a PostgreSQL roleodoowithLOGIN CREATEDB(not superuser) and a random password. - Odoo source + venv — shallow
git cloneof the requested branch into/odoo/odoo-server, and a dedicated virtualenv at/odoo/venvbuilt from Odoo's ownrequirements.txt, keeping Odoo's dependencies isolated from the system Python. /etc/odoo-server.conf— the Odoo config (settings below), includingdb_user/db_passwordfor the Postgres role. Owned byodoo, mode0600.- systemd unit
odoo.service— a clean, modern unit with auto-restart and sandboxing. - nginx reverse-proxy vhost — the reverse-proxy template for
$DOMAIN, enabled, with the default site removed. The config is validated withnginx -tbefore any reload, so a bad config never takes the site down — if validation fails the script aborts and leaves the vhost file in place for inspection. - TLS —
certbot --nginxfor a Let's Encrypt cert + automatic HTTP→HTTPS redirect. Non-fatal if DNS isn't pointed yet (prints a re-run command). - Hardening —
ufw(allows the detected SSH port plus 80/443, default-deny incoming),fail2ban(sshd jail),unattended-upgrades, and sshd hardening (PermitRootLogin no; key-only auth is an opt-in flag).
The run ends with a summary of the URL, service commands, and next steps (DNS +
DB creation). Generated secrets (the master password and the Postgres
db_password) are written to /root/odoo-install-credentials.txt (mode 0600)
rather than printed to the terminal.
Steps are idempotent where reasonable (re-checks for existing user, role,
checkout, venv, packages; reuses the credentials already in
/etc/odoo-server.conf on a re-run) with clear colour-coded step logging.
Set these as environment variables or edit the CONFIG block at the top of the
script.
| Variable | Default | Meaning |
|---|---|---|
DOMAIN |
example.com |
Public FQDN served by nginx. |
DB_NAME |
odoo |
Primary database name (must match the subdomain because dbfilter=^%d$). |
ODOO_VERSION |
18.0 |
Odoo git branch — validated against NN.N / saas~NN.N; the script aborts on anything else. |
ADMIN_EMAIL |
admin@example.com |
Email for certbot registration. |
ADMIN_PASSWD |
(empty) | Odoo master/admin password. Auto-generated if empty (written to the credentials file, not printed). On a re-run the value already in odoo-server.conf is reused. |
ENABLE_GEOFILTER |
false |
Add the nginx GeoIP country allowlist (anglosphere + LATAM). Requires a country MMDB at /var/lib/geoip2/dbip-country.mmdb. |
ENABLE_CERTBOT |
true |
Request a Let's Encrypt cert via certbot --nginx. |
SSH_KEY_ONLY |
false |
Opt-in. Disable SSH password auth (key-only). Off by default to avoid lockout. |
WWW_REDIRECT |
true |
Also serve www.$DOMAIN → apex redirect. |
FORCE_GIT_RESET |
false |
On a re-run, allow a hard git reset of the Odoo checkout (discards local changes). Off by default to protect local patches. |
sudo DOMAIN=acme.com DB_NAME=acme ADMIN_EMAIL=ops@acme.com \
ODOO_VERSION=18.0 ENABLE_GEOFILTER=false \
./install_odoo.shproxy_mode = True— trust theX-Forwarded-*headers nginx sets.dbfilter = ^%d$+list_db = False— subdomain must equal the DB name; the DB manager / selector is hidden. This is how a single host serves multiple isolated databases by subdomain.http_port = 8069,gevent_port = 8072(longpolling/bus).xmlrpc_interface = 127.0.0.1— Odoo binds to loopback only; the public edge is nginx.workers = 2,max_cron_threads = 1, and the memory/time limits (limit_memory_soft/hard,limit_time_cpu = 600,limit_time_real = 1200,limit_request = 8192).db_user/db_passwordfor the Postgres role, plusadmin_passwd,logfile,addons_path(serveraddons+custom_addons).
location /→http://127.0.0.1:8069;location /longpolling→:8072.- The
X-Forwarded-Host/X-Forwarded-For/X-Forwarded-Proto/X-Real-IP/X-Client-IPheader set thatproxy_mode = Trueexpects. proxy_buffers 16 64k,proxy_buffer_size 128k, and 900s read/connect/ send timeouts so long reports and module installs don't get cut off.proxy_next_upstreamfast-fail on backend errors.- gzip block,
client_max_body_size 0(no upload cap), large header buffers. - The
~ /[a-zA-Z0-9_-]*/static/caching block (60m) and the~* \.(js|css|png|…)$2-day cache block. - certbot appends the
listen 443 ssl/ cert paths and the:80→:443redirect.
- Validated nginx reloads. The vhost is validated with
nginx -tbefore any reload — a broken config never takes the site down. On failure the script aborts and leaves the vhost file in place for inspection. - Modern service management. Odoo runs under a clean systemd unit with
auto-restart and sandboxing (
NoNewPrivileges,ProtectSystem=full,ProtectHome,PrivateTmp, scopedReadWritePaths). - Isolated Python. Odoo's dependencies live in a dedicated virtualenv
built from Odoo's own
requirements.txt, separate from the system Python. - Locked-down multi-tenancy.
dbfilter = ^%d$withlist_db = Falseties each database to its subdomain and hides the DB manager/selector. - Safe SSH changes. Key-only auth is opt-in; the sshd drop-in is
validated with
sshd -tand removed automatically if validation fails, so you can't lock yourself out.ufwallows the actual sshd port (detected, not hardcoded) before the firewall comes up. - Stable credentials. The master password and the Postgres
db_passwordare written intoodoo-server.conf(db_user/db_password) and to a0600credentials file. A re-run reuses those values instead of rotating them, so it never severs Odoo's connection to Postgres.
- ufw — allows the detected SSH port(s) (from
sshd -T, falling back to listening sockets, then22with a lockout warning) plus80/443;default deny incoming,default allow outgoing. - fail2ban —
sshdjail (systemd backend, 5 retries / 10 min → 1 h ban). - unattended-upgrades — daily list refresh + auto security upgrades.
- sshd —
PermitRootLogin no,X11Forwarding no,MaxAuthTries 4via a drop-in;PasswordAuthentication noonly whenSSH_KEY_ONLY=true. The config is validated withsshd -tand the drop-in is removed automatically if validation fails, to prevent locking yourself out. - systemd sandboxing —
NoNewPrivileges,ProtectSystem=full,ProtectHome,PrivateTmp, scopedReadWritePaths. - Odoo bound to loopback (
xmlrpc_interface = 127.0.0.1) so only nginx is exposed.
- A fresh Ubuntu 22.04 LTS (jammy) or 24.04 LTS (noble), x86_64 server. (PGDG/wkhtmltopdf package selection assumes these; adjust for other distros.)
- Run as root (or
sudo). - Outbound internet (GitHub, PGDG, wkhtmltopdf releases, Let's Encrypt).
- For TLS to succeed in one shot, point the
$DOMAINDNS A record at the server before running. If it isn't ready, the script still finishes on HTTP and prints the certbot re-run command.
list_db = False means there is no web DB-creation wizard, so create the DB
from the CLI (the summary prints this):
sudo -u odoo /odoo/venv/bin/python /odoo/odoo-server/odoo-bin \
-c /etc/odoo-server.conf -d <DB_NAME> -i base --stop-after-initThen browse to https://$DOMAIN. Service control:
systemctl status odoo
systemctl restart odoo
journalctl -u odoo -n 100 -fCustom modules go in /odoo/custom_addons/ (already on addons_path).
The auto-generated master password and Postgres db_password are in
/root/odoo-install-credentials.txt (mode 0600). Rotate the master password
after first login; Odoo can store it as a bcrypt hash in odoo-server.conf.
Passes shellcheck and bash -n, and runs under set -euo pipefail throughout.