Skip to content

lemacoregit/odoo-compose-zerodowntime

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Odoo 18 Zero-Downtime Deployment

Automated zero-downtime deployment for Odoo 18 using GitHub Actions, Docker Compose primary/standby strategy, and Nginx traffic switching via symlink reload.


Table of Contents


Overview

Every push to the main branch triggers the pipeline:

  1. Detects which custom Odoo modules changed
  2. Deploys the new version to the standby container (not yet receiving traffic)
  3. Runs health checks against the standby container β€” max 90 seconds
  4. If healthy: switches Nginx to standby (graceful reload, zero dropped connections)
  5. Stops the previously active container
  6. Upgrades only the changed modules via XML-RPC
  7. Sends an HTML email notification (success or failure)

If the standby container fails health checks, the pipeline aborts β€” the active container keeps serving traffic with no user impact.

Manual rollback is available via workflow_dispatch β†’ action: rollback.


Architecture

Developer
    β”‚ git push β†’ main
    β–Ό
GitHub Actions
    β”‚
    β”œβ”€ detect-changes  (git diff β†’ find __manifest__.py, depth-independent)
    β”‚
    β”œβ”€ deploy
    β”‚   β”œβ”€ SSH: git pull addons on server
    β”‚   β”œβ”€ SSH: docker compose up <next-slot>
    β”‚   β”œβ”€ SSH: health check (max 94s)
    β”‚   β”œβ”€ SSH: switch Nginx symlink + reload
    β”‚   β”œβ”€ SSH: docker compose stop <previous-slot>
    β”‚   └─ XML-RPC: upgrade changed modules (with retry + verify)
    β”‚
    β”œβ”€ rollback  (workflow_dispatch only β€” switches Nginx back to previous slot)
    β”‚
    └─ notify  (HTML email β†’ recipients, always runs)

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚               Server                β”‚
                    β”‚                                     β”‚
                    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
 User β†’ HTTPS:443 ────▢         Nginx :443             β”‚   β”‚
                    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
                    β”‚                 β”‚ symlink            β”‚
                    β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”            β”‚
                    β”‚         β”‚    ACTIVE     β”‚            β”‚
                    β”‚         β”‚ primary/stby  β”‚            β”‚
                    β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
                    β”‚                                     β”‚
                    β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
                    β”‚         β”‚    STANDBY    β”‚            β”‚
                    β”‚         β”‚   (stopped)   β”‚            β”‚
                    β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
                    β”‚                                     β”‚
                    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
                    β”‚  β”‚ PostgreSQL β”‚  β”‚    Redis     β”‚   β”‚
                    β”‚  β”‚  (shared)  β”‚  β”‚   (shared)   β”‚   β”‚
                    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Documentation

Each component has its own detailed README:

Folder README Covers
docker/ docker/README.md Docker Compose setup, primary/standby concept, volumes, network, environment variables, commands
nginx/ nginx/README.md Nginx config features, traffic switching, SSL, setup.sh, switch.sh, shutdown.sh
github/ github/README.md GitHub Actions workflow, secrets, module detection, force upgrade, rollback, email notification

Prerequisites

Server (Ubuntu 22.04+):

# Update system
sudo apt-get update && sudo apt-get upgrade -y

# Docker Engine
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker

# Docker Compose v2 plugin
sudo apt-get install -y docker-compose-plugin

# Nginx
sudo apt-get install -y nginx

# Certbot (Let's Encrypt)
sudo apt-get install -y certbot python3-certbot-nginx

# Tools
sudo apt-get install -y git curl

# Verify versions
docker --version
docker compose version
nginx -v

GitHub:

  • Repository with this project (or at minimum github/deploy.yml in .github/workflows/)
  • GitHub Actions enabled
  • Secrets configured (see Step 7)

Full Setup Guide

Step 1 β€” Prepare the Server

Run the commands in Prerequisites above on a fresh Ubuntu 22.04+ server.


Step 2 β€” Clone Repository

sudo mkdir -p /opt/zerodowntime
sudo chown $USER:$USER /opt/zerodowntime

git clone https://github.com/your-org/docker-zero-downtime /opt/zerodowntime
cd /opt/zerodowntime

Step 3 β€” Create Required Directories

These directories are git-ignored (runtime data):

cd /opt/zerodowntime/docker

# Odoo runtime data
mkdir -p etc/addons etc/filestore etc/sessions etc/logs

# PostgreSQL data
mkdir -p odoo-postgres

# Odoo core addons (download or copy separately)
mkdir -p addons/default

# Custom addons β€” clone your module repo here
# git clone https://github.com/your-org/your-addons addons/custom

Step 4 β€” Configure Environment

cd /opt/zerodowntime/docker

cp .env.example .env
nano .env

Minimum required changes:

POSTGRES_PASSWORD=your_strong_password
POSTGRES_DB=odoo

ODOO_SESSION_REDIS_PASSWORD=your_redis_password
ODOO_SESSION_REDIS_URL=redis://:your_redis_password@redis:6379/0

Then edit etc/conf/odoo.conf β€” update at minimum:

admin_passwd = your_strong_master_password
db_name = odoo

db_name must match POSTGRES_DB in .env.


Step 5 β€” Start Docker Containers

cd /opt/zerodowntime/docker

# Start infra + primary Odoo
docker compose up -d

# Wait ~30s then check
docker compose ps

Expected output:

NAME                            STATUS
odoo18-zerodowntime-postgres    Up (healthy)
odoo18-zerodowntime-redis       Up (healthy)
odoo18-zerodowntime-primary     Up (healthy)

Test locally:

curl -s -o /dev/null -w "%{http_code}" http://localhost:8018/web/health
# Expected: 200

Step 6 β€” Set Up Nginx and SSL

Make sure your domain (erp.zerodowntime.com) already points to this server's IP via DNS before running this.

cd /opt/zerodowntime
sudo bash nginx/setup.sh

setup.sh does everything in one run:

  1. Checks that nginx and certbot are installed
  2. Obtains a Let's Encrypt certificate via certbot --standalone
  3. Installs odoo18-compose-primary and odoo18-compose-standby to /etc/nginx/sites-available/
  4. Activates primary as default via symlink
  5. Creates /var/www/certbot for future webroot renewals
  6. Validates config (nginx -t) and starts Nginx
  7. Writes /etc/sudoers.d/odoo-nginx so the deploy user can reload Nginx without a password

Verify after setup:

# Check active symlink
readlink /etc/nginx/sites-enabled/odoo18-zerodowntime
# Expected: /etc/nginx/sites-available/odoo18-compose-primary

# Test HTTPS
curl -s -o /dev/null -w "%{http_code}" https://erp.zerodowntime.com/web/health
# Expected: 200

Step 7 β€” Configure GitHub Secrets

Go to: Repository β†’ Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret

Server Access:

Secret Value
SSH_PRIVATE_KEY OpenSSH private key β€” cat ~/.ssh/id_ed25519
SSH_HOST Server IP or hostname
SSH_USER SSH username (e.g. ubuntu)

Paths on Server:

Secret Value
ODOO_ADDONS_PATH /opt/zerodowntime/docker/addons/custom
COMPOSE_PROJECT_PATH /opt/zerodowntime
NGINX_SWITCH_SCRIPT_PATH /opt/zerodowntime/nginx/switch.sh

Odoo Connection:

Secret Value
ODOO_URL https://erp.zerodowntime.com
ODOO_DB odoo
ODOO_ADMIN_USER admin
ODOO_ADMIN_PASSWORD Odoo admin password

Email Notification:

Secret Value
EMAIL_RECIPIENTS dev@zerodowntime.com,ops@zerodowntime.com
SMTP_SERVER smtp.gmail.com
SMTP_PORT 587
SMTP_USER noreply@zerodowntime.com
SMTP_PASSWORD Gmail App Password
EMAIL_FROM Odoo CI/CD <noreply@zerodowntime.com>

Also place the workflow file on GitHub:

# In your repo, copy the workflow to the correct location
mkdir -p .github/workflows
cp github/deploy.yml .github/workflows/deploy.yml
git add .github/workflows/deploy.yml
git commit -m "ci: add odoo zero-downtime deployment workflow"
git push

Step 8 β€” Trigger First Deployment

Push any change to main to trigger the pipeline automatically.

Or use manual trigger:

  1. GitHub β†’ Actions β†’ Odoo CI/CD - Auto Deploy & Upgrade β†’ Run workflow
  2. Leave force_modules empty (auto-detect) or enter a module name
  3. Leave action as deploy
  4. Click Run workflow

Watch the pipeline at GitHub β†’ Actions.


Step 9 β€” Verify Everything

After a successful first deployment, the standby slot becomes active:

# Check running containers
docker ps --filter name=odoo18-zerodowntime

# Check active Nginx config (should now point to standby after first deploy)
readlink /etc/nginx/sites-enabled/odoo18-zerodowntime

# Test public URL
curl -s -o /dev/null -w "%{http_code}" https://erp.zerodowntime.com/web/health
# Expected: 200

# Check Nginx access log
sudo tail -20 /var/log/nginx/odoo-standby.access.log

# Check Odoo logs
docker logs --tail 50 odoo18-zerodowntime-standby

Port Reference

Service Host Port Notes
Odoo Primary (web) 8018 Nginx β†’ primary
Odoo Primary (longpoll) 8027 Nginx β†’ primary longpolling
Odoo Standby (web) 8118 Nginx β†’ standby
Odoo Standby (longpoll) 8127 Nginx β†’ standby longpolling
PostgreSQL 5016 External DB tools access
Redis 5030 External Redis tools access
HTTPS (public) 443 All user traffic via Nginx
HTTP (public) 80 Redirects to HTTPS

Troubleshooting

Containers not starting

docker logs odoo18-zerodowntime-primary
docker logs odoo18-zerodowntime-postgres
docker logs odoo18-zerodowntime-redis

# Check env file
cat /opt/zerodowntime/docker/.env

Standby container fails β€” network not found

The standby requires the network created by the primary compose. Always start primary first:

cd /opt/zerodowntime/docker
docker compose up -d
docker compose -f docker-compose.yml -f docker-compose.standby.yml up -d odoo-standby

Nginx returns 502 Bad Gateway

The active container may not be running:

# Check active slot
readlink /etc/nginx/sites-enabled/odoo18-zerodowntime

# Check containers
docker ps --filter name=odoo18-zerodowntime

# Switch to the running slot
sudo bash /opt/zerodowntime/nginx/switch.sh primary
# or
sudo bash /opt/zerodowntime/nginx/switch.sh standby

Health check fails during deploy

The pipeline aborts and keeps the active container running. Check the failed container:

docker logs odoo18-zerodowntime-standby
docker logs odoo18-zerodowntime-primary

Manual rollback needed

Use the GitHub Actions manual trigger:

  1. Actions β†’ Odoo CI/CD β†’ Run workflow
  2. Set action to rollback
  3. Click Run workflow

Or roll back manually on the server:

sudo bash /opt/zerodowntime/nginx/switch.sh primary
# or
sudo bash /opt/zerodowntime/nginx/switch.sh standby

GitHub Actions SSH fails

# Test from local machine
ssh -i your_key ubuntu@your_server_ip "echo OK"

# Key format must be OpenSSH
head -1 your_key
# Expected: -----BEGIN OPENSSH PRIVATE KEY-----

Module upgrade fails

Run the script manually from any machine with network access to Odoo:

python3 github/script/upgrade_modules.py \
  --url https://erp.zerodowntime.com \
  --db odoo \
  --user admin \
  --password your_password \
  --modules your_module_name

Email not received

Remove Nginx config (decommission)

sudo bash /opt/zerodowntime/nginx/shutdown.sh --yes
# With log cleanup:
sudo bash /opt/zerodowntime/nginx/shutdown.sh --yes --purge-logs

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors