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.
sudo 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:
lsb_release -ds
Expected output:
Ubuntu 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.
sudo 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:
sudo docker run --rm hello-world docker compose version
Expected output includes:
Hello 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.
read -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:
sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw --force enable sudo ufw status numbered
Expected output includes:
Status: 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.
sudo 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.
openssl 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:
cat > .env <<EOF ODOO_DOMAIN=$ODOO_DOMAIN ACME_EMAIL=$ACME_EMAIL EOF
Create the Odoo configuration file:
ODOO_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:
cat > 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.
cat > 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:
sudo docker compose config >/dev/null && echo "Compose config is valid"
Expected output:
Compose config is valid
Step 5 — Start Odoo, PostgreSQL, and Caddy
Start the stack:
cd /opt/odoo sudo docker compose up -d
Verify the containers:
sudo docker compose ps
Expected output includes:
NAME SERVICE STATUS odoo odoo Up odoo-caddy caddy Up odoo-db db Up
Verify PostgreSQL is ready:
sudo docker compose exec -T db pg_isready -U odoo -d postgres
Expected output:
/var/run/postgresql:5432 - accepting connections
Check the Caddy logs for certificate issuance:
sudo docker compose logs --tail=80 caddy
Expected output includes:
certificate obtained successfully
Verify Odoo loads over HTTPS:
ODOO_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:
200 https://erp.example.com/web/database/selector
Step 6 — Create the first Odoo database

Open your Odoo URL in a browser:
echo "https://$(grep '^ODOO_DOMAIN=' /opt/odoo/.env | cut -d= -f2)"
Display the Odoo master password:
sudo cat /opt/odoo/.odoo_master_password
Use the Odoo database creation page to create the first database.
Use these values:
Master 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:
The 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.
cd /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:
grep '^list_db = False' /opt/odoo/config/odoo.conf grep '^dbfilter =' /opt/odoo/config/odoo.conf
Expected output:
list_db = False dbfilter = ^raff_odoo$
Verify Odoo still loads after the restart:
ODOO_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:
200
Step 8 — Create the first Odoo backup
Create a first backup of the Odoo PostgreSQL database and Odoo filestore before adding business data.
cd /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:
cd /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:
odoo-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.
cd /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:
200 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:
1. 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.
cd /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:
sudo 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:
ODOO_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:
cd /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:
cd /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:
cd /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:
/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:
sudo 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:
cd /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.
cd /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:
cd /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
