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
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/