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
fsn1ornbg1: put backups inhel1. - Server in
hel1: put backups infsn1. - 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.
- In the Hetzner Console, create a new project named
odoo-backups. - 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_dumpis 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
/websocketthere. - Proxy mode. Set
proxy_mode = Trueand sendX-Forwarded-HostandX-Forwarded-Proto, or your links come out onhttp://. - Database manager. Set
list_db = Falseand a strongadmin_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
workersabove 0, sized to the server's memory /websocketgoes to port 8072, and chat worksproxy_mode = True, and links in emails start withhttps://list_db = Falseand a strongadmin_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-timersshows 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.