Install and Deploy Odoo 14 from source on DigitalOcean
Tags: odoo, odoo14, ubuntu20, deployment
Published: 2021-04-19 11:44:05

---
title: Install and Deploy Odoo 14 from source on DigitalOcean
published: true
date: 2021-04-04 07:56:09 UTC
tags: Odoo,Odoo14,Ubuntu20,Deployment
canonical_url: https://codingdodo.com/install-and-deploy-odoo-14-from-source-on-digitalocean/
---

 ![Install and Deploy Odoo 14 from source on DigitalOcean](https://codingdodo.com/content/images/2021/04/post-cover-test-3.png)

In this tutorial, we will deploy a production-ready Odoo 14 instance. Install it from GitHub source on a brand new [DigitalOcean](https://m.do.co/c/afb7558193a6) Droplet Ubuntu 20.04 (LTS) x64. What we will do:

- Going through DigitalOcean registration
- Installing all the Ubuntu 20.4 requirements necessary to install Odoo from the source
- Using Pyenv to prepare our server for multiple python versions and virtualenvs in case you want to host other versions of Odoo
- Creating `odoo` user and making Odoo run as a service
- Install Let's Encrypt SSL Certificate
- Making Odoo multiprocess and using Nginx as a reverse proxy
- Basic security with ufw

[DigitalOcean](https://m.do.co/c/afb7558193a6) offers you 100€ credit to use in 6 months since you can create machines and destroy them quickly it's a great place to experiment with Odoo installs or to choose it as your production server!

## Creating a DigitalOcean account

First of all, go to [DigitalOcean](https://m.do.co/c/afb7558193a6) and create an account using the link

![Install and Deploy Odoo 14 from source on DigitalOcean](https://codingdodo.com/content/images/2021/04/digital-ocean-1-1.png)<figcaption>DigitalOcean Register</figcaption>

Create your credentials, validate your email and follow the steps until you arrive on that page

![Install and Deploy Odoo 14 from source on DigitalOcean](https://codingdodo.com/content/images/2021/04/digital-ocean-3-3.png)<figcaption>DigitalOcean Register Choose Control panel</figcaption>

Here it can get a bit confusing because most of the links direct you to the DigitalOcean platform but we want the regular dashboard so click on **Explore our control panel**

When on the Control Panel click on **Get Started with a Droplet** or **Create / Droplet** to arrive on this screen

![Install and Deploy Odoo 14 from source on DigitalOcean](https://codingdodo.com/content/images/2021/04/digital-ocean-5-1.png)<figcaption>Create Ubuntu Droplet</figcaption>

Choose Ubuntu 20.04 and choose the right machine based on your needs. Remember that you are only billed for the time that the machine is up so you can choose a good machine and destroy it later. Choose at least **2 CPUs** or everything will feel very slow.

Scrolling down you will see a space to add your ssh-key

![Install and Deploy Odoo 14 from source on DigitalOcean](https://codingdodo.com/content/images/2021/04/digital-ocean-6-1.png)<figcaption>Adding the SSH KEY</figcaption>

### Adding SSH Key

If you don't have an SSH Key yet you have to generate it with the command. You probably already have an `id_rsa.pub` file in your `.ssh` folder. For this example, we will create a new SSH key that will be used to connect to our DigitalOcean Droplets. Feel free to skip that part or read it to create your first key or handle multiple ssh keys.

```shell
ssh-keygen -C "contact@codingdodo.com"
```

When prompted for the name of the file we choose that

```shell
/Users/codingdodo/.ssh/id_rsa_codingdodo
```

Show the content of the newly generated key:

```shell
cat ~/.ssh/id_rsa_codingdodo.pub
```

On DigitalOcean, click on **New SSH Key** and copy/paste the content printed out. Scroll down more and click on **Create Droplet.** It will take a minute then your Droplet will be ready

![Install and Deploy Odoo 14 from source on DigitalOcean](https://codingdodo.com/content/images/2021/04/digital-ocean-7-1.png)<figcaption>Droplet is Ready</figcaption>

**Copy the IP Address** , we will add it to our SSH config file to make things easier for us. If you handle multiple SSH Keys this is a nice way to handle with which SSH Key you connect to each server.

```shell
vim ~/.ssh/config
```

Add this section with your IP address and the name of the public key you created.

```config
 Host testingdodo
   Hostname 143.198.123.151
   User root
   IdentityFile ~/.ssh/id_rsa_codingdodo
   IdentitiesOnly yes
```

We name our Host `testingdodo` so in our shell, we can type this shortcut to ssh into the machine.

```shell
ssh testingdodo
```

And answer `yes` to the security prompt, and you will be logged in

```shellsession
Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-51-generic x86_64)

...

root@ubuntu-s-1vcpu-1gb-intel-nyc1-01:~#
```

## Ubuntu 20.4 Prerequesites

We start by updating our packages

```shell
apt update
```

### Installing PostgreSQL

```shell
apt install postgresql postgresql-contrib
```

We will later create a Unix user named Odoo, so we will create a PostgreSQL user with the same name. If you would prefer a different name, remember to keep that consistency to make things easier when you come back later to your environment.

```shell
su - postgres
createuser --interactive -P odoo
```

With the `--interactive` flag, PostgreSQL will ask us for a password and the privileges that will be given to our odoo PostgreSQL user.

```shellsession
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) y
Shall the new role be allowed to create more new roles? (y/n) n
```

Next, we will already create that database that will hold our Odoo 14 instance. In this example, it's gonna be named `coding_dodo`.

```shell
createdb -O odoo codingdodo_demo
exit
```

The `-O` flag represents the owner of the database, we choose `odoo` because it's the name of the PostgreSQL user we created just before

### Wkhtmltopdf

Wkhtmltopdf is used by Odoo to generate documents and is a necessary evil. We will pull the deb of version **0.12.6-2** from Github and install it

```shell
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.bionic_amd64.deb
apt install ./wkhtmltox_0.12.6-1.bionic_amd64.deb
```

### Python dev dependencies, git, Node, and other requirements

```shell
apt install -y build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
xz-utils tk-dev libffi-dev liblzma-dev python-openssl git libpq-dev libsasl2-dev libldap2-dev ccze node-less
```

#### Creating the Unix odoo user

```shell
useradd -m -U -r -s /bin/bash odoo
```

`-r` indicate that it's a system account so useradd will not create a home directory for such a user. We want a folder so we use `-m`  
`-U` create a group and `-s` specify the shell that will be used.

Or if you don't want to have `/home/odoo` created to hold your odoo install and would prefer a different directory. This is an example that will create the home directory in `/opt` for example.

```shell
useradd -m -d /opt/odoo -U -r -s /bin/bash odoo
```

_-d [path] indicates we want to create a home but in different place._

## Preparing a good Python environment with Pyenv

### Log in as the odoo user

The pyenv will be installed as the odoo system user. That user is gonna run the odoo instance as a service later.

```shell
su - odoo
```

### Installing pyenv

```shell
curl https://pyenv.run | bash
echo 'export PATH="/home/odoo/.pyenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc
exec $SHELL
```

### Installing python 3.9.2 

```shell
pyenv install 3.9.2
```

Depending on the CPU power you choose, it may take some time, but in the end, you should see:

```shell
Installed Python-3.9.2 to /home/odoo/.pyenv/versions/3.9.2
```

### Creating the virtualenv

```shell
pyenv virtualenv 3.9.2 odoo-14-env
```

## Installing Odoo from GitHub source

### Creating a folder for our custom addons (Optional)

```shell
mkdir /home/odoo/odoo-14-custom-addons
```

This is the folder you will use to store your custom addons. Be careful if you add the custom addon patch to the `--addons-path` command-line argument and there are no valid addons inside, it will not work.

To make it work we will clone our own module inside that folder

```shell
cd /home/odoo/odoo-14-custom-addons
git clone https://github.com/Coding-Dodo/web_widget_markdown.git
cd ~/
```

### Pulling Odoo 14 and activating the virtualenv

We will pull Odoo version 14 with the `-b` flag for the branch and put it in a folder named `odoo-14`

```shell
git clone -b 14.0 --single-branch --depth 1 https://github.com/odoo/odoo.git odoo-14
```

Since we created our virtualenv called `odoo-14-env`, we will "park" it in the `odoo-14` folder we just created. With that every time we `cd` into this folder it will **activate** our virtualenv.

```shell
cd odoo-14
pyenv local odoo-14-env
```

The prompt should change and you should see `(odoo-14-env) odoo@ubuntu-s-1vcpu-1gb-intel-nyc1-01`

### Installing python dependencies

```shell
pip install --upgrade pip
pip install setuptools wheel
pip install -r requirements.txt -e .
```

If you see any error please refer to the part "Python dev dependencies, git, Node and other requirements" and make sure you installed everything.

### Launching Odoo for the first time to test and generate a config file

We will test launch Odoo with some command-line arguments that will be saved in our Odoo configuration file

```shell
./odoo-bin --database=codingdodo_demo --db_user=odoo --db_password=codingdodo -i base --without-demo=all --save -c /home/odoo/.odoorc_codingdodo_demo --stop-after-init
```
<!--kg-card-begin: html-->
 If you chose to create a custom addons folder and it's not empty. The command will be
```

./odoo-bin --database=codingdodo_demo --db_user=odoo --db_password=codingdodo -i base --addons-path="/home/odoo/odoo-14/addons,/home/odoo/odoo-14-custom-addons" --without-demo=all --save -c /home/odoo/.odoorc_codingdodo_demo --stop-after-init

```

<!--kg-card-end: html-->

We directly typed the db name, user, and password and initialized it with the `i`flag.   
`--without-demo=all` is used because we are installing a production-ready environment. If you want demo data, omit that flag.  
With the `-c` flag we told odoo where the config file will be  `/home/odoo/.odoorc_codingdodo_demo`  
The `--save` flag is used to save everything we just typed **into** the newly created config file.

### (Optional) Updating config file admin\_passwd with a random strong password:

If you want to quickly change the password inside your config file

```bash
sed -i "s/\badmin\b/$(openssl rand -base64 32 | awk '{print $1}')/g" /home/odoo/.odoorc_codingdodo_demo
```

This will replace the line `admin_passwd = admin` with a new password randomly generated via OpenSSL.

To check what the new `admin_passwd` is:

```shell
cat /home/odoo/.odoorc_codingdodo_demo | grep admin_passwd

...
admin_passwd = C4Yiyiez4CHMl5tsmRHVZHflfnITd3nF5oXh8l3ZPc8=
```

Keep it somewhere safe because after running the instance it will be hashed.

### Create the logs directory 

We want our log files to be inside `/var/log/odoo/` so we create that folder and give odoo user access to it.

```shell
mkdir /var/log/odoo
touch /var/log/odoo/odoo-14.log
chown odoo: /var/log/odoo
chown -R odoo: /var/log/odoo/*

vim /home/odoo/.odoorc_codingdodo_demo
```

Modify the config file to reflect that change

```shell
logfile = /var/log/odoo/odoo-14.log
```

## Odoo as service

Log out of odoo user and create a service file

```shell
exit
vim /etc/systemd/system/odoo-14.service
```

Inside the file

```bash
[Unit]
Description=Odoo14
Requires=postgresql.service
After=network.target postgresql.service

[Service]
Type=simple
SyslogIdentifier=odoo-14
PermissionsStartOnly=true
User=odoo
Group=odoo
ExecStart=/home/odoo/.pyenv/versions/odoo-14-env/bin/python /home/odoo/odoo-14/odoo-bin -c /home/odoo/.odoorc_codingdodo_demo
StandardOutput=journal+console

[Install]
WantedBy=multi-user.target
```

Reload service

```shell
systemctl daemon-reload
systemctl enable --now odoo-14
```

Check status:

```shell
systemctl status odoo-14
```

## Install Nginx and add SSL with Let's Encrypt

### Installing Nginx 

```shell
apt install nginx
```

### Let's Encrypt with Certbot

[Certbot](https://certbot.eff.org/) will be used to install our first certificate and to renew it every months

```shell
apt install certbot
```

Generate a new set of 2048 bit DH parameters by typing the following command:

```shell
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
```

### Let's Encrypt Nginx Snippet

Let's Encrypt need to do its acme-challenge to validate our domain name and creating the certificate with certbot. If we plan to install multiple instances / different domain names pointing to this server, it is a good habit to isolate the let's encrypt acme-challenge location to **its custom snippet.**

```shell
mkdir -p /var/lib/letsencrypt/.well-known
chgrp www-data /var/lib/letsencrypt
chmod g+s /var/lib/letsencrypt
```

We create a Let's Encrypt snippet

```shell
vim /etc/nginx/snippets/letsencrypt.conf
```

With this content

```nginx
location ^~ /.well-known/acme-challenge/ {
  allow all;
  root /var/lib/letsencrypt/;
  default_type "text/plain";
  try_files $uri =404;
}
```

### SSL Conf Nginx Snippet

```shell
vim /etc/nginx/snippets/ssl.conf
```

Copy-paste that content

```nginx
ssl_dhparam /etc/ssl/certs/dhparam.pem;

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 30s;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

```

### Create A Record with your DNS Provider pointing to the Droplet IP

For the following parts, you must have a domain name that will point to your droplet or else the acme-challenge of Let's Encrypt will not pass and you will not have SSL so the final Nginx config file will not apply to you.

![Install and Deploy Odoo 14 from source on DigitalOcean](https://codingdodo.com/content/images/2021/04/domain-name-1-1.png)<figcaption>Adding a domain name</figcaption>

Here we created a subdomain of codingdodo.com with an **A record** pointing to our Droplet IP Value.

Our final address will be `odoo-14.codingdodo.com`

### Installing the Let's Encrypt certificate

In case the Odoo service is still running.

```shell
service odoo-14 stop
```

#### Basic Nginx Conf file to pass the Certbot acme-challenge

```shell
cd /etc/nginx/site-available
vim odoo-14.codingdodo.com
```

We will first create a basic Nginx conf file to pass certbot acme-challenge:

```nginx
upstream odoo_14 {
 server 127.0.0.1:8069;
}

upstream odoochat_14 {
 server 127.0.0.1:8072;
}

server {
    listen 80;
    listen [::]:80;
    server_name odoo-14.codingdodo.com;
    include snippets/letsencrypt.conf;
}
```

Symlink your site declaration from site-available to site-enabled.

```shell
ln -s /etc/nginx/sites-available/odoo-14.codingdodo.com /etc/nginx/sites-enabled/odoo-14.codingdodo.com
```

Always test the config with `nginx -t` and if everything is okay we reload Nginx

```shell
service nginx reload
```

### Certbot acme-challenge

It is now time to create our SSL certificate.

```shell
certbot certonly --agree-tos --email contact@codingdodo.com --webroot -w /var/lib/letsencrypt/ -d odoo-14.codingdodo.com
```

You should see

```shell
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/odoo-14.codingdodo.com/fullchain.pem
```

### Managing Auto renew

Let's Encrypt already created a cron in the crontab but Nginx needs to reload to take into consideration the new certificate so we will edit this file

```shell
vim /etc/letsencrypt/cli.ini
```

And add the line at the end.

```shell
deploy-hook = systemctl reload nginx
```

To test if our renew will work we can use the `--dry-run` flag to test it

```shell
certbot renew --dry-run
```

## Final Nginx site conf for proxy mode multi workers

Now edit the odoo-14.codingdodo.com file again:

```shell
vim /etc/nginx/sites-available/odoo-14.codingdodo.com
```

```nginx
upstream odoo_14 {
 server 127.0.0.1:8069;
}

upstream odoochat_14 {
 server 127.0.0.1:8072;
}

server {
    listen 80;
    listen [::]:80;
    server_name odoo-14.codingdodo.com;
	include snippets/letsencrypt.conf;
    
    location / {
        return 301 https://odoo-14.codingdodo.com$request_uri;
    }
}

server {
    listen 443 ssl http2 default_server;
    listen [::]:443;
    server_name odoo-14.codingdodo.com ;

    ssl_certificate /etc/letsencrypt/live/odoo-14.codingdodo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/odoo-14.codingdodo.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/odoo-14.codingdodo.com/chain.pem;
	include snippets/ssl.conf;
	include snippets/letsencrypt.conf;

    proxy_buffers 16 64k;
    proxy_buffer_size 128k;

    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_set_header X_ODOO_DBFILTER "codingdodo_demo";
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;

    location / {
        proxy_pass http://odoo_14;
    }

    location /longpolling {
        proxy_pass http://odoochat_14;
    }

    location ~* /web/static/ {
        proxy_cache_valid 200 60m;
        proxy_buffering on;
        expires 864000;
        proxy_pass http://odoo_14;
    }

    location ~* /website/image/ir.attachment/ {
        proxy_cache_valid 200 60m;
        proxy_buffering on;
        expires 864000;
        proxy_pass http://odoo_14;
    }

    gzip_types text/css text/less text/plain text/xml application/xml application/json application/javascript;
    gzip on;
}

```

Use `nginx -t` to test the new config and if everything is okay run

```
service nginx reload
```

### Updating the Odoo config file

```shell
vim /home/odoo/.odoorc_codingdodo_demo
```

Now that we have our reverse proxy we need to modify/add these lines

```bash
proxy_mode = True
workers = 4
max_cron_threads = 1
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_request = 8192
limit_time_cpu = 600
limit_time_real = 1200
```

The number of workers depends on the RAM and CPU you choose during the creation of your droplet. Refer to this [official documentation](https://www.odoo.com/documentation/14.0/setup/deploy.html#id5) to calculate the appropriate number of workers.

- Server with 2 CPU, 4 Thread
- 30 concurrent users
- 30 users / 6 = 5 <- theorical number of worker needed
- (2 \* 2) + 1 = 5 <- theorical maximal number of worker
- We’ll use 4 workers + 1 for cron. 
- RAM = 9 \* ((0.8\*150) + (0.2\*1024)) ~= 3Go RAM for Odoo

### Restart Odoo and see logs

```shell
service odoo-14 restart
```

Go to https://odoo-14.codingdodo.com and in the meantime on the server check the logs

```shell
tail -f /var/log/odoo/odoo-14.log | ccze
```

## Setting the Firewall

```shell
ufw app list
```

If you followed this guide it should show you :

```shell
Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH
```

First, we secure ssh connection

```shell
ufw allow OpenSSH
```

Now if you have installed Nginx with an SSL certificate

```shell
ufw allow 'Nginx Full'
ufw enable
```

## Create a sudo user for day to day actions

```shell
adduser codingdodo

usermod -aG sudo codingdodo

rsync --archive --chown=codingdodo:codingdodo ~/.ssh /home/codingdodo
```

With DigitalOcean we already uploaded our ssh-key to the authorized\_keys of the root so we are using Rsync to copy the folder with a new owner.

## Wrapping up

This is the final Odoo config file

```bash
[options]
addons_path = /home/odoo/odoo-14/addons,/home/odoo/odoo-14-custom-addons
admin_passwd = $pbkdf2-sha512$25000$TwkBoDRGqBUi5LyXMiaE8A$SUyyCVfU1jk0YqiuTbHqVNmT31jw33fyh6tLMkA6t6lLSKpbnutYuQ.dVwQ2wIgWs2hf1OQmhNcHR9ofqGtSFg
csv_internal_sep = ,
data_dir = /home/odoo/.local/share/Odoo
db_host = False
db_maxconn = 64
db_name = codingdodo_demo
db_password = codingdodo
db_port = False
db_sslmode = prefer
db_template = template0
db_user = odoo
dbfilter =
demo = {}
email_from = False
geoip_database = /usr/share/GeoIP/GeoLite2-City.mmdb
http_enable = True
http_interface =
http_port = 8069
import_partial =
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_request = 8192
limit_time_cpu = 60
limit_time_real = 120
limit_time_real_cron = -1
list_db = False
log_db = False
log_db_level = warning
log_handler = :INFO
log_level = info
logfile = /var/log/odoo/odoo-14.log
longpolling_port = 8072
max_cron_threads = 2
osv_memory_age_limit = False
osv_memory_count_limit = False
pg_path =
pidfile =
proxy_mode = True
reportgz = False
screencasts =
screenshots = /tmp/odoo_tests
server_wide_modules = base,web
smtp_password = False
smtp_port = 25
smtp_server = localhost
smtp_ssl = False
smtp_user = False
syslog = False
test_enable = False
test_file =
test_tags = None
transient_age_limit = 1.0
translate_modules = ['all']
unaccent = False
upgrade_path =
without_demo = all
workers = 2
```

And that's it for today. We will still need to do **daily backups** for our database **and our filestore** (located at  
`/home/odoo/.local/share/Odoo/filestore/codingdodo_demo`)

Thanks for reading, if you liked this article please consider:

- ☕️ [Buying me a Coffee](https://www.buymeacoffee.com/CodingDodo)
- 🥳 Register on [Codingdodo.com](https://codingdodo.com)