Skip to Content

Odoo on Hetzner: how to host it properly

Odoo on Hetzner: how to host it properly
October 27, 2026 · 16 min read

Short answer

Hetzner is a good, low-cost home for Odoo. Pick a cloud server with enough memory. Run Postgres on it or on a second server on a private network. Use a Hetzner Cloud Firewall that only lets in 80, 443 and your own SSH. Send a nightly backup of the database and the filestore to Object Storage in another location. Then test a restore before you go live.

Many Odoo teams in Europe pick Hetzner. The servers are fast for what they cost, and you can choose Germany, Finland, the US or Singapore. This guide gives you the exact commands to set Odoo up there so it stays fast and safe after the first week.

For the install itself (Odoo, Postgres, the config file and the proxy), follow our guide on running Odoo on a VPS. This page covers what's different on Hetzner.

Which Hetzner server to pick

Set up the hcloud tool

You can do all of this in the Hetzner Console. The hcloud command line tool is faster and easier to repeat, so we use it here.

First, make an API token. In the Hetzner Console, open your project, then Security, then API tokens, then Generate API token. Pick Read & Write. Copy the token now. You can't see it again.

Then install hcloud on your laptop and link it to the project:

# macOS or Linux with Homebrew
brew install hcloud

# or Linux, without Homebrew
curl -sSLO https://github.com/hetznercloud/cli/releases/latest/download/hcloud-linux-amd64.tar.gz
sudo tar -C /usr/local/bin --no-same-owner -xzf hcloud-linux-amd64.tar.gz hcloud

hcloud context create odoo-prod   # paste the token when asked

Shared or dedicated CPU

Hetzner Cloud has two kinds of servers:

  • Shared vCPU. The CPU is shared with other servers on the same machine. Fine for tests and small teams.
  • Dedicated vCPU (for example CCX23). The CPU is yours alone. Better when people use Odoo all day, because shared CPUs can slow down when your neighbors are busy, and Odoo users feel that at once.

Minimum to start: 2 vCPU, 4 GB of memory and 50 GB of disk. That runs Odoo and Postgres on one server for a small team. Memory matters more than CPU: each worker needs its share, and Postgres wants its own. For more users, big imports or Postgres on its own server, our guide on how many Odoo workers you need shows how to size it.

See what's on offer:

hcloud server-type list
hcloud location list
hcloud image list --type system

Pick the location

Your data lives where the server lives. fsn1 (Falkenstein) and nbg1 (Nuremberg) are in Germany. hel1 is Helsinki, Finland. ash and hil are in the US, and sin is Singapore. For a company in the EU, a German or Finnish location keeps the data in the EU.

Create the server

Add your SSH key, make the firewall (next section), then create the server:

hcloud ssh-key create --name my-laptop --public-key-from-file ~/.ssh/id_ed25519.pub

hcloud server create \
  --name odoo-1 \
  --type ccx23 \
  --image ubuntu-24.04 \
  --location fsn1 \
  --ssh-key my-laptop \
  --firewall odoo-web

Keep both IPv4 and IPv6 (the default). If you add an AAAA record for your domain, make sure the proxy listens on IPv6 too, or some users will not reach Odoo.

Check for CPU steal on shared servers

On a shared server, "steal" is time your server waited for the real CPU. Check it during work hours:

vmstat 5 12

Look at the last column, st. If it often sits above a few percent while users complain that Odoo is slow, move to a dedicated type:

hcloud server poweroff odoo-1
hcloud server change-type --keep-disk odoo-1 ccx23
hcloud server poweron odoo-1

--keep-disk keeps the old disk size, so you can move back down later.

Put the filestore on a volume

Attachments live in the filestore, and it grows every day. A Hetzner Volume lets you add disk space without changing the server. Create one and attach it:

hcloud volume create --name odoo-data --size 100 --server odoo-1 --format ext4 --automount

Hetzner mounts it at /mnt/HC_Volume_<id>. On the server, move Odoo's data there and mount it back at /var/lib/odoo:

ls /mnt                          # find HC_Volume_12345
systemctl stop odoo
rsync -a /var/lib/odoo/ /mnt/HC_Volume_12345/
chown odoo:odoo /mnt/HC_Volume_12345
mv /var/lib/odoo /var/lib/odoo.old
mkdir /var/lib/odoo
echo '/mnt/HC_Volume_12345 /var/lib/odoo none bind,x-systemd.requires-mounts-for=/mnt/HC_Volume_12345 0 0' >> /etc/fstab
systemctl daemon-reload
mount /var/lib/odoo
findmnt /var/lib/odoo            # should show the volume
systemctl start odoo

Open a few attachments in Odoo. When they work, remove /var/lib/odoo.old.

To grow it later:

hcloud volume resize --size 200 odoo-data
lsblk                            # find the device, often /dev/sdb
resize2fs /dev/sdb

Volumes can only grow, not shrink. One more thing to know: Hetzner's server backups and snapshots do not include volumes. Your own backup (below) is what saves the filestore.

Where Postgres goes

Hetzner Cloud has no managed PostgreSQL. So you run it yourself, in one of two ways:

  • On the same server as Odoo. Simple, and fine for most small and mid-size teams. Postgres listens only on localhost, which is the default. Nothing else to do here.
  • On a second server, on a private network. Better when the database grows big or many users work at once.

Either way, Odoo connects with its own database user, odoo, never postgres.

Second server: the private network

Create a network and a subnet. All servers on one private network must be in the same network zone. fsn1, nbg1 and hel1 are all in eu-central.

hcloud network create --name odoo-net --ip-range 10.0.0.0/16
hcloud network add-subnet odoo-net --type cloud --network-zone eu-central --ip-range 10.0.0.0/24

hcloud server create --name odoo-db --type ccx23 --image ubuntu-24.04 \
  --location fsn1 --ssh-key my-laptop --firewall odoo-db

hcloud server attach-to-network odoo-1 --network odoo-net --ip 10.0.0.2
hcloud server attach-to-network odoo-db --network odoo-net --ip 10.0.0.3

Second server: let Postgres listen on the private IP only

On odoo-db (Ubuntu 24.04 ships PostgreSQL 16):

apt install -y postgresql
sudo -u postgres createuser --createdb --pwprompt odoo

In /etc/postgresql/16/main/postgresql.conf:

listen_addresses = 'localhost,10.0.0.3'

At the end of /etc/postgresql/16/main/pg_hba.conf, allow only the Odoo server:

host    all    odoo    10.0.0.2/32    scram-sha-256

Restart and check:

systemctl restart postgresql
ss -tlnp | grep 5432          # should show 127.0.0.1 and 10.0.0.3, never 0.0.0.0

On odoo-1, test the link, then point Odoo at it in odoo.conf:

psql -h 10.0.0.3 -U odoo -d postgres -c 'select 1'
db_host = 10.0.0.3
db_port = 5432
db_user = odoo
db_password = the-password-you-chose

Note that Hetzner Cloud Firewalls don't filter traffic inside a private network. That's why Postgres must listen only on 10.0.0.3 and pg_hba.conf must allow only 10.0.0.2.

Firewall

A Hetzner Cloud Firewall blocks all incoming traffic until you add rules. Outgoing traffic stays open. Create one for the Odoo server:

hcloud firewall create --name odoo-web
hcloud firewall add-rule odoo-web --direction in --protocol tcp --port 80  --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule odoo-web --direction in --protocol tcp --port 443 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule odoo-web --direction in --protocol tcp --port 22  --source-ips 203.0.113.10/32
hcloud firewall add-rule odoo-web --direction in --protocol icmp --source-ips 0.0.0.0/0 --source-ips ::/0

Replace 203.0.113.10 with your own IP. Ping (ICMP) stays open because it helps when you need to find a problem.

For the database server, allow SSH only:

hcloud firewall create --name odoo-db
hcloud firewall add-rule odoo-db --direction in --protocol tcp --port 22 --source-ips 203.0.113.10/32

If a server already exists, attach the firewall with:

hcloud firewall apply-to-resource odoo-web --type server --server odoo-1

Odoo's own ports (8069 and 8072) are not in the rules, so only the proxy on the same server can reach them. Check from your laptop that they are closed:

nc -zv -w 5 <server-ip> 8069    # should time out

Backups

For Odoo you need the database and the filestore, saved together, copied off the server. Here is how to send them to Hetzner Object Storage, in a different location from your server.

Pick the backup location

Object Storage runs in fsn1, nbg1 and hel1. Pick one that is not where your server is:

  • Server in fsn1 or nbg1: put backups in hel1.
  • Server in hel1: put backups in fsn1.
  • Server in the US or Singapore: Object Storage is only in Europe, so your backups would leave your region. If that matters for your data, use a storage service in your own region instead.

Use a separate project for backups

S3 keys on Hetzner work for every bucket in the same project. So give backups their own project. Then the key on your server can only reach the backup bucket.

  1. In the Hetzner Console, create a new project named odoo-backups.
  2. In that project, open Security, then S3 Credentials, then Generate credentials. Name it odoo-1. Copy the access key and the secret key now. You can't see the secret again.

Install the AWS CLI and save the keys

Hetzner Object Storage speaks the S3 API, so the AWS CLI works with it. On odoo-1:

apt install -y unzip
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip   # use awscli-exe-linux-aarch64.zip on Arm servers
unzip awscliv2.zip && ./aws/install

mkdir -p /etc/odoo-backup

/etc/odoo-backup/aws-config:

[default]
endpoint_url = https://hel1.your-objectstorage.com
region = hel1
request_checksum_calculation = when_required
response_checksum_validation = when_required

/etc/odoo-backup/aws-credentials:

[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY
chown -R odoo:odoo /etc/odoo-backup
chmod 600 /etc/odoo-backup/*
export AWS_CONFIG_FILE=/etc/odoo-backup/aws-config
export AWS_SHARED_CREDENTIALS_FILE=/etc/odoo-backup/aws-credentials

Create the bucket with Object Lock

Object Lock stops anyone from deleting a backup for a set number of days. Not you, not a script gone wrong, not someone who breaks into the server. You can only turn it on when you create the bucket. Bucket names are shared across all of Hetzner, so pick your own unique name.

aws s3api create-bucket --bucket your-odoo-backups --region hel1 --object-lock-enabled-for-bucket

aws s3api put-object-lock-configuration --bucket your-odoo-backups --object-lock-configuration \
  '{ "ObjectLockEnabled": "Enabled", "Rule": { "DefaultRetention": { "Mode": "COMPLIANCE", "Days": 14 }}}'

In COMPLIANCE mode, no one can shorten the lock once it is set. That is the point, but start with a number of days you are sure about. GOVERNANCE mode can be undone, which also means a stolen key could undo it.

Keep 30 days, then delete

Save this as lifecycle.json. It expires backups after 30 days, then cleans up the old versions and markers that Object Lock leaves behind:

{
  "Rules": [
    {
      "ID": "expire-backups",
      "Status": "Enabled",
      "Prefix": "",
      "Expiration": { "Days": 30 },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 1 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 2 }
    },
    {
      "ID": "delete-markers",
      "Status": "Enabled",
      "Prefix": "",
      "Expiration": { "ExpiredObjectDeleteMarker": true }
    }
  ]
}
aws s3api put-bucket-lifecycle-configuration --bucket your-odoo-backups --lifecycle-configuration file://lifecycle.json
aws s3api get-bucket-lifecycle-configuration --bucket your-odoo-backups

The backup script

/usr/local/bin/odoo-backup.sh:

#!/bin/bash
# /usr/local/bin/odoo-backup.sh, run as the odoo user
set -euo pipefail
DB=yourdb
DAY=$(date +%F)
DIR=/backups/$DAY
mkdir -p "$DIR"
pg_dump -Fc "$DB" > "$DIR/$DB.dump"
tar -czf "$DIR/$DB-filestore.tar.gz" -C /var/lib/odoo/filestore "$DB"
aws s3 cp --recursive --only-show-errors "$DIR" "s3://your-odoo-backups/$DAY/"
find /backups -mindepth 1 -maxdepth 1 -type d -mtime +3 -exec rm -rf {} +
curl -fsS -m 10 --retry 3 https://hc-ping.com/YOUR-BACKUP-UUID > /dev/null

The last line tells an outside service that the backup worked. More on that under Alerts. Then:

mkdir -p /backups && chown odoo:odoo /backups
chmod 755 /usr/local/bin/odoo-backup.sh

If Postgres runs on odoo-db, run the script on odoo-1 anyway, since the filestore lives there. Add PGHOST=10.0.0.3 to the service below, and put the password in /etc/odoo-backup/pgpass as 10.0.0.3:5432:*:odoo:the-password (owned by odoo, mode 600) with PGPASSFILE=/etc/odoo-backup/pgpass.

Run it every night

/etc/systemd/system/odoo-backup.service:

[Unit]
Description=Odoo backup to Hetzner Object Storage
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=odoo
Environment=AWS_CONFIG_FILE=/etc/odoo-backup/aws-config
Environment=AWS_SHARED_CREDENTIALS_FILE=/etc/odoo-backup/aws-credentials
ExecStart=/usr/local/bin/odoo-backup.sh

/etc/systemd/system/odoo-backup.timer:

[Unit]
Description=Nightly Odoo backup

[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target

Turn it on, run it once by hand, and check the result:

systemctl daemon-reload
systemctl enable --now odoo-backup.timer
systemctl start odoo-backup.service
journalctl -u odoo-backup.service -n 50
systemctl list-timers odoo-backup.timer
aws s3 ls s3://your-odoo-backups/ --recursive
aws s3api get-object-retention --bucket your-odoo-backups --key "$(date +%F)/yourdb.dump"

The last command should show COMPLIANCE and a date 14 days from now.

Restore test

A backup you never restored is a hope, not a backup. Make a test server, pull last night's files and restore them.

hcloud server create --name odoo-restore-test --type ccx23 --image ubuntu-24.04 \
  --location hel1 --ssh-key my-laptop --firewall odoo-web

On it, install Odoo and Postgres the same way as production, plus the AWS CLI and the same /etc/odoo-backup files. Then:

export AWS_CONFIG_FILE=/etc/odoo-backup/aws-config
export AWS_SHARED_CREDENTIALS_FILE=/etc/odoo-backup/aws-credentials
cd /tmp
aws s3 cp --recursive s3://your-odoo-backups/2026-10-25/ .
chown odoo:odoo yourdb.dump yourdb-filestore.tar.gz

sudo -u odoo bash -c '
createdb -O odoo yourdb_restored
pg_restore -d yourdb_restored --no-owner --role=odoo yourdb.dump
mkdir -p /var/lib/odoo/filestore/yourdb_restored
tar -xzf yourdb-filestore.tar.gz -C /var/lib/odoo/filestore/yourdb_restored --strip-components=1
'

Before you start Odoo on the copy, neutralize it. A copy of production still has your mail servers and scheduled jobs, so it can email real customers. As the odoo user, run:

odoo neutralize -c /etc/odoo/odoo.conf -d yourdb_restored

Use odoo-bin instead of odoo if you run Odoo from source. This turns off outgoing mail servers, scheduled actions and payment providers in the copy. Our guide on copying production to staging explains what it covers and what it misses.

Start Odoo with db_name = yourdb_restored. Log in, open some old invoices, and open their PDF attachments. If the attachments open, the backup works. Then delete the test server:

hcloud server delete odoo-restore-test

Do this again every few months, and after every big Odoo upgrade. Our guide on how to know if an Odoo backup will restore covers what to check.

Hetzner backups and snapshots

Hetzner has its own server backups and snapshots. They are useful, but they are not a replacement for the backup above.

  • Backups run once a day at a time Hetzner picks. Each server keeps up to seven. Turn them on with hcloud server enable-backup odoo-1.
  • Snapshots run when you ask, and stay until you delete them.
  • Neither includes volumes. If your filestore is on a volume, it is not in them.
  • They copy the disk while Postgres is running, and they stay with Hetzner. Your pg_dump is the copy you can trust and move anywhere.

Turn on backups for a quick way back after a bad system update. Take a snapshot right before every Odoo upgrade:

hcloud server create-image --type snapshot --description "before Odoo upgrade" odoo-1
hcloud image list --type snapshot

To go back, create a new server from that image with hcloud server create --image <snapshot-id> ....

Alerts

Hetzner shows graphs for CPU, disk reads and writes, and network traffic, in the Console or with hcloud server metrics --type cpu odoo-1. These show activity, not how full the disk is, and not whether Odoo answers. Set up those alerts yourself.

Backup did not run. Create a free check on healthchecks.io, set it to expect one ping a day, and put its URL in the last line of the backup script. If a night is missed, you get an email.

Disk filling up. Make a second check, and this script:

#!/bin/bash
# /usr/local/bin/disk-check.sh
set -euo pipefail
LIMIT=80
for M in / /var/lib/odoo; do
  USE=$(df --output=pcent "$M" | tail -1 | tr -dc '0-9')
  if [ "$USE" -ge "$LIMIT" ]; then
    curl -fsS -m 10 --retry 3 https://hc-ping.com/YOUR-DISK-UUID/fail > /dev/null
    exit 0
  fi
done
curl -fsS -m 10 --retry 3 https://hc-ping.com/YOUR-DISK-UUID > /dev/null

Run it every hour with a service and timer like the backup ones (ExecStart=/usr/local/bin/disk-check.sh, OnCalendar=hourly).

Odoo is down. Point any outside uptime service at https://odoo.example.com/web/health. On Odoo 17 and later, it answers {"status": "pass"} when Odoo is up.

Sending email from Odoo

Hetzner blocks outgoing ports 25 and 465 on cloud servers by default. Port 587 is open. So in Odoo, under Settings, Technical, Outgoing Mail Servers, use your mail provider's SMTP server on port 587 with STARTTLS.

If you need port 25 or 465, you can ask Hetzner to open them with a limit request in the Console. That is possible once your account is a month old and the first invoice is paid. Hetzner decides case by case.

Odoo traps on any server, Hetzner included

  • Websocket. Since Odoo 16, chat and live updates use port 8072. Your proxy must send /websocket there.
  • Proxy mode. Set proxy_mode = True and send X-Forwarded-Host and X-Forwarded-Proto, or your links come out on http://.
  • Database manager. Set list_db = False and a strong admin_passwd.
  • Disk. Logs, the filestore and local backups all grow. Watch the disk, and rotate the logs.

In odoo.conf:

proxy_mode = True
list_db = False
admin_passwd = a-long-random-password
workers = 5
gevent_port = 8072

In the nginx server block:

location /websocket {
    proxy_pass http://127.0.0.1:8072;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Set workers from the server's memory, not from the example above.

Before you go live

  • Odoo runs with workers above 0, sized to the server's memory
  • /websocket goes to port 8072, and chat works
  • proxy_mode = True, and links in emails start with https://
  • list_db = False and a strong admin_passwd
  • The Hetzner firewall only allows 80, 443 and your own SSH, and port 8069 times out from outside
  • If Postgres is on its own server, it listens only on the private IP
  • Outgoing email works on port 587
  • The SSL certificate renews on its own
  • systemctl list-timers shows the backup timer, and last night's files are in the bucket with Object Lock
  • Logs rotate, and you get an alert when the disk is filling up, a backup is missed, or Odoo stops answering
  • A backup of the database and the files has been restored on another server, and the attachments open
  • A snapshot is taken before every Odoo upgrade

Keep Hetzner, hand over the Odoo work

You can keep your Hetzner server and still not run Odoo yourself. With bring your own server, the server stays in your Hetzner account and your chosen location. We run Odoo on it: deploys from Git, staging, backups, updates and help from Odoo experts. You keep the server running and big enough. Our software connects out from your server, so you don't open any port to us.

Skysize is built by ex-Odoo engineers and is ISO 27001 certified.

Talk to an Odoo hosting expert

FAQ

Is Hetzner good for Odoo? Yes. It's fast for the cost, and it has locations in the EU, the US and Singapore. Pick dedicated CPUs for teams that use Odoo all day.

Does Hetzner have managed PostgreSQL for Odoo? No. Run Postgres on the Odoo server, or on a second server on a private network.

Are Hetzner backups enough for Odoo? Not on their own. They don't include volumes, and they stay with Hetzner. Also save the database and the filestore together, off the server, and test a restore.

Which Hetzner location should hold my Odoo backups? A different one from your server. If the server is in Falkenstein or Nuremberg, use Helsinki. If it is in Helsinki, use Falkenstein.

Why can't Odoo send email from my Hetzner server? Ports 25 and 465 are blocked by default on Hetzner Cloud. Use port 587 with STARTTLS, or ask Hetzner to open the other ports once your account qualifies.

Should I put the Odoo filestore on a Hetzner Volume? Yes, if it will grow. A volume can grow without changing the server. Just remember that Hetzner backups and snapshots skip volumes, so your own backup must cover it.

Odoo on Hetzner: how to host it properly
Writes about Odoo infrastructure at Skysize, a Odoo hosting provider based in Belgium, made by ex-Odoo engineers and ISO 27001 certified, serving businesses and agencies worldwide.