In this tutorial, you will deploy Odoo on a Raff Ubuntu 24.04 VM with Docker Compose, PostgreSQL, Caddy automatic HTTPS, persistent storage, database listing lock-down, and a first verified backup.
Odoo is an open-source business application suite used for CRM, sales, inventory, accounting, websites, and operations. This tutorial installs Odoo 18 behind HTTPS, creates the first Odoo database, disables public database listing, and verifies backups before storing business data.
Raff Technologies runs over 10,000 VMs across its compute platform in Vint Hill, Virginia, on AMD EPYC hardware with NVMe storage.
Prerequisites:
- A Raff Ubuntu 24.04 VM
- SSH access with sudo privileges
- A domain or subdomain pointing to your Raff VM, for example
erp.example.com - Ports
80/tcpand443/tcpopen for HTTPS certificate issuance and web access
This tutorial was tested on a Raff VM with 2 vCPU, 4 GB DDR5 RAM, 40 GB NVMe storage, and Ubuntu 24.04.4 LTS.
Tested on Raff infrastructure by Aybars Altınyay, platform engineer and technical writer at Raff Technologies.
📌 Note: Use a domain or subdomain for browser-trusted HTTPS. A raw IP address is acceptable for basic HTTP testing, but it will not receive a normal browser-trusted certificate in this Caddy setup.
Step 1 — Update the Ubuntu server
Update the package index and upgrade installed packages. The upgrade command uses noninteractive options so Ubuntu keeps existing modified configuration files, including SSH configuration, during automated tutorial runs.
bashsudo apt update
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt upgrade -y \
-o Dpkg::Options::=--force-confdef \
-o Dpkg::Options::=--force-confold
sudo apt install -y ca-certificates curl gnupg lsb-release ufw dnsutils openssl
📌 Note: If Ubuntu still asks what to do with a modified
/etc/ssh/sshd_configfile, select keep the local version currently installed. This preserves the current SSH login configuration on the Raff VM.
Verify the server is running Ubuntu 24.04:
bashlsb_release -ds
Expected output:
textUbuntu 24.04.4 LTS
Minor point releases such as Ubuntu 24.04 LTS or Ubuntu 24.04.x LTS are acceptable.
Step 2 — Install Docker and Docker Compose
Install Docker Engine and the Docker Compose plugin from Docker’s official Ubuntu repository.
bashsudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify Docker and Docker Compose:
bashsudo docker run --rm hello-world
docker compose version
Expected output includes:
textHello from Docker!
Docker Compose version v5.1.3
Any current Docker Compose plugin version is acceptable.
Step 3 — Configure DNS and firewall rules
Set your Odoo domain and ACME email address. Caddy uses the email when registering the certificate account.
bashread -rp "Odoo domain, for example erp.example.com: " ODOO_DOMAIN
read -rp "ACME email, for example admin@example.com: " ACME_EMAIL
dig +short "$ODOO_DOMAIN"
ip -4 addr show scope global
The DNS result must show the public IPv4 address of your Raff VM.
📌 Note: Use your own domain or subdomain for production. Temporary wildcard DNS services such as
sslip.ioornip.iocan hit shared certificate rate limits and are only suitable for testing.
Enable the firewall and allow only SSH, HTTP, and HTTPS:
bashsudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status numbered
Expected output includes:
textStatus: active
[ 1] OpenSSH ALLOW IN Anywhere
[ 2] 80/tcp ALLOW IN Anywhere
[ 3] 443/tcp ALLOW IN Anywhere
📌 Note: Odoo will not expose a public host port. Only Caddy listens publicly on ports
80and443.
Step 4 — Create the Odoo Compose stack
Create the deployment directory, configuration directory, addon directory, secret directory, and backup directory.
bashsudo mkdir -p /opt/odoo/{config,addons,secrets,backups}
sudo chown -R "$USER":"$USER" /opt/odoo
cd /opt/odoo
Generate a PostgreSQL password and an Odoo master password. Hex passwords are used to avoid copy-and-paste problems with special characters on the Odoo database creation page.
bashopenssl rand -hex 32 > secrets/postgresql_password
openssl rand -hex 32 > .odoo_master_password
chmod 700 secrets
chmod 644 secrets/postgresql_password
chmod 600 .odoo_master_password
📌 Note: With local Docker Compose file-based secrets, the Odoo container must be able to read the PostgreSQL password file. Keep the
secretsdirectory restricted, but leavesecrets/postgresql_passwordreadable by the container.
Create the .env file for Compose and Caddy:
bashcat > .env <<EOF
ODOO_DOMAIN=$ODOO_DOMAIN
ACME_EMAIL=$ACME_EMAIL
EOF
Create the Odoo configuration file:
bashODOO_MASTER_PASSWORD="$(cat .odoo_master_password)"
cat > config/odoo.conf <<EOF
[options]
admin_passwd = $ODOO_MASTER_PASSWORD
addons_path = /usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons
data_dir = /var/lib/odoo
proxy_mode = True
list_db = True
log_level = warn
EOF
chmod 644 config/odoo.conf
Create the Docker Compose file:
bashcat > compose.yaml <<'EOF'
services:
db:
image: postgres:15
container_name: odoo-db
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: odoo
POSTGRES_PASSWORD_FILE: /run/secrets/postgresql_password
PGDATA: /var/lib/postgresql/data/pgdata
secrets:
- postgresql_password
volumes:
- odoo_db_data:/var/lib/postgresql/data/pgdata
networks:
- odoo_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
interval: 10s
timeout: 5s
retries: 5
odoo:
image: odoo:18.0
container_name: odoo
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
HOST: db
USER: odoo
PASSWORD_FILE: /run/secrets/postgresql_password
secrets:
- postgresql_password
volumes:
- odoo_web_data:/var/lib/odoo
- ./config:/etc/odoo
- ./addons:/mnt/extra-addons
networks:
- odoo_net
caddy:
image: caddy:2
container_name: odoo-caddy
restart: unless-stopped
depends_on:
- odoo
ports:
- "80:80"
- "443:443"
environment:
ODOO_DOMAIN: ${ODOO_DOMAIN}
ACME_EMAIL: ${ACME_EMAIL}
LOG_FILE: /data/access.log
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- odoo_net
secrets:
postgresql_password:
file: ./secrets/postgresql_password
networks:
odoo_net:
driver: bridge
volumes:
odoo_web_data:
name: odoo_web_data
odoo_db_data:
name: odoo_db_data
caddy_data:
caddy_config:
EOF
Create the Caddy reverse proxy configuration. This file enables automatic HTTPS, uses the ACME email from .env, forwards normal Odoo traffic to port 8069, and forwards Odoo websocket traffic to port 8072.
bashcat > Caddyfile <<'EOF'
{
email {$ACME_EMAIL}
}
{$ODOO_DOMAIN} {
log {
level INFO
output file {$LOG_FILE} {
roll_size 10MB
roll_keep 10
}
}
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
@websocket path /websocket
reverse_proxy @websocket odoo:8072
reverse_proxy odoo:8069
}
EOF
Validate the Compose configuration:
bashsudo docker compose config >/dev/null && echo "Compose config is valid"
Expected output:
textCompose config is valid
Step 5 — Start Odoo, PostgreSQL, and Caddy
Start the stack:
bashcd /opt/odoo
sudo docker compose up -d
Verify the containers:
bashsudo docker compose ps
Expected output includes:
textNAME SERVICE STATUS
odoo odoo Up
odoo-caddy caddy Up
odoo-db db Up
Verify PostgreSQL is ready:
bashsudo docker compose exec -T db pg_isready -U odoo -d postgres
Expected output:
text/var/run/postgresql:5432 - accepting connections
Check the Caddy logs for certificate issuance:
bashsudo docker compose logs --tail=80 caddy
Expected output includes:
textcertificate obtained successfully
Verify Odoo loads over HTTPS:
bashODOO_DOMAIN=$(grep '^ODOO_DOMAIN=' /opt/odoo/.env | cut -d= -f2)
curl -L -s -o /dev/null -w '%{http_code} %{url_effective}\n' "https://$ODOO_DOMAIN"
Before the first database exists, expected output is similar to:
text200 https://erp.example.com/web/database/selector
Step 6 — Create the first Odoo database

Open your Odoo URL in a browser:
bashecho "https://$(grep '^ODOO_DOMAIN=' /opt/odoo/.env | cut -d= -f2)"
Display the Odoo master password:
bashsudo cat /opt/odoo/.odoo_master_password
Use the Odoo database creation page to create the first database.
Use these values:
textMaster Password: paste the value from /opt/odoo/.odoo_master_password
Database Name: raff_odoo
Email: admin@example.com
Password: use a strong password that is not reused anywhere else
Language: English
Country: your country
Demo data: leave unchecked for production
⚠️ Warning: Store the Odoo master password and first admin password in a secure password manager. The master password controls database creation, backup, restore, and deletion actions.
📌 Note: Use the database creation form for a new deployment. Do not use the restore form unless you are importing an existing Odoo backup archive.
Visible state check:
textThe browser shows the Odoo database creation page over HTTPS.
You can create the first database.
You can log in to the Odoo dashboard with the first admin account.

After the database is created, log in and confirm the Apps dashboard loads.

Step 7 — Lock down database listing
After the first database exists, disable public database listing and restrict Odoo to the database you created.
bashcd /opt/odoo
read -rp "Odoo database name created in Step 6, for example raff_odoo: " ODOO_DB_NAME
if ! [[ "$ODOO_DB_NAME" =~ ^[A-Za-z0-9_]+$ ]]; then
echo "Use only letters, numbers, and underscores in the database name."
exit 1
fi
printf "%s\n" "$ODOO_DB_NAME" > .odoo_db_name
if grep -q '^list_db =' config/odoo.conf; then
sed -i 's/^list_db = .*/list_db = False/' config/odoo.conf
else
printf "\nlist_db = False\n" >> config/odoo.conf
fi
if grep -q '^dbfilter =' config/odoo.conf; then
sed -i "s|^dbfilter = .*|dbfilter = ^${ODOO_DB_NAME}$|" config/odoo.conf
else
printf "\ndbfilter = ^%s$\n" "$ODOO_DB_NAME" >> config/odoo.conf
fi
sudo docker compose restart odoo
Verify database listing is disabled and the database filter is active:
bashgrep '^list_db = False' /opt/odoo/config/odoo.conf
grep '^dbfilter =' /opt/odoo/config/odoo.conf
Expected output:
textlist_db = False
dbfilter = ^raff_odoo$
Verify Odoo still loads after the restart:
bashODOO_DOMAIN=$(grep '^ODOO_DOMAIN=' /opt/odoo/.env | cut -d= -f2)
curl -L -s -o /dev/null -w '%{http_code} %{url_effective}\n' "https://$ODOO_DOMAIN"
Expected output includes:
text200
Step 8 — Create the first Odoo backup
Create a first backup of the Odoo PostgreSQL database and Odoo filestore before adding business data.
bashcd /opt/odoo
ODOO_DB_NAME="$(cat .odoo_db_name)"
STAMP="$(date +%F-%H%M%S)"
sudo docker compose exec -T db pg_dump -U odoo "$ODOO_DB_NAME" \
| gzip > "backups/odoo-db-${ODOO_DB_NAME}-${STAMP}.sql.gz"
sudo docker run --rm \
-v odoo_web_data:/data:ro \
-v /opt/odoo/backups:/backup \
alpine:3.20 \
tar -czf "/backup/odoo-filestore-${ODOO_DB_NAME}-${STAMP}.tar.gz" -C /data .
Verify both backup files exist and are readable:
bashcd /opt/odoo
ODOO_DB_NAME="$(cat .odoo_db_name)"
LATEST_DB_BACKUP="$(ls -1t backups/odoo-db-"$ODOO_DB_NAME"-*.sql.gz | head -n 1)"
LATEST_FILESTORE_BACKUP="$(ls -1t backups/odoo-filestore-"$ODOO_DB_NAME"-*.tar.gz | head -n 1)"
ls -lh "$LATEST_DB_BACKUP" "$LATEST_FILESTORE_BACKUP"
gzip -t "$LATEST_DB_BACKUP" \
&& echo "Database backup archive is valid"
sudo tar -tzf "$LATEST_FILESTORE_BACKUP" | head
Expected output includes:
textodoo-db-raff_odoo-YYYY-MM-DD-HHMMSS.sql.gz
odoo-filestore-raff_odoo-YYYY-MM-DD-HHMMSS.tar.gz
Database backup archive is valid
./filestore/
⚠️ Warning: Store Odoo backups off-server. Local backups are useful for quick recovery, but production backups should also exist outside the VM.
Step 9 — Verify the complete Odoo deployment

Verify HTTPS, container status, PostgreSQL readiness, database lock-down, and backup files.
bashcd /opt/odoo
ODOO_DOMAIN=$(grep '^ODOO_DOMAIN=' .env | cut -d= -f2)
ODOO_DB_NAME=$(cat .odoo_db_name)
echo "Checking HTTPS:"
curl -L -s -o /dev/null -w '%{http_code} %{url_effective}\n' "https://$ODOO_DOMAIN"
echo "Checking containers:"
sudo docker compose ps
echo "Checking PostgreSQL:"
sudo docker compose exec -T db pg_isready -U odoo -d postgres
echo "Checking Odoo database lock-down:"
grep '^list_db = False' config/odoo.conf
grep '^dbfilter =' config/odoo.conf
echo "Checking backups:"
ls -1 backups/odoo-db-"$ODOO_DB_NAME"-*.sql.gz backups/odoo-filestore-"$ODOO_DB_NAME"-*.tar.gz | tail -n 4
Expected output includes:
text200 https://erp.example.com/web/login
odoo odoo Up
odoo-caddy caddy Up
odoo-db db Up
/var/run/postgresql:5432 - accepting connections
list_db = False
dbfilter = ^raff_odoo$
backups/odoo-db-raff_odoo-YYYY-MM-DD-HHMMSS.sql.gz
backups/odoo-filestore-raff_odoo-YYYY-MM-DD-HHMMSS.tar.gz
Complete the browser verification:
text1. Open https://your-odoo-domain.
2. Log in with the first Odoo admin account.
3. Confirm the Odoo Apps dashboard loads.
4. Open the user menu and confirm you are logged in as the admin user.
5. Refresh the page and confirm the dashboard still loads over HTTPS.
The Odoo deployment is complete when HTTPS works, the Odoo dashboard loads, PostgreSQL accepts connections, database listing is disabled, the database filter is active, and the first backup files exist.
Cleanup (Optional)
Use this section only if you want to remove Odoo from the Raff VM.
⚠️ Warning: The following commands permanently delete the Odoo containers, PostgreSQL data volume, Odoo filestore volume, Caddy certificate storage, configuration files, backups, and uploaded business data. Back up anything you need before proceeding.
bashcd /opt/odoo
sudo docker compose down -v
sudo rm -rf /opt/odoo
Close the firewall ports if this VM no longer hosts public web services:
bashsudo ufw delete allow 80/tcp
sudo ufw delete allow 443/tcp
sudo ufw status numbered
Expected output no longer lists 80/tcp or 443/tcp rules.
Troubleshooting
HTTPS certificate issuance fails
Cause: The domain does not point to the Raff VM, ports 80/tcp and 443/tcp are blocked, or the certificate service cannot reach Caddy.
Fix:
bashODOO_DOMAIN=$(grep '^ODOO_DOMAIN=' /opt/odoo/.env | cut -d= -f2)
dig +short "$ODOO_DOMAIN"
sudo ufw status numbered
cd /opt/odoo
sudo docker compose logs --tail=100 caddy
The DNS output must show the Raff VM public IP. The firewall output must allow 80/tcp and 443/tcp.
Odoo shows a 502 error
Cause: Caddy is running, but the Odoo container is not reachable on the Docker network.
Fix:
bashcd /opt/odoo
sudo docker compose ps
sudo docker compose logs --tail=100 odoo
sudo docker compose restart odoo caddy
Expected output from sudo docker compose ps shows odoo and odoo-caddy as Up.
Odoo restarts with a PostgreSQL secret permission error
Cause: The Odoo container cannot read /run/secrets/postgresql_password.
Fix:
bashcd /opt/odoo
chmod 700 secrets
chmod 644 secrets/postgresql_password
sudo docker compose restart odoo
sudo docker compose logs --tail=50 odoo
Odoo cannot connect to PostgreSQL
Cause: PostgreSQL is not healthy, the password secret was changed after the database volume was created, or the database container did not start correctly.
Fix:
bashcd /opt/odoo
sudo docker compose ps
sudo docker compose exec -T db pg_isready -U odoo -d postgres
sudo docker compose logs --tail=100 db
Expected output:
text/var/run/postgresql:5432 - accepting connections
⚠️ Warning: Do not delete the PostgreSQL volume to fix a password mismatch unless you intentionally want to erase all Odoo databases.
Database creation shows Access Denied
Cause: The value entered in the Odoo Master Password field does not exactly match admin_passwd in /opt/odoo/config/odoo.conf.
Fix:
bashsudo cat /opt/odoo/.odoo_master_password
grep '^admin_passwd =' /opt/odoo/config/odoo.conf
If the password is hard to copy, rotate it to a hex-only value:
bashcd /opt/odoo
openssl rand -hex 32 > .odoo_master_password
NEW_MASTER_PASSWORD="$(cat .odoo_master_password)"
sed -i "s/^admin_passwd = .*/admin_passwd = ${NEW_MASTER_PASSWORD}/" config/odoo.conf
chmod 600 .odoo_master_password
sudo docker compose restart odoo
echo "$NEW_MASTER_PASSWORD"
Store the new master password immediately.
Fix for a new test deployment with no business data:
⚠️ Warning: This removes the test Odoo database. Do not run this on a production database that contains business data.
bashcd /opt/odoo
sudo docker compose stop odoo
sudo docker compose exec -T db dropdb -U odoo raff_odoo
sudo docker compose start odoo
Then open the Odoo database creation page and create the database again.
Backup creation fails
Cause: The Odoo database name does not match the database created in Step 6, or the backup directory is missing.
Fix:
bashcd /opt/odoo
cat .odoo_db_name
ls -ld backups
sudo docker compose exec -T db psql -U odoo -d postgres -c '\l'
The database list must include the name stored in .odoo_db_name.
Conclusion
You now have Odoo 18 running on a Raff Ubuntu 24.04 VM with Docker Compose, PostgreSQL 15, Caddy HTTPS, persistent Docker volumes, database listing disabled, a database filter enabled, and a first verified backup. If you have not deployed your Raff VM yet, you can spin one up in 60 seconds at rafftechnologies.com.
Next: How to Install Docker on Ubuntu 24.04
Related: Automate Server Backups with Cron and Rsync on Ubuntu 24.04
Guide: Cloud Server Backup Strategies

