Short answer
To host Odoo on DigitalOcean, run it on a Droplet with enough memory. Put Postgres on the Droplet or on DigitalOcean's managed PostgreSQL. Close every port but 80 and 443. Send backups of the database and the files to Spaces, in another region. Then test a restore before you go live.
DigitalOcean is simple to use, and its control panel is easy to learn. That makes it a common first home for Odoo. This guide gives you the exact commands for each step, and a way to check that each step worked.
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 DigitalOcean.
Before you start
You will use doctl, DigitalOcean's command line tool. Install it on your own computer. Then create a personal access token in the control panel (API > Tokens) and log in:
doctl auth init
doctl compute ssh-key list
The second command lists your SSH keys. Note the ID or fingerprint of the key you want on the server. If the list is empty, add your key under Settings > Security first.
In this guide, the Droplet and the database live in fra1 (Frankfurt). The backups go to ams3 (Amsterdam). Use your own regions. Just keep the backups in a different region from the Droplet.
Which Droplet to pick
DigitalOcean has Droplets with shared CPUs and Droplets with dedicated CPUs.
- Shared CPU (Basic plans) is fine for tests and small teams.
- Dedicated CPU (General Purpose and others) is better when people use Odoo all day. Their speed then doesn't depend on other customers.
Minimum to start: 2 vCPU, 4 GB of memory and 50 GB of disk. That runs Odoo and Postgres on one Droplet for a small team. Memory matters more than CPU: each worker needs its share, and Postgres wants its own if it runs on the same Droplet. For more users, big imports or managed Postgres, our guide on how many Odoo workers you need shows how to size it.
Pick the region with care. Your data lives where the Droplet lives.
Create a private network
A VPC is a private network. Your Droplet and your database talk over it, so their traffic never crosses the internet.
doctl vpcs create --name odoo-vpc --region fra1
doctl vpcs list
Copy the ID of odoo-vpc. We call it VPC_ID below.
Create the Droplet
First, check the size and image names, since they change over time:
doctl compute size list
doctl compute image list-distribution --format ID,Distribution,Slug
Then create the Droplet. s-2vcpu-4gb is a shared CPU size, and g-2vcpu-8gb is a dedicated CPU size. Both are only examples.
doctl compute droplet create odoo-prod \
--region fra1 \
--size g-2vcpu-8gb \
--image ubuntu-24-04-x64 \
--ssh-keys YOUR_KEY_ID \
--vpc-uuid VPC_ID \
--enable-monitoring \
--enable-backups \
--tag-names odoo \
--wait
What the flags do:
--ssh-keysputs your key on the root account. Without it, DigitalOcean emails you a password.--vpc-uuidputs the Droplet in your private network.--enable-monitoringinstalls the metrics agent. You need it for the disk alert later.--enable-backupsturns on Droplet backups. More on what they do below.
Get the Droplet's ID and its public and private IP addresses:
doctl compute droplet list
We call the ID DROPLET_ID below. Log in and check that the metrics agent runs:
ssh root@PUBLIC_IP
systemctl status do-agent
If you made the Droplet without the flag, install the agent from the Droplet:
curl -sSL https://repos.insights.digitalocean.com/install.sh | sudo bash
Now install Odoo with our guide on running Odoo on a VPS. If you plan to use managed PostgreSQL, skip the step that installs the Postgres server. You still need the Postgres client tools for backups.
Where Postgres goes
- On the same Droplet. Simple, and fine for many teams. Follow the VPS guide as it is.
- On DigitalOcean managed PostgreSQL. DigitalOcean runs the database server for you. It handles updates and keeps its own backups. Connections use SSL and a non-default port.
The rest of this section is for managed PostgreSQL.
Create the cluster
Create it in the same region and the same VPC as the Droplet:
doctl databases options slugs --engine pg
doctl databases create odoo-db \
--engine pg \
--version 17 \
--region fra1 \
--size db-s-1vcpu-1gb \
--num-nodes 1 \
--private-network-uuid VPC_ID \
--wait
doctl databases list
Copy the cluster ID. We call it DB_ID below.
db-s-1vcpu-1gb is the smallest size, shown only as an example. It allows just 22 connections, which is too few for most Odoo setups. See "Connections" below and pick a larger size from the list.
Let only the Droplet in
Add the Droplet as a trusted source. Then nothing else can reach the database:
doctl databases firewalls append DB_ID --rule droplet:DROPLET_ID
doctl databases firewalls list DB_ID
You can use --rule tag:odoo instead. Then every Droplet with the odoo tag gets in.
Get the connection details
Run this on your own computer. --private gives the address on the private network. --show shows the password, which is hidden by default:
doctl databases connection DB_ID --private --show
You get a host that starts with private-, a port (25060 in DigitalOcean's examples), the admin user doadmin, its password, and sslmode=require. Use the port you see.
Create the odoo user and the database
Odoo should not use doadmin. Give it its own user, and make that user the owner of the database. Then Odoo doesn't need the right to create databases.
On the Droplet, install the Postgres client that matches the cluster version. Then connect as doadmin with the connection string from the step before:
psql "postgresql://doadmin:PASSWORD@PRIVATE_HOST:25060/defaultdb?sslmode=require"
Run these three lines:
CREATE ROLE odoo LOGIN PASSWORD 'a-long-random-password';
GRANT odoo TO doadmin;
CREATE DATABASE yourdb OWNER odoo;
Why the middle line? doadmin is not a superuser. On PostgreSQL 16 and later, it can only hand a database to a role it is a member of. Without that line, the third line fails with must be able to SET ROLE "odoo".
We create the user with SQL, not in the control panel, so that doadmin is allowed to grant it. Check the result:
\l yourdb
\du odoo
The owner of yourdb should be odoo. The odoo role should have no extra rights, such as "Create DB". That is what you want.
Point Odoo at it
In /etc/odoo/odoo.conf:
db_host = private-odoo-db-do-user-0000000-0.x.db.ondigitalocean.com
db_port = 25060
db_user = odoo
db_password = a-long-random-password
db_name = yourdb
db_sslmode = require
dbfilter = ^yourdb$
Use your own host from doctl databases connection. Since Odoo can't create databases here, set up the empty database once from the command line:
sudo -u odoo odoo -c /etc/odoo/odoo.conf -d yourdb -i base --stop-after-init
Odoo's live chat also connects to a database called postgres to listen for updates. Check that the odoo user can reach it:
psql "postgresql://odoo:PASSWORD@PRIVATE_HOST:25060/postgres?sslmode=require" -c "select 1"
Connections
Each Odoo process can open up to db_maxconn connections. The default is 64. With 4 workers, 2 cron threads and the live chat process, that is 7 processes. Set a lower limit in odoo.conf:
db_maxconn = 8
Then 7 × 8 = 56 connections at most. Pick a database size that allows more than that. Check the limit with SHOW max_connections; in psql.
Use the direct port, not the pooler
DigitalOcean offers connection pools (PgBouncer). New pools use "transaction" mode by default. In that mode, PgBouncer does not support LISTEN, and Odoo's live chat uses LISTEN. Connect Odoo to the cluster's own port, as above, not to a pool.
What the managed database backs up
The managed database keeps its own backups. You can restore it to any point in the last 7 days. It does not back up the filestore. Attachments, images and documents live in /var/lib/odoo/filestore on the Droplet. They need their own backup, and the backup below takes care of both.
Firewall
Use a DigitalOcean Cloud Firewall. It drops all traffic that no rule allows. Allow:
- 80 and 443 from anywhere
- 22 only from your own IP addresses
Replace 203.0.113.10/32 with your own IP:
doctl compute firewall create --name odoo-fw \
--inbound-rules "protocol:tcp,ports:80,address:0.0.0.0/0,address:::/0 protocol:tcp,ports:443,address:0.0.0.0/0,address:::/0 protocol:tcp,ports:22,address:203.0.113.10/32" \
--outbound-rules "protocol:tcp,ports:all,address:0.0.0.0/0,address:::/0 protocol:udp,ports:all,address:0.0.0.0/0,address:::/0 protocol:icmp,address:0.0.0.0/0,address:::/0" \
--droplet-ids DROPLET_ID
The outbound rules matter. With no outbound rules, the Droplet can't reach anything, not even updates or Spaces.
Check it from your computer:
doctl compute firewall list
nc -vz -w 5 PUBLIC_IP 8069
The second command should time out. Odoo's own ports (8069 and 8072) should only be reached by the proxy on the Droplet.
Keep the same address
A Reserved IP keeps your address the same if you ever rebuild or replace the Droplet:
doctl compute reserved-ip create --droplet-id DROPLET_ID
Point your DNS record at this address, not at the Droplet's own IP.
Backups to Spaces
What Droplet backups do
Droplet backups copy the whole disk, at set times. You can pick every 4, 6 or 12 hours, every day, or every week, in the Droplet's Backups tab. They are kept in the same datacenter as the Droplet.
They are "crash-consistent". They save what is on the disk, not what is in memory. DigitalOcean warns that busy databases are hit hardest, and that heavy write loads may need backups at the application level. So keep Droplet backups, but don't count on them alone for Odoo.
Before a big upgrade, take a snapshot too:
doctl compute droplet-action snapshot DROPLET_ID --snapshot-name odoo-before-upgrade --wait
Create the bucket
Spaces is DigitalOcean's file storage. It works with S3 tools. Create the bucket in the control panel (Spaces Object Storage > Create Bucket), in a different region from the Droplet. Here that is ams3. Leave it private.
Create a key for this bucket only
Don't put a key on the Droplet that can reach all your buckets. Make a limited key that can only use your-odoo-backups:
doctl spaces keys create odoo-backup \
--grants 'bucket=your-odoo-backups;permission=readwrite'
Or in the control panel: Spaces Object Storage > Access Keys > Create Access Key, choose limited access, pick the bucket, and give it Read/Write/Delete. Copy the secret right away. It is shown only once.
Spaces has no "write only" level. The two levels per bucket are read, and read/write/delete. That is one more reason to turn on versioning (see below).
Set up s3cmd for the odoo user
sudo apt install s3cmd
sudo mkdir -p /backups && sudo chown odoo: /backups
sudo -u odoo -H s3cmd --configure
Give these answers:
- Access Key / Secret Key: the limited key from the step before
- Default Region: press Enter
- S3 Endpoint:
ams3.digitaloceanspaces.com - DNS-style bucket template:
%(bucket)s.ams3.digitaloceanspaces.com - Use HTTPS protocol: Yes
- Test access / Save settings: Y
This writes ~odoo/.s3cfg. Check it:
sudo -u odoo -H s3cmd ls s3://your-odoo-backups
The password file
With managed PostgreSQL, the backup needs the odoo user's password. Put it in ~odoo/.pgpass, so it is not in the script:
echo "PRIVATE_HOST:25060:*:odoo:a-long-random-password" | sudo -u odoo tee ~odoo/.pgpass
sudo chmod 600 ~odoo/.pgpass
The backup script
Save the database and the filestore together, then copy both to Spaces:
#!/bin/bash
# /usr/local/bin/odoo-backup.sh, run as the odoo user
set -euo pipefail
# Managed PostgreSQL only. Postgres on the Droplet? Delete this line.
export PGHOST=PRIVATE_HOST PGPORT=25060 PGUSER=odoo PGSSLMODE=require
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"
s3cmd put --recursive "$DIR" s3://your-odoo-backups/
find /backups -mindepth 1 -maxdepth 1 -type d -mtime +3 -exec rm -rf {} +
sudo chmod 755 /usr/local/bin/odoo-backup.sh
The pg_dump version must be the same as the server's major version, or newer. Check with pg_dump --version.
Run it every night
/etc/systemd/system/odoo-backup.service:
[Unit]
Description=Back up Odoo database and filestore to Spaces
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=odoo
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, and run it once by hand:
sudo systemctl daemon-reload
sudo systemctl enable --now odoo-backup.timer
sudo systemctl start odoo-backup.service
journalctl -u odoo-backup.service -n 20
sudo -u odoo -H s3cmd ls s3://your-odoo-backups/
You should see today's folder with two files in it.
Delete old backups on their own
Spaces supports expiry rules. Run this from your own computer, with a key that has full access to the bucket. This one deletes backups after 30 days:
s3cmd expire --expiry-days=30 --expiry-prefix= s3://your-odoo-backups
s3cmd getlifecycle s3://your-odoo-backups
Keep old versions
Spaces also supports versioning. When it is on, a deleted file is kept as an older version. So if someone gets the key on the Droplet and deletes your backups, you can still get them back. It is off by default. Turn it on with the AWS CLI and the regional endpoint:
aws s3api put-bucket-versioning \
--bucket your-odoo-backups \
--endpoint=https://ams3.digitaloceanspaces.com \
--versioning-configuration Status=Enabled
aws s3api get-bucket-versioning \
--bucket your-odoo-backups \
--endpoint=https://ams3.digitaloceanspaces.com
Old versions use space too. Check how they get cleaned up before you rely on the 30-day rule.
Test a restore
Do this on a test Droplet, never on the live one. Download one day's backup:
sudo -u odoo -H mkdir -p /backups/restore
cd /backups/restore
sudo -u odoo -H s3cmd get s3://your-odoo-backups/2026-10-25/yourdb.dump yourdb.dump
sudo -u odoo -H s3cmd get s3://your-odoo-backups/2026-10-25/yourdb-filestore.tar.gz yourdb-filestore.tar.gz
Run the commands below as the odoo user too. With managed PostgreSQL, first set the same PGHOST, PGPORT, PGUSER and PGSSLMODE as in the backup script.
On managed PostgreSQL, the odoo user can't create databases. So create the empty one as doadmin first:
CREATE DATABASE yourdb_restored OWNER odoo;
Then restore as odoo:
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
With Postgres on the Droplet, use createdb -O odoo yourdb_restored instead of the SQL line.
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 on the test Droplet with db_name = yourdb_restored. Log in, and open a few attachments. Our guide on how to know if an Odoo backup will restore covers the full test.
Alerts for disk and downtime
Logs, the filestore and local backups all grow. Get an email before the disk fills up:
doctl monitoring alert create \
--type v1/insights/droplet/disk_utilization_percent \
--compare GreaterThan --value 80 --window 5m \
--entities DROPLET_ID \
--emails [email protected] \
--description "Odoo disk over 80%"
Do the same for memory with --type v1/insights/droplet/memory_utilization_percent. The email must belong to your DigitalOcean account.
Then check that Odoo answers from outside, and that the SSL certificate doesn't run out:
doctl monitoring uptime create odoo-web \
--target https://erp.example.com/web/login \
--type https --regions eu_west,us_east
doctl monitoring uptime list
doctl monitoring uptime alert create CHECK_ID \
--name "Odoo down" --type down --period 2m \
--emails [email protected]
doctl monitoring uptime alert create CHECK_ID \
--name "SSL expires soon" --type ssl_expiry --threshold 14 \
--comparison less_than --period 5m \
--emails [email protected]
CHECK_ID comes from the list command.
Sending email from Odoo
DigitalOcean blocks SMTP ports 25, 465 and 587 on all Droplets. So Odoo can't send mail straight out, or through Gmail or Outlook on those ports.
Use an email sending service that accepts another port. DigitalOcean suggests SendGrid, which accepts port 2525. Check that the port is open from the Droplet:
nc -vz -w 5 smtp.sendgrid.net 2525
Then in Odoo, turn on developer mode. Go to Settings > Technical > Outgoing Mail Servers and add one:
- SMTP Server:
smtp.sendgrid.net - SMTP Port:
2525 - Connection Encryption: TLS (STARTTLS)
- Username:
apikey(the word itself) - Password: your SendGrid API key
Click Test Connection. Then set up SPF and DKIM for your domain, as your sending service explains, so your mail isn't marked as spam.
Odoo traps on any server, DigitalOcean included
Proxy mode, database manager, workers. In odoo.conf:
proxy_mode = True
list_db = False
admin_passwd = a-long-random-string
workers = 4
gevent_port = 8072
Without proxy_mode, links in emails come out on http://. Without list_db = False, anyone can reach the database manager. Size workers to the Droplet's memory.
Websocket. Since Odoo 16, chat and live updates go through port 8072. Your nginx must send /websocket there, and pass on who the visitor is:
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;
}
Your main location / block, which goes to port 8069, needs the same two X-Forwarded lines.
Disk. Rotate Odoo's logs, and keep the disk alert from above.
Before you go live
- Odoo runs with
workersabove 0, sized to the Droplet's memory /websocketgoes to port 8072, and chat worksproxy_mode = True, and links in emails start withhttps://list_db = Falseand a strongadmin_passwd- The Cloud Firewall allows only 80, 443 and your own SSH, and port 8069 times out from outside
- With managed PostgreSQL: Odoo connects as
odooover the private host, withdb_sslmode = require, and only the Droplet is a trusted source - Your DNS points at the Reserved IP
- The SSL certificate renews on its own, and the uptime check warns you before it expires
- Logs rotate, and you get an email when the disk passes 80%
- Odoo sends a test email through a service on port 2525
- The backup timer ran, and today's folder is in Spaces, in another region
- A backup of the database and the files has been restored on another Droplet, and the attachments open
Keep DigitalOcean, hand over the Odoo work
You can keep your Droplet and still not run Odoo yourself. With bring your own server, the Droplet stays in your account and your chosen region. We run Odoo on it: deploys from Git, staging, backups, updates and help from Odoo experts. You keep the Droplet running and big enough. Our software connects out from your Droplet, 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 DigitalOcean good for Odoo? Yes. It's simple to use. Pick a Droplet with enough memory, and dedicated CPUs for teams that use Odoo all day.
Can Odoo use DigitalOcean managed PostgreSQL?
Yes, over SSL with db_sslmode = require. Create the odoo user and the database with SQL as doadmin, and make odoo the owner. Back up the filestore separately.
Should Odoo connect through a DigitalOcean connection pool?
No. Pools use transaction mode by default, and that mode doesn't support the LISTEN command that Odoo's live chat uses. Connect to the cluster's own port.
Are Droplet backups enough for Odoo? Not on their own. They copy the disk while the database is running. Also save the database and the filestore together, off the Droplet, and test a restore.
Why doesn't Odoo send email from my Droplet? DigitalOcean blocks SMTP ports 25, 465 and 587 on Droplets. Send through an email service that accepts another port, such as 2525.
Which Spaces key should the Droplet use? A limited access key for the backup bucket only, with read/write/delete on that one bucket. Never a full access key.