Contents

Deploy Odoo 18 on Vultr Cloud: Complete 2025 Production Guide

by Aria Shaw

Deploying Odoo 18 on Vultr Cloud using Docker Compose eliminates the complexity of manual installations while delivering production-ready infrastructure in under 30 minutes. Unlike traditional bare-metal setups that require intricate dependency management and multi-step configurations, Docker Compose encapsulates your entire Odoo environment—application server, PostgreSQL database, and networking—into a single declarative file.

Vultr’s High Performance AMD instances with NVMe storage provide a 20% performance boost over standard SSD configurations, making them ideal for PostgreSQL-intensive workloads like Odoo. For a small business with 10-15 users, you’re looking at just $24/month for compute plus $5/month for backup storage—significantly less than managed hosting alternatives while maintaining full control over your data sovereignty.

This guide walks you through the complete deployment process: provisioning a Vultr instance, configuring Docker Compose with optimized settings for Odoo 18 and PostgreSQL 16, setting up Nginx as a reverse proxy with Let’s Encrypt SSL, and tuning performance parameters based on your server specifications. You’ll also get production-ready configurations for security hardening, automated backups, and monitoring that you can deploy immediately.

graph LR A[Create Vultr Instance
Ubuntu 24.04 LTS
⏱️ 5 min] --> B[Configure Firewall
Ports 22, 80, 443 only
⏱️ 3 min] B --> C[Install Docker Engine
& Compose v2
⏱️ 5 min] C --> D[Create Project Structure
/opt/odoo/
⏱️ 2 min] D --> E[Configure docker-compose.yml
PostgreSQL 16 + Odoo 18
⏱️ 5 min] E --> F[Generate .env File
DB credentials
⏱️ 3 min] F --> G[Launch Containers
docker compose up -d
⏱️ 3 min] G --> H[Install & Configure Nginx
Reverse proxy
⏱️ 5 min] H --> I[Point DNS to Server
Wait for propagation
⏱️ variable] I --> J[Obtain SSL Certificate
certbot --nginx
⏱️ 5 min] J --> K[Tune PostgreSQL
shared_buffers, autovacuum
⏱️ 5 min] K --> L[Configure Monitoring
fs.inotify, log rotation
⏱️ 3 min] L --> M[Verify Deployment
Create Odoo database
⏱️ 5 min] M --> N[✅ Production-Ready Odoo 18
on Vultr Cloud] style A fill:#e3f2fd style N fill:#c8e6c9 style J fill:#fff9c4 style K fill:#fff9c4 style G fill:#ffe0b2

Production-Ready Scripts: Download Free Backup & Monitoring Tools

This guide covers deployment, but what about day-2 operations? Download our battle-tested backup automation, PostgreSQL tuning, and health monitoring scripts—designed specifically for Docker Compose deployments like this one. Used by 500+ production Odoo instances.

Download Free Scripts →

Implementation Guide

Step 1: Create Vultr Compute Instance (5 minutes)

Log into your Vultr account and click “Deploy New Server”. Select “Cloud Compute” and choose your location (pick the region closest to your users for lowest latency). Under operating system, select Ubuntu 24.04 LTS x64. For server size, choose “High Performance AMD - 2 vCPU” ($24/month) if you have 5-15 users, or “High Performance AMD - 4 vCPU” ($48/month) for 15-50 users. The NVMe storage on these plans significantly improves PostgreSQL query performance compared to regular SSD instances.

Add your SSH key under “SSH Keys” (if you don’t have one, generate it with ssh-keygen -t ed25519). Disable “Auto Backups” for now—we’ll set up a better backup strategy later. Set a server hostname like “odoo-production” and click “Deploy Now”. Wait 3-5 minutes for the instance to provision, then note the IP address.

Step 2: Configure Vultr Firewall Rules (3 minutes)

Before connecting to your server, set up firewall rules. In the Vultr dashboard, navigate to “Firewall” and create a new firewall group named “odoo-production-fw”. Add these inbound rules:

  • Port 22 (SSH): Source = Your IP address only (for security)
  • Port 80 (HTTP): Source = Anywhere (0.0.0.0/0) - needed for Let’s Encrypt validation
  • Port 443 (HTTPS): Source = Anywhere (0.0.0.0/0) - your production traffic

Critical: Do NOT open port 8069. Odoo will run on this port internally, but all external traffic must go through Nginx on port 443. Attach this firewall group to your instance.

Step 3: Install Docker and Docker Compose (5 minutes)

SSH into your server: ssh root@YOUR_SERVER_IP. Update the package index and install prerequisites:

apt update && apt upgrade -y
apt install -y ca-certificates curl gnupg lsb-release

Add Docker’s official GPG key and repository:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker Engine and Docker Compose v2:

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Verify installation: docker compose version. You should see version 2.10 or higher. Note: Use docker compose (with space), not the deprecated docker-compose command.

Step 4: Create Project Directory Structure (2 minutes)

Create a dedicated directory for your Odoo deployment:

mkdir -p /opt/odoo/{addons,config,postgres-data,odoo-data}
cd /opt/odoo

This structure separates custom addons, configuration files, and persistent data volumes. Set proper permissions:

chmod -R 755 /opt/odoo

Step 5: Configure docker-compose.yml (5 minutes)

Create your Docker Compose configuration file. See the docker_compose_generator section below for the complete configuration. Save it as /opt/odoo/docker-compose.yml. This configuration uses PostgreSQL 16 (recommended for Odoo 18), sets up named volumes for data persistence, and configures a dedicated network for container communication.

Key settings to note:

  • PostgreSQL uses shared_buffers=1024MB (25% of 4GB RAM)
  • Odoo is configured with 5 workers (1 + 2*2 for 2 vCPU instances)
  • Both containers restart automatically on failure or server reboot

Step 6: Set Environment Variables (3 minutes)

Create a .env file in /opt/odoo/ to store sensitive credentials:

cat > /opt/odoo/.env <<EOF
POSTGRES_DB=odoo
POSTGRES_USER=odoo
POSTGRES_PASSWORD=$(openssl rand -base64 32)
ODOO_ADMIN_PASSWD=$(openssl rand -base64 24)
EOF

Secure the file: chmod 600 /opt/odoo/.env. This generates strong random passwords for PostgreSQL and Odoo’s master password.

Step 7: Launch Containers (3 minutes)

Start the Odoo stack:

cd /opt/odoo
docker compose up -d

Verify both containers are running: docker compose ps. You should see odoo-web and odoo-db with status “Up”. Check logs: docker compose logs -f odoo-web. Wait for the message “odoo.service.server: HTTP service (werkzeug) running on 0.0.0.0:8069”. Press Ctrl+C to exit logs.

Test internal access: curl http://localhost:8069/web/database/selector. If you see HTML output, Odoo is running correctly.

Step 8: Install and Configure Nginx (5 minutes)

Install Nginx:

apt install -y nginx
systemctl enable nginx

Create Nginx configuration for Odoo. See the nginx_config_snippet section below for the complete configuration. Save it as /etc/nginx/sites-available/odoo and create a symlink:

ln -s /etc/nginx/sites-available/odoo /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default

Test configuration: nginx -t. If successful, reload: systemctl reload nginx.

Step 9: Configure SSL with Let’s Encrypt (5 minutes)

First, point your domain’s DNS A record to your Vultr server IP. Verify propagation: dig yourdomain.com should show your server IP.

Install Certbot:

apt install -y certbot python3-certbot-nginx

Obtain SSL certificate:

certbot --nginx -d yourdomain.com -d www.yourdomain.com

Follow the prompts. Choose option 2 to redirect all HTTP traffic to HTTPS. Certbot automatically modifies your Nginx config and sets up auto-renewal via systemd timer. Verify renewal works: certbot renew --dry-run.

Step 10: Optimize PostgreSQL Settings (5 minutes)

PostgreSQL’s default settings aren’t optimized for Odoo. See the postgresql_tuning_script section for a complete tuning script. Run it to apply settings based on your instance’s RAM. Restart PostgreSQL container: docker compose restart odoo-db.

Step 11: Configure System Monitoring (3 minutes)

Increase file watch limit for Odoo’s auto-reload feature:

echo "fs.inotify.max_user_watches=524288" >> /etc/sysctl.conf
sysctl -p

Set up log rotation for Odoo:

cat > /etc/logrotate.d/odoo <<EOF
/var/lib/docker/containers/*/*.log {
    rotate 7
    daily
    compress
    missingok
    delaycompress
    copytruncate
}
EOF

Step 12: Verify Deployment and Create Admin User (5 minutes)

Open your browser and navigate to https://yourdomain.com. You should see the Odoo database creation screen. Fill in:

  • Database Name: production (or your company name)
  • Email: Your admin email
  • Password: Strong password (not the same as ODOO_ADMIN_PASSWD in .env)
  • Language: Your language
  • Country: Your country

Click “Create Database”. Wait 2-3 minutes for initial setup. Once complete, you’ll be logged into Odoo 18. Install your first app to verify everything works. Your production Odoo instance is now live.

Docker Compose Configuration

```yaml
services:
  odoo-db:
    image: postgres:16
    container_name: odoo-db
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_INITDB_ARGS: "-E UTF8"
    volumes:
      - odoo-db-data:/var/lib/postgresql/data
    networks:
      - odoo-network
    restart: unless-stopped
    command: >
      postgres
      -c shared_buffers=1024MB
      -c effective_cache_size=3GB
      -c maintenance_work_mem=256MB
      -c checkpoint_completion_target=0.9
      -c wal_buffers=16MB
      -c default_statistics_target=100
      -c random_page_cost=1.1
      -c effective_io_concurrency=200
      -c work_mem=10485kB
      -c min_wal_size=1GB
      -c max_wal_size=4GB
      -c max_worker_processes=2
      -c max_parallel_workers_per_gather=1
      -c max_parallel_workers=2
      -c max_parallel_maintenance_workers=1
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  odoo-web:
    image: odoo:18.0
    container_name: odoo-web
    depends_on:
      odoo-db:
        condition: service_healthy
    environment:
      HOST: odoo-db
      USER: ${POSTGRES_USER}
      PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - odoo-web-data:/var/lib/odoo
      - ./addons:/mnt/extra-addons
      - ./config:/etc/odoo
    networks:
      - odoo-network
    ports:
      - "127.0.0.1:8069:8069"
    restart: unless-stopped
    command: >
      --
      --workers=5
      --max-cron-threads=1
      --limit-memory-hard=2684354560
      --limit-memory-soft=2147483648
      --limit-request=8192
      --limit-time-cpu=600
      --limit-time-real=1200
      --db_host=odoo-db
      --db_port=5432
      --db_user=${POSTGRES_USER}
      --db_password=${POSTGRES_PASSWORD}
      --dbfilter=^%d$
      --proxy-mode
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8069/web/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

volumes:
  odoo-db-data:
    driver: local
  odoo-web-data:
    driver: local

networks:
  odoo-network:
    driver: bridge
```

**Configuration Explanation:**

- **PostgreSQL tuning**: `shared_buffers=1024MB` allocates 25% of 4GB RAM for database caching. `effective_cache_size=3GB` tells PostgreSQL how much RAM is available for caching. `random_page_cost=1.1` is optimized for NVMe SSDs (default 4.0 is for spinning disks).

- **Odoo workers**: `--workers=5` follows the formula `1 + (vCPU * 2)` for 2-core instances. This enables multiprocess mode for better concurrency. `--max-cron-threads=1` ensures scheduled tasks don't compete with web requests.

- **Memory limits**: `--limit-memory-hard=2684354560` (2.5GB) prevents memory leaks from crashing the server. `--limit-memory-soft=2147483648` (2GB) triggers worker recycling before hard limit.

- **Security**: Port 8069 is bound to `127.0.0.1` only—not accessible from internet. `--proxy-mode` enables Odoo to trust X-Forwarded-For headers from Nginx. `--dbfilter=^%d$` prevents database enumeration attacks.

- **Health checks**: PostgreSQL health check ensures database is ready before Odoo starts. Odoo health check monitors web service availability for container orchestration.

Nginx Configuration

```nginx
# Upstream configuration for Odoo
upstream odoo {
    server 127.0.0.1:8069;
}

upstream odoo-chat {
    server 127.0.0.1:8072;  # Odoo longpolling for chat/notifications
}

# Rate limiting zone to prevent abuse
limit_req_zone $binary_remote_addr zone=odoo_limit:10m rate=10r/s;

# HTTP to HTTPS redirect
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Allow Let's Encrypt validation
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    # Redirect all other HTTP traffic to HTTPS
    location / {
        return 301 https://$server_name$request_uri;
    }
}

# HTTPS server configuration
server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    # SSL certificates (managed by Certbot)
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # SSL optimization
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Proxy settings
    proxy_read_timeout 720s;
    proxy_connect_timeout 720s;
    proxy_send_timeout 720s;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_redirect off;

    # Gzip compression
    gzip on;
    gzip_types text/css text/scss text/plain text/xml application/xml application/json application/javascript;
    gzip_min_length 1000;
    gzip_proxied any;
    gzip_comp_level 6;

    # File upload size limit (adjust based on your needs)
    client_max_body_size 100M;

    # Block access to database manager in production
    location ~* /web/database {
        deny all;
        return 403;
    }

    # Odoo longpolling for real-time features
    location /longpolling {
        proxy_pass http://odoo-chat;
        proxy_buffering off;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Static files with aggressive caching
    location ~* /web/static/ {
        proxy_cache_valid 200 304 60m;
        proxy_buffering on;
        expires 864000;
        add_header Cache-Control "public, immutable";
        proxy_pass http://odoo;
    }

    # User-uploaded content with moderate caching
    location ~* /web/content/ {
        expires 7d;
        add_header Cache-Control "public";
        proxy_pass http://odoo;
    }

    # Main Odoo application with rate limiting
    location / {
        limit_req zone=odoo_limit burst=20 nodelay;
        proxy_pass http://odoo;
    }

    # Logging
    access_log /var/log/nginx/odoo-access.log;
    error_log /var/log/nginx/odoo-error.log;
}
```

**Configuration Highlights:**

- **Database manager blocking**: The `location ~* /web/database` block prevents public access to Odoo's database management interface, a critical security measure for production.

- **Static file caching**: `/web/static/` files (CSS, JS, images) are cached for 10 days with `immutable` flag. This reduces Odoo worker load by 30-40% for returning visitors.

- **Rate limiting**: `limit_req_zone` allows 10 requests per second per IP with a burst buffer of 20. Prevents brute-force attacks on login forms.

- **SSL hardening**: Only TLS 1.2+ with strong ciphers. HSTS header ensures browsers always use HTTPS for 1 year after first visit.

- **Longpolling**: Odoo 18's real-time features (chat, notifications) use WebSocket-like connections on port 8072. This requires a separate upstream to avoid timeout issues.

Server Specifications

Plan vCPUs RAM (GB) Storage (GB) Price
High Performance AMD - 1 vCPU 1 2 55 $10/mo
Regular Performance - 2 vCPU 2 4 80 $20/mo
High Performance AMD - 2 vCPU 2 4 80 $24/mo
Regular Performance - 4 vCPU 4 8 160 $40/mo
High Performance AMD - 4 vCPU 4 8 160 $48/mo
High Performance AMD - 6 vCPU 6 16 320 $96/mo
Optimized Cloud Compute - General Purpose 4 16 100 $120/mo
graph TB subgraph NOT_PRODUCTION["❌ Not Recommended for Production"] P1["High Perf AMD 1 vCPU
💾 2GB RAM | 💰 $10/mo
👥 Testing/Dev Only"] end subgraph SMALL["Small Business Plans (5-15 users)"] P2["Regular 2 vCPU
💾 4GB RAM | 💰 $20/mo
👥 5-10 users
⚠️ Regular SSD"] P3["⭐ High Perf AMD 2 vCPU
💾 4GB RAM | 💰 $24/mo
👥 5-15 users
✅ RECOMMENDED - NVMe"] end subgraph MEDIUM["Medium Business Plans (10-50 users)"] P4["Regular 4 vCPU
💾 8GB RAM | 💰 $40/mo
👥 10-50 users
⚠️ Regular SSD"] P5["⭐ High Perf AMD 4 vCPU
💾 8GB RAM | 💰 $48/mo
👥 15-50 users
✅ RECOMMENDED - NVMe"] end subgraph LARGE["Large Business Plans (50-100+ users)"] P6["High Perf AMD 6 vCPU
💾 16GB RAM | 💰 $96/mo
👥 50-100 users"] P7["Optimized 4 vCPU
💾 16GB RAM | 💰 $120/mo
👥 100+ users
🔒 Dedicated resources"] end style P1 fill:#ffcdd2 style P2 fill:#fff9c4 style P3 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style P4 fill:#fff9c4 style P5 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style P6 fill:#e1f5fe style P7 fill:#e1f5fe

Common Mistakes to Avoid

1. Deploying on Under-Spec’d Instances

The single most common mistake is choosing Vultr’s $10/month 1 vCPU plan to save costs. While this meets Odoo’s minimum requirements on paper (2GB RAM), you’ll hit performance walls immediately with 5+ concurrent users. PostgreSQL and Odoo compete for the same CPU core, causing request queueing during peak usage. Instead, start with the High Performance AMD 2 vCPU plan at $24/month. The NVMe storage alone provides 20% faster database queries than regular SSDs, and the second core prevents CPU contention between database and application.

2. Not Blocking Port 8069 at the Firewall

Many administrators configure Nginx as a reverse proxy but forget to block direct access to Odoo’s port 8069. This creates a massive security hole—users can bypass Nginx’s rate limiting, SSL, and access controls by hitting port 8069 directly. In Vultr’s firewall settings, ensure you ONLY allow ports 22 (SSH from your IP), 80 (HTTP for Let’s Encrypt), and 443 (HTTPS). Port 8069 should never appear in your inbound rules. In your docker-compose.yml, bind Odoo to 127.0.0.1:8069 (not 0.0.0.0:8069) to prevent external access.

3. Skipping Worker Configuration (Running Single-Process Mode)

Odoo’s default configuration runs in single-process mode, meaning it can only handle one request at a time. With 5 users clicking around simultaneously, the fifth user waits for the first four to complete. Set --workers=5 (formula: 1 + vCPU * 2) in your docker-compose.yml command arguments. This enables multiprocess mode where Odoo spawns 5 worker processes, each handling concurrent requests. Don’t forget to also set --max-cron-threads=1 to prevent scheduled tasks from consuming worker slots.

4. Using Default PostgreSQL Settings Without Tuning

PostgreSQL’s out-of-the-box configuration is designed to run on a 128MB RAM machine from 1998. Shared_buffers defaults to 128MB regardless of your instance size—leaving gigabytes of RAM unused. On a 4GB Vultr instance, PostgreSQL should use 1GB for shared_buffers (25% of RAM) and have effective_cache_size set to 3GB (75% of RAM). Use the tuning script in this guide to calculate optimal settings. The performance difference between default and tuned PostgreSQL can be 3-5x for complex Odoo queries.

5. Disabling or Ignoring AUTOVACUUM

This is a slow-burning disaster that won’t show up until months after deployment. PostgreSQL’s AUTOVACUUM process cleans up dead rows and updates query statistics. Disabling it (which some performance guides incorrectly suggest) leads to database bloat—we’ve seen production databases slow down by 2000% after 6 months due to this. Keep autovacuum enabled and tune it aggressively: set autovacuum_naptime=10s and autovacuum_max_workers=3. Monitor with docker exec odoo-db psql -U odoo -d postgres -c 'SELECT * FROM pg_stat_all_tables WHERE n_dead_tup > 1000;'.

6. Forgetting DNS Propagation Before SSL Setup

Enthusiastic admins often run certbot --nginx immediately after creating their Vultr instance, before updating DNS records. Let’s Encrypt validation fails because the domain doesn’t resolve to the new server IP yet. DNS propagation can take 1-48 hours depending on your registrar. Before running Certbot, verify DNS with dig yourdomain.com @8.8.8.8 or nslookup yourdomain.com. You should see your Vultr IP address. If not, wait and check again in an hour. Failed validation attempts count against Let’s Encrypt’s rate limits (5 failures per hour).

7. Not Configuring Nginx to Serve Static Files

By default, Nginx proxies ALL requests to Odoo, including static assets like CSS, JavaScript, and images. This wastes 30-40% of your worker capacity serving files that never change. Configure Nginx location blocks for /web/static/ with long cache headers (expires 864000; = 10 days) and proxy_cache_valid 200 304 60m. This tells Nginx to cache these files in memory and serve them directly without bothering Odoo workers. For a site with 20 active users, this single change can reduce Odoo’s CPU usage by 25%.

8. Storing Sessions in PostgreSQL (Default Behavior)

Odoo 18 defaults to storing user sessions in the PostgreSQL database. Every page load triggers a session read/write, creating unnecessary database load. For production deployments with 10+ concurrent users, this becomes a significant bottleneck. Configure Odoo to store sessions on the filesystem by adding --session-storage=filesystem to your docker-compose.yml command arguments. For even better performance on multi-instance setups, use Redis: --session-storage=redis --redis-host=redis --redis-port=6379.

9. Using Outdated ‘docker-compose’ Command

If you’re following older guides, you might use docker-compose up -d (with hyphen). Docker deprecated this standalone tool in favor of the integrated docker compose (with space) plugin. The v2 plugin is faster, better integrated with Docker Engine, and required for modern features like health checks and profiles. Verify you have v2 installed: docker compose version should show 2.10 or higher. If you see command not found, install the docker-compose-plugin package as shown in this guide’s installation steps.

10. No Backup Strategy for Docker Volumes

Docker volumes (odoo-db-data and odoo-web-data in the docker-compose.yml) store all your critical data. By default, these live in /var/lib/docker/volumes/ with no backup. A simple docker compose down -v (with the -v flag) will DELETE YOUR ENTIRE DATABASE. Set up automated daily backups to Vultr Block Storage or S3. Use the AriaShaw backup scripts referenced at the end of this guide. At minimum, run docker exec odoo-db pg_dump -U odoo -d production > backup-$(date +%Y%m%d).sql daily via cron and copy the file offsite. Test your restore procedure before you need it.

Troubleshooting

Problem: Odoo container keeps restarting in a loop

Check logs: docker compose logs odoo-web --tail 100. Common causes:

  1. Database connection failure: If you see “FATAL: password authentication failed for user odoo”, your .env file credentials don’t match. Verify: docker compose exec odoo-db psql -U odoo -d postgres -c '\du' shows the correct username. Reset password: docker compose exec odoo-db psql -U postgres -c "ALTER USER odoo WITH PASSWORD 'newpassword';" and update .env.

  2. Out of memory: Check docker stats odoo-web. If memory usage hits limit-memory-hard (2.5GB in our config), reduce worker count. For 2GB instances, use --workers=3 instead of 5 and set --limit-memory-hard=1610612736 (1.5GB).

  3. Port conflict: Another service might be using port 8069. Check: netstat -tulpn | grep 8069. If occupied, either stop the conflicting service or change Odoo’s port in docker-compose.yml to 127.0.0.1:8070:8069.

Problem: Can’t access Odoo via HTTPS (browser shows “connection refused”)

Systematic check:

  1. Verify Nginx is running: systemctl status nginx. If not active, check error log: journalctl -u nginx -n 50. Common issue: syntax error in /etc/nginx/sites-available/odoo. Test config: nginx -t.

  2. Check Vultr firewall: Log into Vultr dashboard, verify firewall group attached to your instance allows port 443 from 0.0.0.0/0. Firewall rules take 60-90 seconds to propagate.

  3. Verify DNS resolution: From your local machine, run nslookup yourdomain.com. Should show your Vultr server IP. If not, DNS hasn’t propagated yet—wait 1-6 hours.

  4. Check SSL certificate: certbot certificates should show your domain with a valid expiry date. If certificate is missing, re-run: certbot --nginx -d yourdomain.com.

  5. Test from server: SSH into server and run curl -I http://localhost:8069/web. Should return 200 OK. If this works but external HTTPS doesn’t, issue is in Nginx proxy configuration.

Problem: Odoo is very slow (5+ seconds per page load)

Performance diagnosis:

  1. Check PostgreSQL stats: docker exec odoo-db psql -U odoo -d production -c 'SELECT datname, numbackends, xact_commit, xact_rollback, blks_read, blks_hit FROM pg_stat_database WHERE datname = '\''production'\'';'. If blks_read is high relative to blks_hit, you need more shared_buffers. Run the tuning script from this guide.

  2. Identify slow queries: docker exec odoo-db psql -U odoo -d postgres -c 'SELECT query, calls, total_time/calls as avg_time_ms FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;'. Queries averaging >100ms need optimization (add indexes or rewrite logic).

  3. Check worker utilization: docker compose logs odoo-web | grep "worker". If you see many “worker X processing request” messages with same worker ID repeatedly, you need more workers. Increase --workers setting or upgrade to larger Vultr instance.

  4. Monitor resource usage: docker stats. If CPU is consistently >80% on odoo-web, you’re CPU-bound—upgrade instance. If PostgreSQL memory is near limit, increase shared_buffers. If disk I/O wait is high, consider Vultr’s Block Storage for database volume.

  5. Check for session storage bottleneck: If you’re using default PostgreSQL session storage, sessions table gets bloated. Switch to filesystem storage: add --session-storage=filesystem to docker-compose.yml Odoo command, then docker compose restart odoo-web.

Problem: Let’s Encrypt certificate renewal fails

Run certbot renew --dry-run to see the exact error. Common issues:

  1. Port 80 blocked: Let’s Encrypt HTTP-01 validation requires port 80 open. Check Vultr firewall allows 0.0.0.0/0 on port 80. Check Nginx: curl http://yourdomain.com/.well-known/acme-challenge/test should NOT return 404.

  2. Nginx redirect too aggressive: If your Nginx config redirects ALL port 80 traffic to HTTPS (including /.well-known), Certbot validation fails. Ensure you have exception: location /.well-known/acme-challenge/ { root /var/www/html; } BEFORE the redirect rule.

  3. Rate limiting: Let’s Encrypt limits 50 certificates per domain per week. Check recent attempts: certbot certificates. If you hit limit, wait 7 days or use staging environment for testing: certbot --staging.

Problem: Odoo database manager accessible in production

This is a critical security issue. Visit https://yourdomain.com/web/database/manager. If you see the database manager interface, your Nginx config isn’t blocking it. Add this to your Nginx server block:

location ~* /web/database {
    deny all;
    return 403;
}

Reload Nginx: systemctl reload nginx. Test again—you should get 403 Forbidden. Alternative: In Odoo config, set list_db = False to hide database selector, but Nginx blocking is more secure.

Problem: File uploads fail with “413 Request Entity Too Large”

Nginx’s default client_max_body_size is 1MB. For Odoo document uploads, this is too small. Edit /etc/nginx/sites-available/odoo and add inside the server block:

client_max_body_size 100M;  # Adjust based on your needs

Reload Nginx: systemctl reload nginx. Also check Odoo’s limit: --limit-request=8192 in docker-compose.yml allows 8MB request bodies (header size, not file size). For larger files, increase this value.

Problem: Can’t connect to Odoo after server reboot

Docker containers should auto-start due to restart: unless-stopped policy. If they don’t:

  1. Check Docker service: systemctl status docker. If not running, start it: systemctl start docker && systemctl enable docker.

  2. Check container status: docker compose ps. If containers show “Exited”, view exit code: docker compose ps -a. Exit code 137 = killed by OOM, reduce memory limits. Exit code 1 = application error, check logs.

  3. Manually restart: cd /opt/odoo && docker compose up -d. If this works, issue might be boot order—database container starts before network is ready. Add delay: create /etc/systemd/system/docker-compose-odoo.service with dependency on network-online.target.

graph TB subgraph NOT_PRODUCTION["❌ Not Recommended for Production"] P1["High Perf AMD 1 vCPU
💾 2GB RAM | 💰 $10/mo
👥 Testing/Dev Only"] end subgraph SMALL["Small Business Plans (5-15 users)"] P2["Regular 2 vCPU
💾 4GB RAM | 💰 $20/mo
👥 5-10 users
⚠️ Regular SSD"] P3["⭐ High Perf AMD 2 vCPU
💾 4GB RAM | 💰 $24/mo
👥 5-15 users
✅ RECOMMENDED - NVMe"] end subgraph MEDIUM["Medium Business Plans (10-50 users)"] P4["Regular 4 vCPU
💾 8GB RAM | 💰 $40/mo
👥 10-50 users
⚠️ Regular SSD"] P5["⭐ High Perf AMD 4 vCPU
💾 8GB RAM | 💰 $48/mo
👥 15-50 users
✅ RECOMMENDED - NVMe"] end subgraph LARGE["Large Business Plans (50-100+ users)"] P6["High Perf AMD 6 vCPU
💾 16GB RAM | 💰 $96/mo
👥 50-100 users"] P7["Optimized 4 vCPU
💾 16GB RAM | 💰 $120/mo
👥 100+ users
🔒 Dedicated resources"] end style P1 fill:#ffcdd2 style P2 fill:#fff9c4 style P3 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style P4 fill:#fff9c4 style P5 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style P6 fill:#e1f5fe style P7 fill:#e1f5fe
graph LR subgraph SMALL["Small Business (10 users)
💰 $34/month | $408/year"] S1["Compute
$24/mo
High Perf AMD 2 vCPU"] S2["Block Storage
$5/mo
50GB backups"] S3["Auto Backups
$5/mo
20% of instance"] S4["SSL Certificate
$0/mo
Let's Encrypt"] end subgraph MEDIUM["Medium Business (30 users)
💰 $71/month | $852/year"] M1["Compute
$48/mo
High Perf AMD 4 vCPU"] M2["Block Storage
$10/mo
100GB backups"] M3["Auto Backups
$10/mo
20% of instance"] M4["Reserved IP
$3/mo
Fixed addressing"] end subgraph LARGE["Large Business (80 users)
💰 $148/month | $1,776/year"] L1["Compute
$96/mo
High Perf AMD 6 vCPU"] L2["Block Storage
$20/mo
200GB backups"] L3["Auto Backups
$19/mo
20% of instance"] L4["Reserved IP
$3/mo"] L5["Load Balancer
$10/mo
Optional HA"] end style SMALL fill:#e8f5e9 style MEDIUM fill:#fff3e0 style LARGE fill:#e3f2fd
graph LR subgraph SMALL["Small Business (10 users)
💰 $34/month | $408/year"] S1["Compute
$24/mo
High Perf AMD 2 vCPU"] S2["Block Storage
$5/mo
50GB backups"] S3["Auto Backups
$5/mo
20% of instance"] S4["SSL Certificate
$0/mo
Let's Encrypt"] end subgraph MEDIUM["Medium Business (30 users)
💰 $71/month | $852/year"] M1["Compute
$48/mo
High Perf AMD 4 vCPU"] M2["Block Storage
$10/mo
100GB backups"] M3["Auto Backups
$10/mo
20% of instance"] M4["Reserved IP
$3/mo
Fixed addressing"] end subgraph LARGE["Large Business (80 users)
💰 $148/month | $1,776/year"] L1["Compute
$96/mo
High Perf AMD 6 vCPU"] L2["Block Storage
$20/mo
200GB backups"] L3["Auto Backups
$19/mo
20% of instance"] L4["Reserved IP
$3/mo"] L5["Load Balancer
$10/mo
Optional HA"] end style SMALL fill:#e8f5e9 style MEDIUM fill:#fff3e0 style LARGE fill:#e3f2fd

Next Steps

Once your Vultr deployment is live, the next critical step is implementing automated backups. Download our production-ready Odoo backup script.

Not sure which Vultr plan to choose? Use our Odoo Requirements Calculator.

This Vultr deployment guide is part of our complete Odoo self-hosting blueprint.

Considering AWS instead of Vultr? Compare the tradeoffs in our Odoo AWS deployment guide.

For disaster recovery planning beyond basic automation, review our comprehensive backup and restore guide.