Self Hosted Odoo 18 on Ubuntu 24.04 using docker containers (nginx, postgresql, certbot and odoo)

How to self-host your personal Odoo 18 instance on Ubuntu 24.04 using docker containers
May 26, 2025 by
Self Hosted Odoo 18 on Ubuntu 24.04 using docker containers (nginx, postgresql, certbot and odoo)
Alixsander Haj Saw
| No comments yet

This guide walks you through setting up an Odoo instance using Docker, complete with PostgreSQL as the database backend, Nginx as a reverse proxy, and Certbot to issue a free SSL certificate. This setup ensures a secure and modular deployment.


Prerequisites

  • An Ubuntu 24.04 server 
  • A domain name pointing to your Ubuntu server 
  • Internet access and hustle


Install Docker

Follow the official docker documentation found here, which outlines the following docker installation using apt:

sudo apt-get update
sudo apt-get install ca-certificates curl
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

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

After adding the repository install docker and docker compose by running:

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin


Create the necessary directories

To keep things organized, I suggest creating a docker directory in users home directory, and the domain name within it. This is good as you can have multiple docker projects within on server:

cd

mkdir -p docker/example.com

cd docker/example.com

The following directories will be used as volumes and for containers to share data between each other, for example certbot container and nginx container must share the webroot to generate certificates successfully 

mkdir -p nginx/{conf,ssl,inc} config addons


Creating docker compose file

Inside your docker project create the compose.yml file:

nano compose.yml

 Then add the following contents to it:

services:
  db:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=odoo
      - POSTGRES_PASSWORD=odoo
      - PGDATA=/var/lib/postgresql/data/pgdata
    volumes:
      - odoo-db-data:/var/lib/postgresql/data

  odoo:
    image: odoo:18.0
    depends_on:
      - db
    ports:
      - "8069:8069"
    volumes:
      - odoo-web-data:/var/lib/odoo
      - ./config:/etc/odoo
      - ./addons:/mnt/extra-addons
    command: odoo -d odoo_db -i base --db_user=odoo --db_password=odoo --db_host=db

  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
- ./nginx/inc:/etc/nginx/inc
      - ./nginx/ssl:/etc/nginx/ssl
      - ./nginx/certbot/www:/var/www/certbot
      - ./nginx/certbot/conf:/etc/letsencrypt
    depends_on:
      - odoo

  certbot:
    image: certbot/certbot
    volumes:
      - ./nginx/certbot/www:/var/www/certbot
      - ./nginx/certbot/conf:/etc/letsencrypt

volumes:
  odoo-db-data:
  odoo-web-data:

In the compose.yml file we include 4 containers, a db container with postgres version 16, an Odoo container with version 18, an nginx container with the latest version and a certbot container.

Here is a detailed description for each service:

db

Creates a postgresql container for our Odoo application to store data in.

  • image: specify the image name in docker hub, in our case postgres version 16
  • Environment variables are used to create the database name, database user and the users password, the same values will be used by our Odoo container to connect to the database.
  • The odoo-db-data volume stores postgresql data on the host machine to remain persistent.


odoo

Creates our Odoo container and connects it to our postgresql database using the variables created in the db container.

  • image: odoo image from docker hub with version 18
  • depends on option ensures that the db service is running before odoo starts.
  • ports option maps port 8069 on the host to the containers port 8069.
  • 4 volumes are created
    • odoo-web-data to store Odoo data 
    • ./config to provide Odoo with the config file
    • ./addons to provide Odoo with custom addons
    • ./logs to store Odoo logs and make them accessible on the host machine
  • Command provides Odoo with the previously created postgresql variables to initialize and connect to the database.


nginx

Used to serve traffic to the Odoo container, and enables secure HTTPS traffic by generating SSL certificates in conjunction with certbot.

  • image: provides the latest nginx image on docker hub.
  • Ports maps 2 ports between the host and container, 80 for HTTP and 443 for HTTPS.
  • 4 volumes are created:
    • ./nginx/conf directory is used to add nginx configs.
    • ./nginx/inc directory is used to include supplementary nginx configurations.
    • ./nginx/ssl directory used to stores SSL certs and DH params.
    • ./nginx/certbot/www webroot used by both Certbot and NGINX for ACME challenge.
    • ./nginx/certbot/conf stores Certbot config and certs.
  • Depends on makes sure Odoo container is created before creating the nginx container.


cerbot

Used to generate Let's Encrypt certificates and renew them.

  • image: provides the certbot image on docker hub.
  • 2 volumes are created:
    • ./nginx/certbot/www shares the webroot directory with Nginx for successful ACME challenge.
    • ./nginx/certbot/conf shares certificate files with Nginx.
  • Depends on makes sure Odoo container is created before creating the nginx container.


Initial Nginx configuration

For nginx and certbot to work we'll need to allow TCP ports 80 and 443 inside UFW (uncomplicated firewall):

sudo ufw allow http
sudo ufw allow https
sudo ufw status

Now we can continue with our nginx setup by creating the following configuration file:

nano nginx/conf/example.com.conf

and add the following directives to it, make sure to use your domain name:

server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

The above directive will enable certbot to solve the ACME challenge and create the SSL certificates.

Start the db, odoo and nginx containers by running:

sudo docker compose up -d db odoo nginx


Add gzip compression

Gzip is a compression method that reduces the size of HTTP responses by encoding content like HTML, CSS, and JavaScript.

Create the following gzip.conf file:

nano nginx/inc/gzip.conf

And add the following to it:

# Gzip configuration
gzip on;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_proxied any;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;


Generate SSL certificates and create SSL configurations

Use the following command to generate certificates using certbot. Make sure to your domain is used after the -d flag and replace user@gmail.com with your email.

sudo docker compose run --rm certbot certonly --webroot --webroot-path=/var/www/certbot -d example.com --email user@gmail.com --agree-tos --no-eff-email

Next generate your own DH Parameter file, this would further secure your cryptographic key exchange.

openssl dhparam -out nginx/ssl/dhparam.pem 2048

Next create the following file: 

nano nginx/ssl/ssl.conf

and add the following directives to it and replace yourdomain.com with your domain:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;

ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_dhparam "/etc/nginx/ssl/dhparam.pem";
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 1.0.0.1 valid=300s;


Final nginx configuration over https

Now open our nginx configuration file we created earlier:

nano nginx/conf/example.com.conf

and replace the contents with the following to serve traffic securly over https, make sure your domain name is used after server_name, and inside ssl_certificate and ssl_certificate_key directives:

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    # Include Mozilla's SSL settings
    include /etc/nginx/ssl/ssl.conf;

    # Certificate paths (ensure these match your mounted volume)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    access_log /var/log/nginx/odoo_access.log;
    error_log  /var/log/nginx/odoo_error.log;

    #Gzip
    include /etc/nginx/inc/gzip.conf;

    # Proxy settings for Odoo
    location / {
        proxy_pass http://odoo:8069;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Host $host;
        proxy_redirect off;
        proxy_request_buffering off;
        # Recommended timeouts for standard requests
        proxy_connect_timeout 30s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
        send_timeout          60s;
    }

    # Cache static files
    location ~* /web/static/ {
        proxy_cache_valid 200 60m;
        proxy_buffering on;
        expires 864000;
        proxy_pass http://odoo:8069;
    }

    # Increase timeouts for long polling
    location /longpolling {
        proxy_pass http://odoo:8069;
        proxy_connect_timeout 60s;
        proxy_send_timeout    300s;
        proxy_read_timeout    300s;
        send_timeout          300s;
    }

    location /websocket {
        proxy_pass http://odoo:8069;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name example.com;

    # This allows Certbot to access the challenge URL
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

In addition to the SSL configurations and Gzip compression this includes necessary security headers such as X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Strict-Transport-Security

Restart nginx to take in the new configuration:

sudo docker compose restart nginx

Now we'll add a cronjob to handle certificate renewal, open crontab using:

sudo crontab -e

Now add the following line at the bottom of the crontab and replace /path/to/your/compose.yml with your actual path to the compose.yml file:

0 8 * * 0 docker compose -f /path/to/your/compose.yml run --rm certbot renew && docker compose -f /path/to/your/compose.yml exec nginx nginx -s reload

This cronjob will run 8am every Sunday, which would renew the certificate if it is expiring in less than 30 days. The second part of this command reloads nginx to take in the new certificate.


Adding Odoo config options

You can add various Odoo related configurations by creating the following file and adding them to it:

nano config/odoo.conf

Here are some helpful configurations, list_db will hide database options from the login page, proxy_mode will let Odoo know that it is served behind a reverse proxy, and if you are planning to install custom addons, then uncomment addons_path and add your custom addons inside the addons directory we created in the beginning:

[options]
list_db = False
proxy_mode = True
#addons_path = /mnt/extra-addons

Save the file and restart odoo:

sudo docker compose restart odoo

Check if the database management option is removed from the login page.


Accessing the website

Now you should be able to access your website securely by visiting your domain. The default user/pass are admin/admin.

You can run couple of scans on your website to check your certificate rating and security rating based on security headers.

For certificate rating:
https://www.ssllabs.com/ssltest

For security rating based on security headers:
https://securityheaders.com/


Sign in to leave a comment