Skip to content

Repository files navigation

Deployment Manager for Odoo images

Abstract

This project supports self-hosted Odoo deployments on a Linux host. It provides a custom Odoo Docker image, a rerunnable Ubuntu host configuration script, and an XML-RPC deployment manager for instance lifecycle tasks such as create/reset/restart/upgrade, hostname-to-port routing updates, and SSL certificate management. It also handles client-side encrypted database and filestore backup/restore workflows using pg_dump/pg_restore and rclone (with zero-knowledge crypt overlay), with scheduled retention cleanup.

The XML-RPC API can be used from an Odoo instance to do self upgrades of Odoo source code.

System architecture

flowchart LR
    GHCR["GHCR odoo-src image"]
    User["Users and Browsers"]
    DNS["DNS and Hostname"]
    Nginx["NGINX routing and SSL"]
    Odoo["Odoo instances"]
    PG["PostgreSQL databases"]
    FS["Filestore volumes"]
    DeployManager["Deployment Manager"]
    Backup["Backups"]
    Rclone["rclone crypt to S3 storage"]
    Monitoring["Prometheus Grafana Loki"]
    Metrics["System metrics"]
    Logs["Logging (promtail)"]
    Modules["Odoo modules"]

    GHCR --> Docker
    Docker --> Odoo
    Modules -->|self upgrades| Odoo

    User --> DNS
    DNS --> Nginx
    Nginx -->|hostname -> port| Odoo
    Odoo --> PG
    Odoo --> FS
    Odoo -->|cloudflare api| DNS
    Odoo --> DeployManager

    Backup --> PG
    Backup --> FS

    DeployManager --> Docker
    DeployManager --> Nginx
    DeployManager --> Backup
    DeployManager -->|git| Modules
    Backup --> Rclone

    Docker --> Metrics
    Docker --> Logs

    Metrics --> Monitoring
    Logs --> Monitoring


    subgraph OdooHost["Linux VPS - Odoo"]
        Docker
        Odoo
        PG
        FS
        DeployManager
        Modules
        Nginx
        Backup
        Metrics
        Logs
    end


    subgraph MonHost["Linux VPS - Monitoring"]
      Monitoring
    end


Loading

Installation

Prerequisite

Use a fresh Ubuntu 26.04 LTS server and a separate empty Hetzner Volume. Attach the volume without formatting or automatically mounting it. See LUKS.md for the storage layout and recovery precautions.

Point 19.eniemela.fi to this server and allow TCP 9019 in the host and Hetzner firewalls. The agent is available at https://19.eniemela.fi:9019 through nginx basic authentication; its backend is loopback-only on port 8019.

Installation

Identify the volume by matching its MODEL, SERIAL, and SIZE with the Hetzner Console. Use its /dev/sdX name only for the initial format; /etc/crypttab stores the stable UUID used after that. luksFormat and mkfs.ext4 destroy data on the selected device, so confirm the lsblk output before continuing. LUKS asks for the passphrase interactively and does not store it on the server.

apt update
apt install -y cryptsetup git vim tmux
git clone -b 19.0 --recurse-submodules https://github.com/elmeriniemela/deploy-manager.git /opt/19
cd /opt/19
lsblk -So NAME,MODEL,SERIAL,SIZE,TYPE
INSTALL_DEVICE=/dev/sdX
cryptsetup luksFormat --type luks2 "$INSTALL_DEVICE"
LUKS_UUID="$(cryptsetup luksUUID "$INSTALL_DEVICE")" && echo "$LUKS_UUID"
udevadm trigger --action=change --name-match="$INSTALL_DEVICE"
udevadm settle
readlink -e "/dev/disk/by-uuid/$LUKS_UUID"
cryptsetup open "$INSTALL_DEVICE" appdata
mkfs.ext4 /dev/mapper/appdata
cryptsetup luksHeaderBackup "$INSTALL_DEVICE" --header-backup-file "/root/appdata-luks-header-$LUKS_UUID.img"

Append the UUID entry to /etc/crypttab:

echo "appdata UUID=$LUKS_UUID none luks,noauto" >> /etc/crypttab

Configure the host. This command is idempotent and can be rerun after a partial failure or configuration change. It also installs the Loki Docker plugin, Promtail, node exporter, the scheduled backup job, and the required module repositories.

bash ./ubuntu-install.sh

Finish the configuration by adding secrets:

# Add passwrods
htpasswd -B -C 12 -c /etc/nginx/.htpasswd cloud
chown root:www-data /etc/nginx/.htpasswd
chmod 640 /etc/nginx/.htpasswd

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
rsync -aHAX root@10.0.0.2:/srv/secure/rclone-config/rclone.conf /srv/secure/rclone-config/rclone.conf
rsync -aHAX root@10.0.0.2:/srv/secure/secrets/cloudflare.ini /srv/secure/secrets/cloudflare.ini
rsync -aHAX root@10.0.0.2:/etc/letsencrypt/ /etc/letsencrypt/
# OR
python3 -m agentd.api ssl_wildcard

Encrypt /root/appdata-luks-header-<uuid>.img before transferring it, then remove the plaintext copy. See LUKS.md.

reboot

After every reboot, Ubuntu and SSH are available but application services stay stopped. From a root login shell, unlock, mount, and start them with:

# Mount encrypted partitions
systemctl start systemd-cryptsetup@appdata.service
# Start applications
systemctl start odoo-app.target

The systemd drop-ins installed by ubuntu-install.sh pull in the encrypted bind mounts and prevent protected services from starting if a required mount fails. Use systemctl status odoo-app.target to inspect the services.

Unattended upgrades continue to download and install updates, but needrestart is configured to list services using outdated libraries instead of restarting them. Those services keep their old mapped libraries until an operator explicitly restarts them or performs the controlled reboot, LUKS unlock, and application startup procedure above. Plan that maintenance so security fixes take effect.

The encrypted filesystem also contains nginx request-body temporary files, agent temporary files and rotating agent logs.

Backup setup (Client-Side Encrypted S3 Backups):

  • All database dumps and Odoo filestores are encrypted client-side via rclone's crypt backend before upload to AWS S3.
  • Generate an obscured password for rclone config:
    • rclone obscure 'YourStrongSecretPassphrase' --config /srv/secure/rclone-config/rclone.conf (Important: Back up this passphrase in an offline password manager. If lost, encrypted backups cannot be recovered!)
  • /etc/cron.d/deploy-manager19 runs the scheduled backup at 00:30 daily.
Decrypting a single file without rclone:

To manually decrypt a downloaded file without rclone (using only Python and pip install pynacl):

  • python3 docs/decrypt.py <encrypted_file> <decrypted_file> <password>

Other notes

Pulling the image

  • docker pull ghcr.io/elmeriniemela/odoo-src:19.0

DB isolation:

Creating a personal github access token (write packages only):

Image development and publishing

Production hosts pull the public image without GitHub credentials. Building and publishing require a developer workstation authenticated to GHCR with package write permission.

SSH key setup (optional, repos are public)

  • scp .gitconfig 19.eniemela.fi:
  • cd .ssh && ssh-keygen -f id_ecdsa -t ecdsa -b 521
  • cat id_ecdsa.pub
  • go to github / settings / SSH keys / Add 'Odoo 19.0 Hetzner Server key'

Local mermaid-cli installation for AGENTS.md verification of the diagram:

  • Install sudo pacman -S mermaid-cli for your development platform.
  • See AGENTS.md for the rerunnable validation commands.

Tests

  • python3 -m unittest discover -s agentd/tests -t .
  • coverage run -m unittest discover -s agentd/tests -t . && coverage report -m

Random notes

  • Docker logs
  • Docker volumes: ls /var/lib/docker/volumes
  • Delete everything: docker system prune -a --volumes
  • Login as root: docker exec -it -u root <uid> bash

About

Deployment manager for Odoo images

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages