Skip to content

Scaling for Multiple Companies

Guide for deploying JDX Odoo to multiple companies/clients.

Deployment Strategies

Option 1: Multi-Company (Single Instance)

Use Odoo's built-in multi-company feature. Single deployment serves multiple companies.

┌─────────────────────────────────────────────┐
│              Single Odoo Instance            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐     │
│  │Company A│  │Company B│  │Company C│     │
│  └─────────┘  └─────────┘  └─────────┘     │
│                    │                        │
│              ┌─────┴─────┐                  │
│              │ Shared DB │                  │
│              └───────────┘                  │
└─────────────────────────────────────────────┘

Pros: - Single deployment to maintain - Shared infrastructure costs - Easy user access across companies

Cons: - Data isolation concerns - Single point of failure - Resource contention

Best for: Related companies, franchises, holding companies

Each company gets their own isolated deployment.

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Company A   │  │  Company B   │  │  Company C   │
│  ┌────────┐  │  │  ┌────────┐  │  │  ┌────────┐  │
│  │  Odoo  │  │  │  │  Odoo  │  │  │  │  Odoo  │  │
│  │  PWA   │  │  │  │  PWA   │  │  │  │  PWA   │  │
│  │  Docs  │  │  │  │  Docs  │  │  │  │  Docs  │  │
│  └────────┘  │  │  └────────┘  │  │  └────────┘  │
│  ┌────────┐  │  │  ┌────────┐  │  │  ┌────────┐  │
│  │   DB   │  │  │  │   DB   │  │  │  │   DB   │  │
│  └────────┘  │  │  └────────┘  │  │  └────────┘  │
└──────────────┘  └──────────────┘  └──────────────┘

Pros: - Complete data isolation - Independent scaling - Custom configurations per client - No cross-company risk

Cons: - More infrastructure to manage - Higher costs per client - Updates applied individually

Best for: SaaS offering, independent clients, compliance requirements

Quick Setup for New Company

Step 1: Clone Repository

# Create company directory
mkdir -p /opt/clients/company-name
cd /opt/clients/company-name

# Clone from template
git clone git@github.com:yourorg/odoo15-production.git .

Step 2: Configure Environment

# Copy and customize environment
cp .env.example .env

# Edit with company-specific settings
nano .env

Key variables to customize:

# Docker Compose - use production overlay
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml

# Database
DB_NAME=odoo_companyname
DB_HOST=companyname-db.xxxxx.us-east-1.rds.amazonaws.com
DB_PASSWORD=<unique-strong-password>

# Domains
DOMAIN_ERP=erp.companyname.com
DOMAIN_PWA=field.companyname.com
DOMAIN_DOCS=docs.companyname.com

# Odoo
ODOO_ADMIN_PASSWORD=<unique-admin-password>

# Integrations (company-specific)
JUSTCALL_API_KEY=<company-justcall-key>
XERO_CLIENT_ID=<company-xero-id>

Step 3: Initialize Database

# Start database (COMPOSE_FILE in .env auto-loads both files)
docker compose up -d db
sleep 10

# Initialize Odoo
docker compose exec odoo odoo -i base --stop-after-init -d odoo_companyname

Step 4: Configure Company Data

# Access Odoo shell
docker compose exec odoo odoo shell -d odoo_companyname
# Configure company
company = env['res.company'].browse(1)
company.write({
    'name': 'Company Name LLC',
    'email': 'info@companyname.com',
    'phone': '+1-555-123-4567',
    'street': '123 Main St',
    'city': 'Austin',
    'state_id': env.ref('base.state_us_48').id,  # Texas
    'country_id': env.ref('base.us').id,
    'zip': '78701',
})

# Configure JustCall lines
lines = [
    {'name': 'Sales', 'phone': '+1-555-111-1111', 'line_type': 'main', 'is_default': True},
    {'name': 'Support', 'phone': '+1-555-222-2222', 'line_type': 'cs'},
]
for line in lines:
    env['justcall.line'].create(line)

env.cr.commit()

Step 5: Start Services

docker compose up -d

Step 6: Configure SSL

certbot --nginx \
  -d erp.companyname.com \
  -d field.companyname.com \
  -d docs.companyname.com

Configuration Templates

Company Configuration Script

Create scripts/setup-company.sh:

#!/bin/bash
# Usage: ./setup-company.sh <company-slug> <company-name> <admin-email>

COMPANY_SLUG=$1
COMPANY_NAME=$2
ADMIN_EMAIL=$3

if [ -z "$COMPANY_SLUG" ]; then
    echo "Usage: $0 <company-slug> <company-name> <admin-email>"
    exit 1
fi

echo "Setting up: $COMPANY_NAME ($COMPANY_SLUG)"

# Generate passwords
DB_PASS=$(openssl rand -base64 32)
ADMIN_PASS=$(openssl rand -base64 16)

# Create .env
cat > .env << EOF
# $COMPANY_NAME Configuration
# Generated: $(date)

# Docker Compose - use production overlay
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml

DB_NAME=odoo_${COMPANY_SLUG}
DB_USER=odoo
DB_PASSWORD=${DB_PASS}
DB_HOST=db

ODOO_ADMIN_PASSWORD=${ADMIN_PASS}

DOMAIN_ERP=erp.${COMPANY_SLUG}.com
DOMAIN_PWA=field.${COMPANY_SLUG}.com
DOMAIN_DOCS=docs.${COMPANY_SLUG}.com

TZ=America/Chicago
EOF

echo "Environment file created: .env"
echo "Admin password: $ADMIN_PASS"
echo ""
echo "Next steps:"
echo "1. Configure DNS for *.${COMPANY_SLUG}.com"
echo "2. Run: docker compose up -d"
echo "3. Configure SSL certificates"

JustCall Configuration Template

Create scripts/setup-justcall.py:

#!/usr/bin/env python3
"""
Configure JustCall lines for a company.
Usage: docker compose exec odoo python3 /scripts/setup-justcall.py
"""
import os

# Configuration - customize per company
COMPANY_LINES = [
    {
        'name': os.environ.get('JUSTCALL_LINE1_NAME', 'Main'),
        'phone': os.environ.get('JUSTCALL_LINE1_PHONE', '+1-555-000-0001'),
        'line_type': 'main',
        'is_default': True,
    },
    {
        'name': os.environ.get('JUSTCALL_LINE2_NAME', 'Support'),
        'phone': os.environ.get('JUSTCALL_LINE2_PHONE', '+1-555-000-0002'),
        'line_type': 'cs',
        'is_default': False,
    },
]

def setup_lines(env):
    """Create or update JustCall lines."""
    JustCallLine = env['justcall.line']

    for line_data in COMPANY_LINES:
        existing = JustCallLine.search([('phone', '=', line_data['phone'])])
        if existing:
            existing.write(line_data)
            print(f"Updated: {line_data['name']}")
        else:
            JustCallLine.create(line_data)
            print(f"Created: {line_data['name']}")

    env.cr.commit()
    print("JustCall configuration complete!")

if __name__ == '__main__':
    # When run in Odoo shell context
    setup_lines(env)

Multi-Tenant Architecture

For true SaaS with many clients, consider:

Kubernetes Deployment

# Per-tenant namespace
apiVersion: v1
kind: Namespace
metadata:
  name: tenant-companyname
---
# Odoo deployment per tenant
apiVersion: apps/v1
kind: Deployment
metadata:
  name: odoo
  namespace: tenant-companyname
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: odoo
        image: odoo:15
        envFrom:
        - secretRef:
            name: odoo-secrets

Terraform for Infrastructure

# modules/client/main.tf
variable "company_slug" {}
variable "company_name" {}

resource "aws_db_instance" "odoo" {
  identifier = "odoo-${var.company_slug}"
  engine     = "postgres"
  # ...
}

resource "aws_route53_record" "erp" {
  name = "erp.${var.company_slug}"
  # ...
}

Checklist: New Company Onboarding

  • Create infrastructure (server, database, DNS)
  • Clone repository
  • Configure environment variables
  • Initialize database
  • Configure company details in Odoo
  • Set up JustCall phone lines
  • Configure Xero integration (if needed)
  • Set up SSL certificates
  • Create admin user account
  • Test all services
  • Configure backups
  • Document client-specific settings
  • Hand off to client

Maintenance

Updating All Clients

#!/bin/bash
# update-all-clients.sh
CLIENTS_DIR="/opt/clients"

for client in "$CLIENTS_DIR"/*/; do
    echo "Updating: $client"
    cd "$client"
    git pull origin main
    docker compose pull
    docker compose up -d --build
    echo "---"
done

Backup All Clients

#!/bin/bash
# backup-all-clients.sh
CLIENTS_DIR="/opt/clients"
BACKUP_BUCKET="s3://company-backups"

for client in "$CLIENTS_DIR"/*/; do
    client_name=$(basename "$client")
    echo "Backing up: $client_name"
    cd "$client"
    ./scripts/backup.sh full
    aws s3 sync /tmp/odoo-backups/ "$BACKUP_BUCKET/$client_name/"
done