{"id":18465549,"url":"https://github.com/numtide/zero-to-odoo","last_synced_at":"2025-09-07T02:05:15.499Z","repository":{"id":249963993,"uuid":"829874632","full_name":"numtide/zero-to-odoo","owner":"numtide","description":null,"archived":false,"fork":false,"pushed_at":"2024-07-24T09:07:19.000Z","size":14,"stargazers_count":19,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-02T23:55:51.183Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Nix","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/numtide.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-17T07:03:25.000Z","updated_at":"2025-02-22T20:33:18.000Z","dependencies_parsed_at":"2024-07-24T12:14:32.923Z","dependency_job_id":null,"html_url":"https://github.com/numtide/zero-to-odoo","commit_stats":null,"previous_names":["numtide/zero-to-odoo"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numtide%2Fzero-to-odoo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numtide%2Fzero-to-odoo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numtide%2Fzero-to-odoo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numtide%2Fzero-to-odoo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/numtide","download_url":"https://codeload.github.com/numtide/zero-to-odoo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247804587,"owners_count":20999017,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T09:13:31.261Z","updated_at":"2025-04-08T08:31:54.383Z","avatar_url":"https://github.com/numtide.png","language":"Nix","funding_links":[],"categories":[],"sub_categories":[],"readme":"# It takes 68 steps to deploy Odoo with NixOS\n\nAfter spending a few years providing NixOS consulting, and building tooling\naround it, it's time to take account. How hard is it to go from zero to a\ndeployed application running on NixOS?\n\nThe goal of this article is to serve as a benchmark for future tooling to\nreduce the number of steps needed to deploy NixOS from scratch. And reduce the\nTotal Cost of Ownership for using bare-metal and self-hosting vs cloud and\nSaaS.\n\nFor this exercise, I picked Odoo as the application, a popular CRM in the\nEnterprise world.\n\nHere we go (skip to the [conclusion](#conclusion) if you are not technical).\n\n## Prerequisites (10 steps)\n\nSkill level required: advanced.\n\nI will assume you have access to a few things already and count those as\nsteps.\n\n 1. A Linux machine with:\n 2. [Nix](https://nixos.org/nix) installed on it.\n 3. [direnv](https://direnv.net) installed on it.\n 4. A SSH key generated with `ssh-keygen -t ed25519`\n 5. A corresponding age key\n 6. A [Hetzner](https://hetzner.com) account.\n 7. A credit card.\n 8. A domain (we're using `ntd.one`).\n 9. A DNS provider.\n10. A S3-compatible object store (we're using Cloudflare R2).\n\n## Order server (6 steps)\n\nLet's get a nice machine to put the service on it. Our friends at Hetzner\noffer incredibly cheap bare-metal servers that are 5-10x less expensive than\nAWS VMs. Price: EUR 54.7/month, plus EUR 46.41 setup fee.\n\n1. Order \u003chttps://www.hetzner.com/dedicated-rootserver/matrix-ax/\u003e\n2. AX42 is plenty enough. \u003chttps://www.hetzner.com/dedicated-rootserver/ax42/configurator/#/\u003e\n3. Keep all the defaults with the rescue system.\n4. Add your SSH public key (from `~/.ssh/id_ed25519.pub`)\n5. Order.\n6. In a few minutes/hours, get back an email with the host's addresses.\n\n`! ipv4=65.21.223.114` `! ipv6=2a01:4f9:3071:295c::2`\n\n## Repo init (3 steps)\n\nWhile the server is prepared, let's create a bare repository to hold the\nconfiguration. I will use [blueprint](https://github.com/numtide/blueprint) to\nreduce the amount of glue code and save a few steps.\n\n```console\n$ mkdir -p ~/src/zero-to-odoo\n$ cd ~/src/zero-to-odoo\n$ nix flake init --template github:numtide/blueprint\n\nwrote: /home/zimbatm/src/zero-to-odoo/flake.nix\n```\n\nThis creates a basic skeleton that we will populate with more content.\n\n### Add flake inputs (7 steps)\n\nAdd a few more dependencies we are going to need later.\n\nWe take some extra effort to compress the dependency tree to keep things lean.\nThis requires inspecting each dependency with `nix flake metadata` and then\nconnecting the inputs using the \"follows\" mechanism.\n\n```diff\n\ndiff --git a/flake.nix b/flake.nix\n\nindex af07574..27ce2ee 100644\n--- a/flake.nix\n+++ b/flake.nix\n@@ -6,8 +6,6 @@\n     nixpkgs.url = \"github:NixOS/nixpkgs?ref=nixos-unstable\";\n     blueprint.url = \"github:numtide/blueprint\";\n     blueprint.inputs.nixpkgs.follows = \"nixpkgs\";\n+    disko.url = \"github:nix-community/disko\";\n+    disko.inputs.nixpkgs.follows = \"nixpkgs\";\n+    sops-nix.url = \"github:mic92/sops-nix\";\n+    sops-nix.inputs.nixpkgs.follows = \"nixpkgs\";\n+    sops-nix.inputs.nixpkgs-stable.follows = \"\";\n+    srvos.url = \"github:nix-community/srvos\";\n+    srvos.inputs.nixpkgs.follows = \"nixpkgs\";\n   };\n```\n\n### Add devshell with a couple of tools (2 steps)\n\nCreate a shell environment with all the tools we're going to need.\n\n\u003e In reality, I had to come back a few times to add missing dependencies.\n\nAdd: [$ devshell.nix](devshell.nix) as nix\n\n```nix\n{ pkgs, perSystem }:\npkgs.mkShellNoCC {\n  packages = [\n    perSystem.sops-nix.default\n    pkgs.nixos-anywhere\n    pkgs.nixos-rebuild\n    pkgs.age\n    pkgs.pwgen\n    pkgs.sops\n    pkgs.ssh-to-age\n  ];\n}\n```\n\n```console\n$ git add devshell.nix\n```\n\n### Configure direnv (2 steps)\n\nConfigure direnv to automatically load the tools into the environment when\nentering the project folder.\n\nAdd: [$ .envrc](.envrc) as shell\n\n```shell\n#!/usr/bin/env bash\n\nwatch_file devshell.nix\n\nuse flake\n```\n\n```console\ndirenv: error /home/zimbatm/src/zero-to-odoo/.envrc is blocked. Run `direnv allow` to approve its content\n\n$ direnv allow\n\ndirenv: loading ~/src/zero-to-odoo/.envrc\n\ndirenv: using flake\n\nwarning: Git tree '/home/zimbatm/src/zero-to-odoo' is dirty\n\ndirenv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_BUILD_TOP +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_LDFLAGS +NIX_STORE +NM +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +TEMP +TEMPDIR +TMP +TMPDIR +__structuredAttrs +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +preferLocalBuild +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~XDG_DATA_DIRS\n```\n\n### Prepare your user (5 steps)\n\nWe are going to generate an AGE key from our SSH private key.\n\n\u003e NOTE: the age key is stored decrypted at rest. This is a limitation of age.\n\n```console\n$ mkdir -p ~/.config/sops/age\n$ ssh-to-age -private-key -i ~/.ssh/id_ed25519 \u003e\u003e ~/.config/sops/age/keys.txt\n```\n\nThen, add our user information to the repo, making place for potentially more\nusers in the future.\n\n`! USER=zimbatm`\n\n```console\n$ mkdir -p users/$USER\n$ cat ~/.ssh/id_ed25519.pub \u003e users/$USER/authorized_keys\n$ git add users\n```\n\n### Prepare some shared configuration (3 steps)\n\nCreate a NixOS module with some basic configuration we can share will all the\npotential future servers.\n\n\u003e In reality, I had to come back a few times.\n\n```console\n$ mkdir -p modules/nixos\n```\n\nAdd: `\u003e modules/nixos/server.nix`\n\n```nix\n{ inputs, flake, ... }:\n{\n  imports = [\n    inputs.disko.nixosModules.default\n    inputs.sops-nix.nixosModules.default\n    inputs.srvos.nixosModules.server\n  ];\n  \n  # Allow you to SSH to the servers as root\n  users.users.root.openssh.authorizedKeys.keyFiles = [\n    \"${flake}/users/zimbatm/authorized_keys\"\n  ];\n}\n```\n\n```console\n$ git add modules\n```\n\n## Host bootstrap\n\nOk, the base skeleton is in place. Next, configure and deploy a naked\nconfiguration to the host.\n\n### Bind DNS entry (2 steps)\n\nUse your DNS provider to bind the IPv4 and IPv6 to it.\n\n* `odoo.$domain.\t\t300\tIN\tA\t$ipv4`\n* `odoo.$domain.\t\t300\tIN\tAAAA\t$ipv6`\n\n### Prepare the host configuration (5 steps)\n\nOur machine is going to be called \"odoo1\" (this is my weird naming scheme).\n\n```console\n$ mkdir -p hosts/odoo1\n```\n\nWe are going to use [disko](https://github.com/nix-community/disko) to\npartition the machine declaratively. This saves 5-10 steps from the original\nNixOS installation manual.\n\nGetting this configuration right usually takes a few iterations, but we are\nlucky, I had a ZFS config from another machine.\n\nAdd: `\u003e hosts/odoo1/disko.nix` as nix\n\n```nix\n{ ... }:\nlet\n  mirrorBoot = idx: {\n    type = \"disk\";\n    device = \"/dev/nvme${idx}n1\";\n    content = {\n      type = \"gpt\";\n      partitions = {\n        ESP = {\n          size = \"512M\";\n          type = \"EF00\";\n          content = {\n            type = \"filesystem\";\n            format = \"vfat\";\n            mountpoint = \"/boot${idx}\";\n          };\n        };\n        zfs = {\n          size = \"100%\";\n          content = {\n            type = \"zfs\";\n            pool = \"zroot\";\n          };\n        };\n      };\n    };\n  };\nin\n{\n  boot.loader.grub = {\n    enable = true;\n    efiSupport = true;\n    efiInstallAsRemovable = true;\n    mirroredBoots = [\n      {\n        path = \"/boot0\";\n        devices = [ \"nodev\" ];\n      }\n      {\n        path = \"/boot1\";\n        devices = [ \"nodev\" ];\n      }\n    ];\n  };\n\n  disko.devices = {\n    disk = {\n      x = mirrorBoot \"0\";\n      y = mirrorBoot \"1\";\n    };\n\n    zpool = {\n      zroot = {\n        type = \"zpool\";\n        rootFsOptions = {\n          compression = \"lz4\";\n          \"com.sun:auto-snapshot\" = \"true\";\n        };\n        datasets = {\n          \"root\" = {\n            type = \"zfs_fs\";\n            options.mountpoint = \"none\";\n            mountpoint = null;\n          };\n          \"root/nixos\" = {\n            type = \"zfs_fs\";\n            options.mountpoint = \"/\";\n            mountpoint = \"/\";\n          };\n        };\n      };\n    };\n  };\n}\n```\n\nNext, add the main NixOS configuration. We already have the Hetzner hardware\nconfiguration in [SrvOS](https://github.com/nix-community/srvos), which saves\nus a few steps here.\n\n\u003e This led Mic92 and I to re-think why the hostId is needed. It won't be\n\u003e necessary once \u003chttps://github.com/nix-community/srvos/pull/465\u003e is merged.\n\n[$ hosts/odoo1/configuration.nix](hosts/odoo1/configuration.nix) as nix\n\n```nix\n{ inputs, flake, ... }:\n{\n  imports = [\n    ./disko.nix\n    ./odoo.nix\n    flake.nixosModules.server\n    # The Hetzner hardware config is handled by SrvOS\n    inputs.srvos.nixosModules.hardware-hetzner-online-amd\n  ];\n\n  # The machine architecture.\n  nixpkgs.hostPlatform = \"x86_64-linux\";\n\n  # The machine hostname.\n  networking.hostName = \"odoo1\";\n\n  # Needed by ZFS. `head -c4 /dev/urandom | od -A none -t x4`\n  networking.hostId = \"ceb8cad3\";\n\n  # Needed because Hetzner Online doesn't provide RA. Replace the IPv6 with your own.\n  systemd.network.networks.\"10-uplink\".networkConfig.Address = \"2a01:4f9:3071:295c::2\";\n\n  # Load secrets from this file.\n  sops.defaultSopsFile = ./secrets.yaml;\n\n  # Used by NixOS to handle state changes.\n  system.stateVersion = \"24.05\";\n}\n```\n\n```console\n# Add some blank odoo config for now.\n$ echo '{}' \u003e hosts/odoo1/odoo.nix\n$ git add hosts/odoo1\n```\n\nNow, we have almost everything needed to deploy a blank machine.\n\n### Bootstrap SOPS (6 steps)\n\nWe lean on SOPS and sops-nix to share secrets between the deployer (me) and\nthe machine. The nice thing about this approach is that it doesn't require\nextra infrastructure like Vault to store the secrets while still keeping them\nencrypted at rest.\n\nWe generate the target machine SSH host key so we know what its public\ncertificate is going to be in advance.\n\n```console\n# Generate a SSH key for the host\n$ ssh-keygen -t ed25519 -N \"\" -f hosts/odoo1/ssh_host_ed25519_key\n# Configure sops\n$ cat \u003c\u003cSOPS \u003e .sops.yaml\n\ncreation_rules:\n  - path_regex: ^hosts/odoo1/secrets.yaml$\n    key_groups:\n      - age:\n        - $(ssh-to-age -i hosts/odoo1/ssh_host_ed25519_key.pub)\n        - $(ssh-to-age -i users/$USER/authorized_keys)\nSOPS\n# Generate the host secret file\n\ncat \u003c\u003cSECRETS \u003e hosts/odoo1/secrets.yaml\n\nssh_host_ed25519_key: |\n$(sed \"s/^/  /\" \u003c hosts/odoo1/ssh_host_ed25519_key)\nSECRETS\n# Now encrypt the file\n$ sops --encrypt --in-place hosts/odoo1/secrets.yaml\n# Remove the unencrypted private host key\n$ rm hosts/odoo1/ssh_host_ed25519_key\n# Add things to git for flakes\n$ git add hosts/odoo1\n```\n\n### Bootstrap the host (8 steps)\n\nIt's time to deploy the host.\n\nWe use [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) to\nlive-replace the target machine with our desired disk partitioning and NixOS\nconfiguration. This saves us a lot of steps as we don't have to faff around\nwith ISOs, or figuring how the host provider handles IPXE or other system\nimages. If the host provider supports Ubuntu, Debian or Fedora, we just\nreplace it.\n\n```console\n# Prepare the SSH host key to upload\n$ temp=$(mktemp -d)\n$ install -d -m755 \"$temp/etc/ssh\"\n$ sops --decrypt --extract '[\"ssh_host_ed25519_key\"]' hosts/odoo1/secrets.yaml \u003e \"$temp/etc/ssh/ssh_host_ed25519_key\"\n$ chmod 600 \"$temp/etc/ssh/ssh_host_ed25519_key\"\n\n# Deploy!\n$ nixos-anywhere --extra-files \"$temp\" --flake .#odoo1 root@odoo.ntd.one\n\u003csnip\u003e\ncopying path '/nix/store/zqwbhdf7ljq6rh6rbb7qn078k4srcsva-linux-6.6.39-modules' from 'https://cache.nixos.org'...\ncopying path '/nix/store/kk8vvdihcbpw7gl5kdiddx19rdhak07q-firmware' from 'https://cache.nixos.org'...\ncopying path '/nix/store/8cjsjjf11pw52632q25zprjwz8r8bvaj-etc-modprobe.d-firmware.conf' from 'https://cache.nixos.org'...\n### Installing NixOS ###\nPseudo-terminal will not be allocated because stdin is not a terminal.\ninstalling the boot loader...\nsetting up /etc...\nupdating GRUB 2 menu...\ninstalling the GRUB 2 boot loader into /boot0...\nInstalling for x86_64-efi platform.\nInstallation finished. No error reported.\nupdating GRUB 2 menu...\ninstalling the GRUB 2 boot loader into /boot1...\nInstalling for x86_64-efi platform.\nInstallation finished. No error reported.\ninstallation finished!\numount: /mnt/boot1 unmounted\n\numount: /mnt/boot0 unmounted\n\numount: /mnt (zroot/root/nixos) unmounted\n### Waiting for the machine to become reachable again ###\nWarning: Permanently added '65.21.223.114' (ED25519) to the list of known hosts.\n### Done! ###\n\n# Cleanup\n$ rm -rf \"$temp\"\n```\n\nThe machine should now be a blank machine with just SSH up and running. Let's\ntest this!\n\n```console\n# Add the host to our list of known hosts\n$ echo \"odoo.$domain $(\u003c hosts/odoo1/ssh_host_ed25519_key.pub)\" \u003e\u003e ~/.ssh/known_hosts\n$ ssh root@65.21.223.114\n\nLast login: Mon Jul 15 09:49:34 2024 from 178.196.175.78\n\n[root@odoo1:~]# \n```\n\nOk, that works!\n\n## Deploy Odoo\n\nNow that the machine is up and running, let's deploy Odoo on it.\n\nThe general approach to configuring a NixOS service is to:\n\n1. [Search the NixOS configuration](https://search.nixos.org/options?channel=24.05\u0026from=0\u0026size=50\u0026sort=relevance\u0026type=packages\u0026query=odoo)\n2. [Search Github](https://github.com/search?q=language%3ANix+odoo\u0026type=code)\n\n(1) lets you know if NixOS includes that service and all related options. And (2) shows you how other users are doing it.\n\n\u003e While writing this article I found that Odoo wasn't very well supported in nixpkgs. The rest of the article depends on those PRs being available in nixos-unstable. Always be upstreaming. \u003chttps://github.com/NixOS/nixpkgs/pull/327641\u003e \u003chttps://github.com/NixOS/nixpkgs/pull/327729\u003e\n\n### NixOS modules (5 step)\n\nAdd the following to: [$ hosts/odoo1/odoo.nix](hosts/odoo1/odoo.nix) as nix\n\n```nix\n{ inputs, config, lib, ... }:\nlet\n  domain = \"odoo.ntd.one\";\nin\n{\n  imports = [\n    # Enable Nginx with good defaults.\n    inputs.srvos.nixosModules.mixins-nginx\n  ];\n\n  # Basic Odoo config.\n  services.odoo = {\n    enable = true;\n    domain = domain;\n    # install addons declaratively.\n    addons = [ ];\n    # add the demo database\n    autoInit = true;\n  };\n\n  # Enable Let's Encrypt and HTTPS by default.\n  services.nginx.virtualHosts.${domain} = {\n    enableACME = true;\n    forceSSL = true;\n  };\n\n  # Daily snapshots of the database.\n  services.postgresqlBackup = {\n    enable = true;\n    databases = [ \"odoo\" ];\n    # Let restic handle the compression so it can de-duplicate chunks.\n    compression = \"none\";\n  };\n\n  # Backup and restore\n  sops.secrets.restic_odoo_password = {};\n  sops.secrets.restic_odoo_environment = {};\n  services.restic.backups.\"odoo\" = {\n    initialize = true;\n    paths = [\n      \"/var/lib/private/odoo\"\n      \"/var/backup/postgresql\"\n    ];\n    pruneOpts = [\n      \"--keep-daily 5\"\n      \"--keep-weekly 3\"\n      \"--keep-monthly 2\"\n    ];\n    environmentFile = config.sops.secrets.restic_odoo_environment.path;\n    passwordFile = config.sops.secrets.restic_odoo_password.path;\n    # We use Cloudflare R2 for this demo, but use whatever works for you.\n    repository = \"s3:186a9b0a6ef4bf5c3792c9f4b4ebfbda.r2.cloudflarestorage.com/zero-to-infra-odoo\";\n    timerConfig.OnCalendar = \"hourly\";\n  };\n}\n```\n\nAdd the secrets:\n\n```console\n$ sops --set '[\"restic_odoo_password\"] \"'$(pwgen 32 1)'\"' hosts/odoo1/secrets.yaml\n# Provided by Cloudflare R2\n$ cat \u003c\u003cENV_FILE \u003e env_file\nAWS_ACCESS_KEY_ID=e45ae998fe51bd166399c46bbe8be2e5\nAWS_SECRET_ACCESS_KEY=6dddd70cbc95a81a73223e742d6d575c1bb11ef0f16fb86db838bdc58422399b\nENV_FILE\n$ sops --set '[\"restic_odoo_environment'] '\"$(jq -Rs . \u003c env_file)\" hosts/odoo1/secrets.yaml\n$ rm env_file\n```\n\nThis is the bare minimum.\n\nWe raise the bar from 99% of blog posts out there by including backup to the\nbare minimum.\n\n### Deploy changes (4 step)\n\n```console\n$ nixos-rebuild --flake .#odoo1 --target-host root@odoo.ntd.one switch\n\u003csnip\u003e\n```\n\nThe former blank machine now has Odoo running with some demo data, Nginx in\nfront with HTTPS, Postgres. \u003chttps://odoo.ntd.one\u003e (default credentials are\nadmin/admin).\n\nTo test that backups are working, trigger them manually:\n\n```console\n$ ssh root@odoo.ntd.one\n\n[root@odoo1:~]# systemctl start postgresqlBackup-odoo.service\n\n[root@odoo1:~]# ls /var/backup/postgresql/\nodoo.sql\n\n[root@odoo1:~]# systemctl start restic-backups-odoo.service\n\n[root@odoo1:~]# journalctl -u restic-backups-odoo.service --no-pager\n\u003csnip\u003e\nJul 17 12:57:07 odoo1 restic[11533]: no parent snapshot found, will read all files\n\nJul 17 12:57:09 odoo1 restic[11533]: Files:        1135 new,     0 changed,     0 unmodified\n\nJul 17 12:57:09 odoo1 restic[11533]: Dirs:          489 new,     0 changed,     0 unmodified\n\nJul 17 12:57:09 odoo1 restic[11533]: Added to the repository: 53.299 MiB (12.709 MiB stored)\nJul 17 12:57:09 odoo1 restic[11533]: processed 1135 files, 65.890 MiB in 0:02\n\nJul 17 12:57:09 odoo1 restic[11533]: snapshot 6c40eb6f saved\n\nJul 17 12:57:12 odoo1 restic[11568]: Applying Policy: keep 5 daily, 3 weekly, 2 monthly snapshots\n\nJul 17 12:57:12 odoo1 restic[11568]: keep 1 snapshots:\nJul 17 12:57:12 odoo1 restic[11568]: ID        Time                 Host        Tags        Reasons           Paths\n\nJul 17 12:57:12 odoo1 restic[11568]: -----------------------------------------------------------------------------------------------\nJul 17 12:57:12 odoo1 restic[11568]: 6c40eb6f  2024-07-17 12:57:05  odoo1                   daily snapshot    /var/backup/postgresql\n\nJul 17 12:57:12 odoo1 restic[11568]:                                                        weekly snapshot   /var/lib/private/odoo\n\nJul 17 12:57:12 odoo1 restic[11568]:                                                        monthly snapshot\n\nJul 17 12:57:12 odoo1 restic[11568]: -----------------------------------------------------------------------------------------------\nJul 17 12:57:12 odoo1 restic[11568]: 1 snapshots\n\nJul 17 12:57:12 odoo1 systemd[1]: restic-backups-odoo.service: Deactivated successfully.\nJul 17 12:57:12 odoo1 systemd[1]: Finished restic-backups-odoo.service.\nJul 17 12:57:12 odoo1 systemd[1]: restic-backups-odoo.service: Consumed 4.860s CPU time, received 62.3K IP traffic, sent 12.8M IP traffic.\n```\n\n## Conclusion\n\nOne of the best feelings with NixOS is how few moving pieces there are. I know\nthis service will run for the next year with minimal intervention. If anything\nbreaks, I can rollback to a previous deployment. Or order another machine and\nrestore from backups. And there is zero vendor lock-in; I can replace all the\nproviders with an alternative.\n\nTo get there, 68 steps is still relatively substantial. It took me around a\nday and a half to get everything up and running, including a few side quests\nand taking those notes. For a novice, it would probably take a lot more trial\nand errors. In particular:\n\n* Getting the disk layout right (it takes a lot of reboots).\n* Figuring out the proper project structure and how to glue everything together.\n* SOPS secret bootstrapping.\n\nA production environment would also include other aspects which I didn't have\ntime to cover in this article:\n\n* Automated dependency updates.\n* Monitoring.\n* CI and binary cache.\n* GitOps.\n* Developer shell for Odoo addon development.\n\nThere is an opportunity to compress the number of steps needed, and I am\ninterested in making this happen one way or another. If you are working in\nthis area, ping me.\n\nI hope you saw some interesting things in this article.\n\nSee you!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumtide%2Fzero-to-odoo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnumtide%2Fzero-to-odoo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumtide%2Fzero-to-odoo/lists"}