Skip to content

How to Deploy Odoo on a VPS

Updated Oct 2026

verified on Ubuntu 26.04 · Oct 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Odoo Community Edition — the modular open-source ERP — on a VPS with PostgreSQL in Docker Compose behind Caddy HTTPS, a locked-down database manager, a restorable backup and a safe upgrade path.

Before you start
  • A VPS with 2 GB+ RAM (the catalog's floor for Odoo is 2 GB; 4 GB is more comfortable once you load real modules and users)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain or subdomain you can point at the server
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Odoo is

Odoo is a modular business suite: CRM, sales, invoicing, inventory, purchasing, manufacturing, HR, a website builder and e-commerce are separate apps on one shared database and one shared login. It is written in Python on PostgreSQL, with a JavaScript web client, and it is rated 3 / 5 to deploy. People reach for it as a self-hosted alternative to Salesforce or HubSpot, or as a single system to replace a pile of spreadsheets.

One licensing point matters before you install. Odoo ships in two editions. Community is open source under the LGPL-3.0 and is what this guide installs, using the official Docker image. Enterprise is a separate, proprietary, paid edition with additional apps (full Accounting and Payroll among them) and is not covered here. Check that the Community modules cover what you need before you commit your company's data to it.

This guide goes from "I have a server" to Odoo on HTTPS with a database created, the default administrator password replaced, the public database manager switched off and a backup you have restored once. For help choosing a provider and tier, see Best VPS for self-hosting.

Server sizing

The catalog puts Odoo's floor at 2 GB of RAM, and that is the number to plan around. On the catalog's test box the app container idled at roughly 106 MB with an empty database (the catalog's measured figure), and the whole install used about 3.5 GB of disk, most of it images. Our own run measured the same shape: about 104 MB for the app container, 63 MB for PostgreSQL and 14 MB for Caddy at idle, and 3.7 GB of images. Idle numbers flatter Odoo: memory grows with every installed app, every concurrent user and every report that touches a large table, and the default single-process mode handles one request at a time per container.

Plan on 2 GB for a small team trying it out and 4 GB or more for daily use by a real team. Disk grows with the database and with the filestore, where Odoo keeps attachments and generated documents, so size it for those.

Prepare the server

Update the system and install the few tools this guide uses:

sudo apt-get update && sudo apt-get -y upgrade
sudo apt-get install -y curl openssl ufw dnsutils

If you're logged in as root, create a non-root sudo user and continue as that user:

adduser deploy
usermod -aG sudo deploy

Then open only SSH and the two ports the reverse proxy needs:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable

Odoo will be reachable only through the reverse proxy. That matters on a Docker host: a published container port goes around ufw entirely, so the Compose file below binds the app to 127.0.0.1, and Caddy becomes the only public door. PostgreSQL publishes nothing at all. (The catalog's own Compose snippet publishes 8069 and uses a fixed database password, which is fine for a quick look and not for a server on the internet.)

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Docker

If Docker isn't on the server yet, the convenience script installs Docker Engine and the Compose plugin (the Docker and Compose on Ubuntu guide covers the details):

command -v docker >/dev/null || curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
sudo docker compose version

The group change applies on your next login; until then the commands below use sudo docker.

Install Odoo

Create a project directory and generate the secrets once. Three values are needed: the PostgreSQL password, the master password that guards Odoo's database-management functions, and the first administrator's password. Set your hostname first (this guide uses erp.example.com; replace it with yours). The guard keeps the step safe to re-run, because regenerating the database password on an existing install would lock Odoo out of its own database:

mkdir -p ~/odoo && cd ~/odoo
DOMAIN=erp.example.com
if [ ! -f .env ]; then
  ( umask 077
    {
      echo "DOMAIN=$DOMAIN"
      echo "POSTGRES_PASSWORD=$(openssl rand -hex 16)"
      echo "ODOO_MASTER_PASSWORD=$(openssl rand -hex 16)"
      echo "ODOO_ADMIN_PASSWORD=$(openssl rand -hex 12)"
    } > .env )
fi
grep -c -E '^(DOMAIN|POSTGRES_PASSWORD|ODOO_MASTER_PASSWORD|ODOO_ADMIN_PASSWORD)=' .env

Odoo reads its settings from a config file. This one points the app at the database container, pins it to a single database called odoo and tells Odoo it sits behind a reverse proxy. list_db = False stops Odoo listing the databases it hosts and switches the database-manager page off (in our run the page loaded and reported the manager as disabled by the administrator), so visitors do not get a database picker or a form to try your master password in. The random master password in admin_passwd is still the gate on the database functions, so keep it secret. The container runs as the odoo user (user ID 100, group 101), so the file is handed to it and made unreadable to everyone else:

cd ~/odoo
. ./.env
if [ ! -f odoo.conf ]; then
  ( umask 077
    cat > odoo.conf <<EOF
[options]
db_host = db
db_port = 5432
db_user = odoo
db_password = $POSTGRES_PASSWORD
db_name = odoo
dbfilter = ^odoo\$
list_db = False
admin_passwd = $ODOO_MASTER_PASSWORD
proxy_mode = True
EOF
  )
fi
sudo chown 100:101 odoo.conf
ls -l odoo.conf

Now the Compose file. It uses the official odoo image and PostgreSQL, with the app bound to loopback and Caddy added in front. The odoo:18.0 tag follows the 18.0 release series, which the Odoo project rebuilds regularly with fixes; the PostgreSQL image is pinned to a major version:

cd ~/odoo
cat > docker-compose.yml <<'EOF'
services:
  web:
    image: odoo:18.0
    restart: always
    depends_on:
      db:
        condition: service_healthy
    ports: ["127.0.0.1:8069:8069"]
    volumes:
      - odoo_data:/var/lib/odoo
      - ./odoo.conf:/etc/odoo/odoo.conf:ro
  db:
    image: postgres:17
    restart: always
    environment:
      POSTGRES_USER: odoo
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: postgres
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes: ["db_data:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
      start_period: 15s
      interval: 10s
      timeout: 5s
      retries: 10
  caddy:
    image: caddy:2
    restart: always
    ports: ["80:80", "443:443"]
    environment:
      DOMAIN: ${DOMAIN}
    volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "caddy_data:/data"]
    depends_on: [web]
volumes:
  odoo_data:
  db_data:
  caddy_data:
EOF
cat > Caddyfile <<'EOF'
{$DOMAIN} {
    encode zstd gzip
    reverse_proxy web:8069
}
EOF

A brand-new Odoo has no database yet. Normally you would create one in the browser, but that page is exactly what we just turned off, and the command line is repeatable. Start PostgreSQL, then initialise the odoo database with the base module and no demo data. This takes a minute or two:

cd ~/odoo
sudo docker compose up -d db
sudo docker compose run --rm web odoo -d odoo -i base --without-demo=all --stop-after-init > init.log 2>&1
rc=$?
tail -n 3 init.log
[ "$rc" -eq 0 ]

Initialisation creates a default administrator whose login is admin and whose password is also admin. That is public knowledge, so replace it before Odoo is ever started in its web-facing mode. This opens Odoo's shell and sets the password from the value in .env. The shell reads the command from standard input, so the password never appears in a process list. The last line fails loudly if the password was not set:

cd ~/odoo
. ./.env
sudo docker compose run --rm -T web odoo shell -d odoo --no-http > shell.log 2>&1 <<EOF
env.ref('base.user_admin').password = '$ODOO_ADMIN_PASSWORD'
env.cr.commit()
print('admin password updated')
EOF
rc=$?
tail -n 2 shell.log
[ "$rc" -eq 0 ] && grep -q 'admin password updated' shell.log

Now start the whole stack and wait for Odoo to answer. The loop is bounded, so a broken install fails instead of hanging:

cd ~/odoo
sudo docker compose up -d
code=000
for i in $(seq 1 40); do
  code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8069/web/health)
  [ "$code" = "200" ] && break
  sleep 3
done
echo "Odoo health endpoint answered HTTP $code"
sudo docker compose ps

HTTP 200 with all three containers running means Odoo is up. If it never answers, jump to Troubleshooting.

HTTPS + domain

Create an A record for your hostname (say erp.example.com) pointing at the server's public IP, and confirm it resolves before Caddy tries to get a certificate:

dig +short erp.example.com

Caddy is already running. It requests the certificate as soon as the name resolves and renews it on its own (see Automatic HTTPS with Caddy for how that works). Check that HTTPS answers, retrying while the certificate is issued:

. ~/odoo/.env
out=000
for i in $(seq 1 20); do
  out=$(curl -s -o /dev/null -w '%{http_code} %{ssl_verify_result}' "https://$DOMAIN/web/login") && [ "${out%% *}" = "200" ] && break
  sleep 5
done
echo "HTTPS status and certificate check: $out"

A second number of 0 means the certificate verified.

First login and hardening

Open https://erp.example.com/web/login. The login is admin; the password is the ODOO_ADMIN_PASSWORD line in ~/odoo/.env. Print it on the server with grep ODOO_ADMIN_PASSWORD ~/odoo/.env, sign in, then move it into your password manager. Logging in through the browser was not part of this guide's automated verification, which checked the login page, the health endpoint and the HTTPS certificate.

Then:

  1. Open your profile and change the administrator password and email to a long random one of your own. Create real users for your team instead of sharing the admin login.
  2. Install apps one at a time from the Apps menu, starting with the ones you will use this month. Every installed app adds tables, background jobs and memory use.
  3. Keep ODOO_MASTER_PASSWORD somewhere safe. It is the key to database creation, deletion and backup through Odoo's web and RPC interfaces, so treat it like a root password.
  4. Configure outgoing mail under Settings → Technical → Outgoing Mail Servers so invoices and notifications reach people. Email delivery was not tested in this guide's verification run, because it needs a live provider account.
  5. For more than a handful of users, add workers = 2 (or more) to odoo.conf and restart. Odoo's default threaded mode is meant for development and light use; worker mode needs the extra memory, so size the server first.

Backups

State lives in two places: the PostgreSQL database and Odoo's filestore (the odoo_data volume, which holds attachments and generated files). A database dump without the filestore restores a system with missing documents, so back up both. Both archives can contain customer data, so they are created readable by their owner only. Start with the database dump. The password is read inside the container, so it never appears on the host command line:

cd ~/odoo
( umask 077; sudo docker compose exec -T db sh -c 'pg_dump -U odoo -Fc odoo' > ~/odoo-db-$(date +%F).dump )
test -s ~/odoo-db-$(date +%F).dump && ls -lh ~/odoo-db-*.dump

Then archive the filestore and the configuration (which holds the database and master passwords):

cd ~/odoo
( umask 077
  sudo docker run --rm -v odoo_odoo_data:/data alpine tar czf - -C /data . > ~/odoo-files-$(date +%F).tar.gz
  sudo tar czf - docker-compose.yml Caddyfile odoo.conf .env > ~/odoo-config-$(date +%F).tar.gz )
ls -lh ~/odoo-files-*.tar.gz ~/odoo-config-*.tar.gz

Copy these files off the server, to encrypted object storage or another machine, and run them from cron. A backup you have never restored is a guess, so prove the dump loads. This starts a throwaway PostgreSQL container, restores the dump into it and counts the users in res_users:

cd ~
sudo docker rm -f odoo-restore-test >/dev/null 2>&1
sudo docker run -d --name odoo-restore-test -e POSTGRES_PASSWORD=scratch postgres:17 >/dev/null
for i in $(seq 1 40); do
  sudo docker exec odoo-restore-test pg_isready -h 127.0.0.1 -U postgres >/dev/null 2>&1 && break
  sleep 2
done
sudo docker exec odoo-restore-test createdb -U postgres odoo
sudo docker exec -i odoo-restore-test pg_restore -U postgres -d odoo --no-owner < ~/odoo-db-$(date +%F).dump
sudo docker exec odoo-restore-test psql -U postgres -d odoo -At -c "SELECT COUNT(*) FROM res_users"
sudo docker rm -f odoo-restore-test >/dev/null

It should print a number of at least 1, since Odoo always has the administrator and a few internal system users (it printed 5 in this guide's verification run). The check proves the dump loads and the schema is intact; it does not start Odoo against the restored database or check the filestore archive. For a real restore onto a new server, install Docker, unpack the config archive into ~/odoo, start only the database, create the odoo database and pg_restore the dump into it, unpack the files archive into the odoo_data volume, then start everything.

Upgrades

There are two kinds of upgrade, and they are not equally easy.

Updates within a release series (for example the regular rebuilds of the odoo:18.0 image) are routine. Take a backup, then pull and recreate:

cd ~/odoo
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs --tail 20 web

If a module's database schema changed, update it explicitly with sudo docker compose run --rm web odoo -d odoo -u all --stop-after-init before starting the stack again. Read the logs for module errors either way.

Moving to a new major version (this guide pins 18.0 to match the catalog entry; a newer major is already published) is a migration project, not a tag bump. Odoo does not upgrade a database in place between majors; the project and its partners provide upgrade scripts and a paid upgrade service, and custom or third-party modules must be ported first. Rehearse it on a copy of your restored backup. Keep the postgres major version pinned as well; a new PostgreSQL major is a dump-and-restore job.

Troubleshooting

The site returns 502 through Caddy, or never answers. Check sudo docker compose ps: if web is restarting, read sudo docker compose logs web. The usual causes are a wrong database password in odoo.conf (the config was regenerated after the database volume already existed) or a database that was never initialised, which logs database "odoo" does not exist. Re-run the initialisation step above.

odoo.conf permission errors. The container user cannot read the file. Run sudo chown 100:101 ~/odoo/odoo.conf and restart the web service.

Login redirects loop, or links use http://. Odoo does not know it is behind a proxy. Confirm proxy_mode = True is in odoo.conf, and that you are browsing the HTTPS address rather than the loopback port.

Caddy never gets a certificate. Confirm dig +short erp.example.com returns the server's IP and that your provider's network-level firewall, not just ufw, allows inbound 80 and 443. A blocked port 80 silently breaks the ACME challenge.

Odoo is killed or very slow. Run sudo docker stats and dmesg | grep -i oom. An out-of-memory kill means the server is too small for the apps you installed; move up a tier. Worker mode makes the problem visible sooner, because each worker takes its own memory.

Verification + next steps

You're done when https://erp.example.com loads the login page with a valid certificate, you can sign in as admin with a password that is not admin, /web/database/manager reports the manager as disabled, docker compose ps shows every service up after a reboot (restart: always brings them back), and you have restored a backup onto a scratch box at least once.

From there, install the first two or three apps you will really use, import your contacts, and configure outgoing mail and your company details. See the Odoo entry for catalog details and Best VPS for self-hosting for the ranked server picks.

Next steps

How to self-host Odoo →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AFFiNE on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy copyparty on a VPS →How to Deploy Directus on a VPS →How to Deploy Discourse on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Flarum on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghost on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy a GitHub Actions Self-Hosted Runner on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Infisical on a VPS →How to Deploy Invoice Ninja on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Kanboard on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy a Minecraft Server on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy ONLYOFFICE Docs on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Plex Media Server on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy WordPress on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.