How to Deploy Odoo on Kubernetes: A Step-by-Step Guide

blog-banner

Getting Odoo up and running on your local machine doesn't have to be complicated. This guide will walk you through everything you need to know. We'll be installing Odoo (the popular open-source ERP and CRM platform) alongside PostgreSQL, all running on a local Kubernetes (K8s) cluster with persistent storage.

What makes this setup particularly useful is that we're configuring proper storage persistence for all the important bits, your Odoo data, application logs, any custom modules you develop, enterprise add-ons, and of course, the PostgreSQL database itself. This means your data sticks around even when containers restart, making it perfect whether you're developing, testing, or even running a smaller production environment.

If you want to take this setup further for your business, Odoo Consulting Services can help you optimize configurations, integrate custom modules, and ensure smooth scalability. With the right guidance, your Odoo environment can be tailored to fit your exact business needs.

Overview of Deploying Odoo on Kubernetes (K8s)

Odoo is essentially a complete business management platform, but like most enterprise applications, it relies heavily on a robust database to handle all your business data - that's where PostgreSQL comes in. While you could run Odoo in various ways, Kubernetes (K8s) gives you some real advantages: your setup becomes much more scalable, you get built-in fault tolerance, and managing everything becomes significantly easier once you get the hang of it.

We'll stick with local volumes for storage here. Why? Well, when you're running everything on your own hardware and want to avoid the nightmare of configuring network-attached storage or dealing with cloud providers, local volumes are your friend. They're straightforward to set up, won't strain your resources if you're on older hardware, and honestly - they just work. Your data stays put when containers restart, which is really all we need for this kind of setup.

Here's what we'll accomplish: we'll create a dedicated Kubernetes (K8s) namespace to keep everything organized, configure persistent storage so your data survives restarts, get both PostgreSQL and Odoo deployed and talking to each other, and finally expose Odoo through a NodePort service so you can actually access it from your web browser. I've tested these steps multiple times to make sure everything works reliably, so you should have a smooth installation experience.

Prerequisites

Your Kubernetes (K8s) Setup: Make sure you've got a working Kubernetes (K8s) cluster running. I usually recommend minikube if you're just testing things out or learning - it's super easy to set up on your laptop. But if this is going into production or you want something more robust, kubeadm is your best bet. Whatever you choose, just verify it's actually running before we continue. 

Kubectl Tool: You'll need kubectl installed and talking to your cluster properly. Quick test - try running kubectl version in your terminal. If it spits back version info without complaining, you're all set. 

Storage Handler: If you want Kubernetes (K8s) to automatically create storage volumes (which is pretty convenient), you'll need a local path provisioner running. Don't have one? No worries - we can create the volumes manually instead. 

Admin Rights: You're going to need sudo or root access on your Kubernetes node since we'll be creating directories and setting up permissions. 

Step 1: Directory Setup:

Persistent storage is required to preserve Odoo and PostgreSQL data in case of pod restarts. Here's the deal with containers - when they restart (and they will), everything inside gets wiped clean. That's why we need to set up persistent storage to keep our Odoo and PostgreSQL data safe.

What we're doing is creating specific folders on your Kubernetes server for all the important stuff - your Odoo application data, log files, any custom modules you've built, enterprise add-ons, and the PostgreSQL database files. Kubernetes (K8s) will then mount these folders as Persistent Volumes inside your containers, so your data survives restarts.

Let's create these directories now:

  1. sudo mkdir -p /mnt/kubernetes/odoo/data

  2. sudo mkdir -p /mnt/kubernetes/odoo/logs

  3. sudo mkdir -p /mnt/kubernetes/odoo/custom-addons

  4. sudo mkdir -p /mnt/kubernetes/odoo/enterprise-addons

  5. sudo mkdir -p /mnt/kubernetes/postgres/data

  6. sudo mkdir -p odoo-with-kubernetes/

directory setup code
 

  1. /mnt/kubernetes/odoo/data: Stores Odoo's core data, such as configuration and session data.

  2. /mnt/kubernetes/odoo/logs: Holds Odoo logs for monitoring and debugging.

  3. /mnt/kubernetes/odoo/custom-addons: Stores custom Odoo modules or addons developed for particular business needs.

  4. /mnt/kubernetes/odoo/enterprise-addons: Keeps enterprise addons for Odoo paid features (if any).

  5. /mnt/kubernetes/postgres/data: Stores PostgreSQL database files to ensure data is maintained.

  6. odoo-with-kubernetes/: Local working directory for maintaining Kubernetes (K8s) manifest files.

Step 2: Set Permissions

Proper permissions enable PostgreSQL and Odoo containers to access and write to the directories. This step assigns ownership to the current user and grants permissive permissions to the directory of logs, which Odoo requires for the sake of writing logs.

Execute the following commands: 

  1. sudo chown -R $USER:root /mnt/kubernetes/odoo/data

  2. sudo chown -R $USER:root /mnt/kubernetes/odoo/logs

  3. sudo chown -R $USER:root /mnt/kubernetes/odoo/custom-addons

  4. sudo chown -R $USER:root /mnt/kubernetes/odoo/enterprise-addons

  5. sudo chown -R $USER:root /mnt/kubernetes/postgres/data

  6. sudo chown -R $USER:root odoo-with-kubernetes/

  7. sudo chmod 777 -R /mnt/kubernetes/odoo/logs
    set permissions code
     

  8. chown -R $USER:root: Changes ownership to the current user and root group so the containers (which are run as particular users) can read the directories.

  9. chmod -R 777 /mnt/kubernetes/odoo/logs: Grants read, write, and execute rights to all users on the logs directory because Odoo requires higher permissions for logging.

Step 3: PostgreSQL Deployment 

PostgreSQL is the database backend used by Odoo to hold all the business information. It creates a Persistent Volume (PV) and Persistent Volume Claim (PVC) for storage, a Deployment to run the PostgreSQL container, and a Service to enable communication with Odoo. 

Create PostgreSQL Persistent Volume and PVC 

PV identifies the node's storage location, and the PVC requests storage for the PostgreSQL pod. Local storage implementation offers ease for self-hosted installations. 

First, navigate to your project folder: 

cd odoo-with-kubernetes/ 

Now we need to create a configuration file for PostgreSQL storage. Create a new file called postgres-pv-pvc.yml and add this content: 

nano postgres-pv-pvc.yml

Add the following configuration:

apiVersion: v1 

kind: PersistentVolume 

metadata: 

  name: postgres-pv 

  labels: 

    type: local 

spec: 

  capacity: 

    storage: 3Gi 

  accessModes: 

    - ReadWriteOnce 

  storageClassName: local-storage 

  local: 

    path: /mnt/kubernetes/postgres/data 

  nodeAffinity: 

    required: 

      nodeSelectorTerms: 

        - matchExpressions: 

            - key: kubernetes.io/hostname 

              operator: In 

              values: 

                - node_name #Replace with your node name 

--- 

apiVersion: v1 

kind: PersistentVolumeClaim 

metadata: 

  name: postgres-pvc 

  namespace: odoo 

spec: 

  accessModes: 

    - ReadWriteOnce 

  resources: 

    requests: 

      storage: 3Gi 

  storageClassName: local-storage

Here's what this file does: The PersistentVolume section tells Kubernetes about a 3GB storage space located at /mnt/kubernetes/postgres/data on your server. The nodeAffinity part makes sure this volume gets attached to a specific node - you'll need to replace node_name with your actual node's hostname (just run kubectl get nodes to find it). 

The PersistentVolumeClaim is basically your app saying "hey, I need 3GB of storage from the odoo namespace." It matches up with the PersistentVolume we just defined since they both use the same storage class called local-storage.

Provision PostgreSQL Deployment 

The Deployment creates the PostgreSQL container, both in terms of image, environment variables, and storage mounts. 

Create a file called postgres-deployment.yml: 

nano postgres-deployment.yml 

Add the following configuration:

  apiVersion: apps/v1

kind: Deployment

metadata:

  name: postgres

spec: 

  replicas: 1

  selector:

    matchLabels:

      app: postgres

  template:

    metadata:

      labels:

        app: postgres

    spec:

      nodeSelector:

        kubernetes.io/hostname: node_name #Replace with your node name

      containers:

      - name: postgres

        image: postgres:13

        ports:

          - containerPort: 5432

        env:

          - name: POSTGRES_DB

            value: "postgres"

          - name: POSTGRES_USER

            value: "odoo"

          - name: POSTGRES_PASSWORD

            value: "postgres"

        volumeMounts:

          - name: postgres-data

            mountPath: /var/lib/postgresql/data

      volumes:

        - name: postgres-data

          persistentVolumeClaim:

            claimName: postgres-pvc

 

replicas: 1: Scales up to one PostgreSQL pod for simplicity. 

image: postgres:13: Utilizes PostgreSQL 13, the compatible one with Odoo 19. 

env: Specifies the database name (postgres), user (odoo), and password (postgres). Change the password in production. 

volumeMounts: Mounts the PVC at /var/lib/postgresql/data to persist database files. 

  • nodeSelector: Guarantees the pod to be planned on the given node (replace node_name). 

Create PostgreSQL Service 

The Service exposes PostgreSQL in the cluster for Odoo to use to connect to. 

Create a file called postgres-service.yml: 

  • nano postgres-service.yml 

apiVersion: v1

kind: Service

metadata:

  name: postgres

  namespace: odoo

spec:

  selector:

    app: postgres

  ports:

    - port: 5432

      targetPort: 5432

selector: app: postgres: Picks the PostgreSQL pod. 

ports: Opens up the default port 5432 for PostgreSQL for communication within the cluster. 

namespace: odoo: Places the service in the odoo namespace. 

Create Odoo Namespace and Deploy PostgreSQL: 

Setting Up Your Odoo Workspace and Getting PostgreSQL Running 

kubectl create namespace odoo 

kubectl apply -f postgres-pv-pvc.yml 

kubectl apply -f postgres-deployment.yml -n odoo 

kubectl apply -f postgres-service.yml 

Odoo Namespace and Deploy PostgreSQL
 

kubectl create namespace odoo: This keeps all our Odoo stuff organized and separate from other things running on your cluster. 

kubectl apply: This command actually deploys PostgreSQL itself along with the service that lets other components talk to it. 

Odoo partner
 

Step 4: Odoo Deployment 

Odoo Deployment requires persistent storage to store its data, logs, add-ons, and configuration files. This section builds PVs, PVCs, an Odoo configuration ConfigMap, an Odoo application Deployment, and an external access NodePort Service for Odoo. 

Odoo Persistent Volumes and PVCs 

Odoo uses four PVs and PVCs for data, logs, custom add-ons, and enterprise add-ons for persistence and modularity. 

Create a file named odoo-pv-pvc.yml: 

  • nano odoo-pv-pvc.yml 

Set the following setting: 

     #1. Odoo Data

apiVersion: v1

kind: PersistentVolume

metadata:

  name: odoo-data-pv

  labels:

    type: local

spec:

  capacity:

    storage: 3Gi

  accessModes:

    - ReadWriteOnce

  storageClassName: local-storage

  local:

    path: /mnt/kubernetes/odoo/data

  nodeAffinity:

    required:

      nodeSelectorTerms:

        - matchExpressions:

            - key: kubernetes.io/hostname

              operator: In

              values:

                - node_name #Replace with your node name

--- 

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

  name: odoo-data-pvc

  namespace: odoo

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 3Gi

  storageClassName: local-storage

  

#2.Odoo Logs

--- 

apiVersion: v1

kind: PersistentVolume

metadata:

  name: odoo-logs-pv

  labels:

    type: local

spec:

  capacity:

    storage: 2Gi

  accessModes:

    - ReadWriteOnce

  storageClassName: local-storage

  local:

    path: /mnt/kubernetes/odoo/logs

  nodeAffinity:

    required:

      nodeSelectorTerms:

        - matchExpressions:

            - key: kubernetes.io/hostname

              operator: In

              values:

                - node_name #Replace with your node name

--- 

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

  name: odoo-logs-pvc

  namespace: odoo

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 2Gi

  storageClassName: local-storage

  

#3.Odoo Custom Addons

--- 

apiVersion: v1

kind: PersistentVolume

metadata:

  name: odoo-customaddons-pv

  labels:

    type: local

spec:

  capacity:

    storage: 2Gi

  accessModes:

    - ReadWriteOnce

  storageClassName: local-storage

  local:

    path: /mnt/kubernetes/odoo/custom-addons

  nodeAffinity:

    required:

      nodeSelectorTerms:

        - matchExpressions:

            - key: kubernetes.io/hostname

              operator: In

              values:

                - node_name #Replace with your node name

--- 

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

  name: odoo-customaddons-pvc

  namespace: odoo

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 2Gi

  storageClassName: local-storage

  

#4.Odoo Enterprise Addons

--- 

apiVersion: v1

kind: PersistentVolume

metadata:

  name: odoo-enterpriseaddons-pv

  labels:

    type: local

spec:

  capacity:

    storage: 3Gi

  accessModes:

    - ReadWriteOnce

  storageClassName: local-storage

  local:

    path: /mnt/kubernetes/odoo/enterprise-addons

  nodeAffinity:

    required:

      nodeSelectorTerms:

        - matchExpressions:

            - key: kubernetes.io/hostname

              operator: In

              values:

                - node_name #Replace with your node name

--- 

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

  name: odoo-enterpriseaddons-pvc

  namespace: odoo

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 3Gi

  storageClassName: local-storage

Every PV/PVC is linked with a specific directory (data, logs, custom-addons, enterprise-addons). 

storage: 3Gi or 2Gi: Specifies storage sizes (variable based on requirement). 

nodeAffinity: Binds each PersistentVolume (PV) to the specified node (use node_name). 

namespace: odoo: Put PVCs in the odoo namespace.

Create an Odoo ConfigMap manifest file.

This ConfigMap holds the odoo.conf file, which is basically Odoo's instruction manual for things like connecting to PostgreSQL and locating your installed modules.

Create a file named configmap-odoo.yml:

  • nano configmap-odoo.yml

Include the following context:

apiVersion: v1 

kind: ConfigMap 

metadata: 

     name: odoo-config 

     namespace: odoo 

data: 

    odoo.conf: | 

         [options] 

         addons_path = /mnt/extra-addons/enterprise-addons,/mnt/extra-addons/custom-addons,/usr/lib/python3/dist-packages/ 

         data_dir = /var/lib/odoo 

         admin_passwd = admin_password 

         csv_internal_sep = , 

         db_maxconn = 64 

         db_host = postgres.odoo.svc.cluster.local 

         db_name = 

         db_port = 5432 

         db_template = template1 

         dbfilter = .* 

         debug_mode = False 

         email_from = False 

         limit_memory_hard = 1677721600 

         limit_memory_soft = 6291456000 

         limit_request = 8192 

         limit_time_cpu = 600 

         limit_time_real = 1200 

         list_db = True 

         log_db = False 

         log_handler = [':INFO'] 

         log_level = info 

         logfile = /var/log/odoo/odoo.log 

         longpolling_port = 8072 

         max_cron_threads = 2 

         smtp_password = False 

         smtp_port = 25 

         smtp_server = localhost 

         smtp_ssl = False 

         smtp_user = False 

         workers = 0 

         xmlrpc = True 

         xmlrpc_interface = 

         xmlrpc_port = 8069 

         ;xmlrpcs = True 

         ;xmlrpcs_interface = 

         ;xmlrpcs_port = 8071  

  
  • addons_path: Determines enterprise and custom addon paths

  • data_dir: Specifies the data directory to /var/lib/odoo, mounted via PVC.

  • admin_passwd: Your initial admin login password

  • db_host: How Odoo finds your PostgreSQL database using Kubernetes' internal naming (postgres.odoo.svc.cluster.local).

  • logfile: Where Odoo dumps all its log messages at /var/log/odoo/odoo.log, also saved to persistent storage

Create an Odoo Deployment manifest file. 

Alright, create the deployment file for Odoo itself. This is basically the blueprint that tells Kubernetes (K8s), "Here's how I want my Odoo container to run" - covering everything from the Docker image we're using to the ports we need open, plus all the environment variables and storage connections. 

Go ahead and create the deployment file: 

  • nano odoo-deployment.yml 

Place the following settings: 

 apiVersion: apps/v1 

kind: Deployment 

metadata: 

  name: odoo 

spec: 

  replicas: 1 

  selector: 

    matchLabels: 

      app: odoo 

  template: 

    metadata: 

      labels: 

        app: odoo 

    spec: 

      nodeSelector: 

        kubernetes.io/hostname: node_name #Replace with your node name 

      securityContext: 

        runAsUser: 0 

        runAsGroup: 0 

        fsGroup: 0 

      containers: 

        - name: odoo 

          image: odoo:17 

          ports: 

            - containerPort: 8069 

            - containerPort: 8072 

          env: 

            - name: HOST 

              value: "postgres.odoo.svc.cluster.local" 

            - name: USER 

              value: "odoo" 

            - name: PASSWORD 

              value: "postgres" 

          volumeMounts: 

            - name: odoo-data 

              mountPath: /var/lib/odoo 

            - name: odoo-config 

              mountPath: /etc/odoo/odoo.conf 

              subPath: odoo.conf 

            - name: odoo-logs 

              mountPath: /var/log/odoo 

            - name: enterprise-addons 

              mountPath: /mnt/extra-addons/enterprise-addons 

            - name: custom-addons 

              mountPath: /mnt/extra-addons/custom-addons 

      volumes: 

        - name: odoo-config 

          configMap: 

            name: odoo-config 

        - name: odoo-data 

          persistentVolumeClaim: 

            claimName: odoo-data-pvc 

        - name: odoo-logs 

          persistentVolumeClaim: 

            claimName: odoo-logs-pvc 

        - name: custom-addons 

          persistentVolumeClaim: 

            claimName: odoo-customaddons-pvc 

        - name: enterprise-addons 

          persistentVolumeClaim: 

            claimName: odoo-enterpriseaddons-pvc  

  

 image: odoo:17: Uses Odoo version 17. 

  • ports: Listens on 8069 (primary HTTP port) and 8072 
  • env: Specifies database connection parameters (host, user, password). 
  • securityContext: Runs the container as root (user/group 0) to match directory permissions. 
  • volumeMounts: Mounts the data, logs, addons, and the ConfigMap for the config file.

Create a Service manifest file for Odoo. 

The Service exposes Odoo externally via a NodePort, allowing browser access. 

Create a file called odoo-service-nodeport.yml: 

  • nano odoo-service-nodeport.yml 

Add the following configuration: 

 apiVersion: v1 

kind: Service 

metadata: 

  name: odoo-service 

spec: 

  selector: 

    app: odoo 

  type: NodePort 

  ports: 

    - protocol: TCP 

      port: 8069 

      targetPort: 8069 

      nodePort: 30069
  • type: NodePort: Exposes Odoo on node IP port 30069. 
  • selector: app: odoo: Chooses the Odoo pod. 
  • ports: Maps port 8069 to container port 8069.

    odoo-service-nodeport

Install the entire set of Odoo manifest files. 

Install the Odoo manifests: 

  • kubectl apply -f odoo-pv-pvc.yml 
  • kubectl get pv
    Odoo manifests file
     
  • kubectl apply -f configmap-odoo.yml -n odoo 
  • kubectl apply -f configmap-odoo.yml -n odoo 
  • kubectl apply -f odoo-deployment.yml 
  • kubectl apply -f odoo-service-nodeport.yml -n odoo

     install odoo

 

Step 5: Access Odoo 

After deployment, access Odoo through the NodePort service: 

Find the node IP address: 

  • kubectl get nodes -o wide
    node IP address
     
  • Open Odoo at http://<internal-node-ip>:30069. 
  • Log in as the admin with the default password set as admin_password (defined in the ConfigMap).

Access Odoo Panel
 

Step 6: Verify Deployment 

Ensure everything is working well: 

kubectl get pods -n odoo 

kubectl get services -n odoo 

kubectl get pods: Verifies running PostgreSQL and Odoo pods 

kubectl get services: Verifies that postgres and odoo-service services are active. 

Ensure that both the PostgreSQL and Odoo pods are in the Running state. 

Final Thoughts on Deploying Odoo 19 on Kubernetes (K8s)


What we've built here gives you a solid starting point for running Odoo on Kubernetes (K8s) without breaking the bank. The local storage approach keeps things simple and cheap, while still giving you all the benefits of containerized deployment.

Following this guide, you end up with a complete Odoo system that keeps your data safe and has PostgreSQL handling the heavy lifting on the database side. It's perfect for dev work, testing out new configurations, or even running a smaller business – anywhere you don't need enterprise-level redundancy.

When you're ready to scale this up for serious production use, there are plenty of ways to expand it. You could move to managed storage solutions for better performance, lock down security with proper secrets management and stronger authentication, or add multiple Odoo instances behind a load balancer for high availability.

The whole setup follows Kubernetes conventions pretty well, so it's maintainable and won't paint you into a corner later. If you're looking to host your own ERP system without vendor lock-in or monthly subscription fees, this approach gives you a lot of flexibility to grow as your needs change. And if you want expert help in setting up, customizing, or scaling further, you can always hire Odoo developer to make the process smoother and more efficient.

Notes:

  1. Don't forget to swap out node_name in your manifest files with your actual node's hostname - just run kubectl get nodes to see what it's called.
  2. Second, make sure you update that admin password in the ConfigMap - the default one is fine for testing
  3. Storage sizes (e.g., 3Gi, 2Gi) can be resized to whatever you need.
  4. For production, employ a managed storage system or dynamic provisioning instead of local volumes.
  5. Ensure the local path provisioner is set if dynamic storage provisioning is enabled.

 

FAQ on Deploying Odoo 19 on Kubernetes (K8s)

1. Do I need separate containers for Odoo and PostgreSQL?

Yes. Odoo and PostgreSQL should run in separate containers. It makes scaling, updates, and maintenance much easier.

2. Can I run multiple Odoo instances on the same cluster?

Yes. You can deploy multiple Odoo pods as long as each one points to its own database or schema. Use separate PVCs and config maps for clean isolation.

3. What’s the best way to manage Odoo configuration in Kubernetes (K8s)?

Use ConfigMaps for general settings and Secrets for sensitive values like database passwords. Mount them as environment variables or files depending on preference.

4. How do I handle file storage for Odoo on Kubernetes (K8s)?

Odoo stores attachments on disk. Use a PersistentVolume and mount it into the Odoo container. For distributed setups, use shared storage like NFS, Ceph or an equivalent cloud option.

5. How do I scale Odoo on K8s?

You can add more replicas of the Odoo web workers. The Odoo longpolling service should run separately. Database scaling will depend on your PostgreSQL setup.

Contact us

For Your Business Requirements

Contact us