odoo-devops-tools 1.24.2__tar.gz → 1.24.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-devops-tools
3
- Version: 1.24.2
3
+ Version: 1.24.4
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -134,7 +134,7 @@ Then start the Docker Compose environment:
134
134
 
135
135
  ```bash
136
136
  cd ./odoo19
137
- docker compose up -d --build
137
+ docker compose up -d --build --wait
138
138
  ```
139
139
 
140
140
  Optionally initialize the Odoo database from the command line instead of through the web interface:
@@ -174,7 +174,7 @@ Sync the configured addon repositories and refresh the generated Docker workflow
174
174
 
175
175
  ```bash
176
176
  odoo-compose --sync-addons
177
- docker compose up -d --build
177
+ docker compose up -d --build --wait
178
178
  ```
179
179
 
180
180
  The addon repositories are cloned into `ROOT/odoo-addons/oca-web/` and `ROOT/odoo-addons/oca-helpdesk/` and bind-mounted into the Odoo container.
@@ -215,7 +215,7 @@ Sync the Odoo source and generate the Docker artifacts:
215
215
 
216
216
  ```bash
217
217
  odoo-compose --sync-all
218
- docker compose up -d --build
218
+ docker compose up -d --build --wait
219
219
  ```
220
220
 
221
221
  The resolved Odoo source is bind-mounted read-only at `/opt/odoo`. The Odoo Python source from the base image is removed while its installed dependencies remain available.
@@ -226,14 +226,14 @@ Enable Mailpit in the local Docker workflow when you need to inspect or test ema
226
226
 
227
227
  ```bash
228
228
  odoo-compose --set docker:mailpit_enabled=true
229
- docker compose up -d --build
229
+ docker compose up -d --build --wait
230
230
  ```
231
231
 
232
232
  Mailpit is available at http://localhost:8025 by default. When running multiple workspaces, override the host web UI port, for example:
233
233
 
234
234
  ```bash
235
235
  odoo-compose --set docker:mailpit_enabled=true --set docker:mailpit_webui_port=8026
236
- docker compose up -d --build
236
+ docker compose up -d --build --wait
237
237
  ```
238
238
 
239
239
  The generated local Odoo configuration sends outgoing email to `mailpit:1025`. When Mailpit is enabled, local Docker SMTP connection settings (`smtp_server`, `smtp_port`, `smtp_ssl`, `smtp_user`, and `smtp_password`) are intentionally overridden; the deploy configuration keeps the values from `[config]` unchanged.
@@ -256,7 +256,33 @@ target = /mnt/exchange
256
256
 
257
257
  `odoo-compose` does not create the source directory or change its ownership or permissions; host provisioning or the deployment process must prepare the directory before Docker starts.
258
258
 
259
- ### 1.5. Script reference
259
+ ### 1.5. Limiting Docker Compose resources
260
+
261
+ You can optionally set CPU and memory limits for each service in the generated `compose.yaml` using `[docker]` in `odoo-project.ini`:
262
+
263
+ ```ini
264
+ [docker]
265
+ odoo_cpus = 2.0
266
+ odoo_mem_limit = 4g
267
+
268
+ db_cpus = 1.0
269
+ db_mem_limit = 2g
270
+
271
+ mailpit_enabled = true
272
+ mailpit_cpus = 0.5
273
+ mailpit_mem_limit = 256m
274
+ ```
275
+
276
+ Each option is independent and optional. If an option is missing or empty, `odoo-compose` omits that limit from the Compose service; no CPU or memory limit is imposed by the generator. CPU values must be positive decimal numbers (such as `0.5` or `2.0`). Memory limits must be positive amounts (such as `256m`, `2g`, or `4g`); zero and negative values are rejected. A memory limit is a hard container limit: exceeding it may cause an out-of-memory termination, so size it for your workload.
277
+
278
+ Regenerate the Docker configuration after editing the INI, then start the services:
279
+
280
+ ```bash
281
+ odoo-compose
282
+ docker compose up -d --build --wait
283
+ ```
284
+
285
+ ### 1.6. Script reference
260
286
 
261
287
  The Docker workflow generates platform-specific helper scripts under `ROOT/docker/local/scripts/`. Unix-like systems generate `.sh` helpers; Windows generates the corresponding `.bat` helpers.
262
288
 
@@ -283,7 +309,7 @@ ODOO_DOCKER_SUDO=1 ./docker/local/scripts/run.sh --help
283
309
 
284
310
  Docker helpers use `[config].db_name` when configured, or `odoo` by default.
285
311
 
286
- #### 1.5.1. run
312
+ #### 1.6.1. run
287
313
 
288
314
  Runs Odoo in a one-off Docker container for the configured database:
289
315
 
@@ -297,7 +323,7 @@ Any extra arguments are passed through to Odoo:
297
323
  ./docker/local/scripts/run.sh -i base --stop-after-init
298
324
  ```
299
325
 
300
- #### 1.5.2. shell
326
+ #### 1.6.2. shell
301
327
 
302
328
  Opens an Odoo shell for the configured database:
303
329
 
@@ -307,7 +333,7 @@ Opens an Odoo shell for the configured database:
307
333
 
308
334
  Any extra arguments are passed through to the underlying Odoo shell command.
309
335
 
310
- #### 1.5.3. update
336
+ #### 1.6.3. update
311
337
 
312
338
  Updates installed addons using `click-odoo-update`:
313
339
 
@@ -321,7 +347,7 @@ Any extra arguments are passed through to `click-odoo-update`:
321
347
  ./docker/local/scripts/update.sh --update-all
322
348
  ```
323
349
 
324
- #### 1.5.4. Database backup
350
+ #### 1.6.4. Database backup
325
351
 
326
352
  Create a PostgreSQL custom-format dump under `ROOT/odoo-backups/`:
327
353
 
@@ -341,7 +367,7 @@ Pass an output path explicitly when needed:
341
367
  ./docker/local/scripts/backup-db.sh ./odoo-backups/pre-upgrade.dump
342
368
  ```
343
369
 
344
- #### 1.5.5. Database restore
370
+ #### 1.6.5. Database restore
345
371
 
346
372
  Restore a database dump with `pg_restore`:
347
373
 
@@ -349,7 +375,7 @@ Restore a database dump with `pg_restore`:
349
375
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
350
376
  ```
351
377
 
352
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
378
+ Database restores use copy semantics by default.
353
379
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
354
380
  database can coexist with the source database:
355
381
 
@@ -372,7 +398,7 @@ docker compose stop odoo
372
398
  docker compose up -d odoo
373
399
  ```
374
400
 
375
- #### 1.5.6. Database neutralization
401
+ #### 1.6.6. Database neutralization
376
402
 
377
403
  For development or test databases restored from production, run the generated neutralization helper explicitly:
378
404
 
@@ -407,7 +433,7 @@ docker compose stop odoo
407
433
  docker compose up -d odoo
408
434
  ```
409
435
 
410
- #### 1.5.7. Filestore backup
436
+ #### 1.6.7. Filestore backup
411
437
 
412
438
  Create a compressed tar archive of the selected database filestore:
413
439
 
@@ -438,7 +464,7 @@ docker compose stop odoo
438
464
  docker compose up -d odoo
439
465
  ```
440
466
 
441
- #### 1.5.8. Filestore restore
467
+ #### 1.6.8. Filestore restore
442
468
 
443
469
  Restore a filestore archive:
444
470
 
@@ -454,7 +480,7 @@ docker compose stop odoo
454
480
  docker compose up -d odoo
455
481
  ```
456
482
 
457
- #### 1.5.9. Restic filestore backup
483
+ #### 1.6.9. Restic filestore backup
458
484
 
459
485
  The generated Docker image also includes `restic`. Restic is an additional option intended especially for large filestores where incremental snapshots and deduplication are useful.
460
486
 
@@ -492,7 +518,7 @@ List or inspect snapshots through the generic wrapper:
492
518
  ./docker/local/scripts/restic.sh check
493
519
  ```
494
520
 
495
- #### 1.5.10. Restic filestore restore
521
+ #### 1.6.10. Restic filestore restore
496
522
 
497
523
  Restore the latest matching filestore snapshot:
498
524
 
@@ -514,7 +540,7 @@ docker compose up -d odoo
514
540
 
515
541
  With `--force`, the restore helper removes the target database filestore before restoring it. When `latest` is used, the snapshot selection is restricted to the current `RESTIC_HOST` and source filestore path. Use the same `RESTIC_HOST` value that was used when the snapshot was created if the restore is performed from another host.
516
542
 
517
- ### 1.6. Creating a Docker deploy build context
543
+ ### 1.7. Creating a Docker deploy build context
518
544
 
519
545
  Use `--create-docker-deploy` to generate a self-contained Docker build context for CI/CD, testing, staging, production, or another non-local deployment workflow.
520
546
 
@@ -787,7 +813,7 @@ Restore a database dump with `pg_restore`:
787
813
  ./odoo-scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
788
814
  ```
789
815
 
790
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
816
+ Database restores use copy semantics by default.
791
817
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
792
818
  database can coexist with the source database:
793
819
 
@@ -1412,6 +1438,10 @@ This section is optional.
1412
1438
  - `mailpit_enabled` — enables a persistent Mailpit service in the local Docker workflow for SMTP and POP3 mail testing. Default: `false`. It does not affect the deploy build context.
1413
1439
  - `mailpit_version` — version of `ghcr.io/axllent/mailpit` in local Compose. Default: `v1.31.3`. An explicitly set value must be a concrete version; empty values and `latest` are invalid.
1414
1440
  - `mailpit_webui_port` — host port used for the Mailpit web UI. Default: `8025`. The internal Mailpit web UI port remains `8025`.
1441
+ - `odoo_cpus`, `db_cpus`, `mailpit_cpus` — optional CPU limits for the respective Compose services; positive decimal CPU counts such as `0.5` or `2.0`.
1442
+ - `odoo_mem_limit`, `db_mem_limit`, `mailpit_mem_limit` — optional memory limits for the respective Compose services; positive amounts such as `256m` or `4g`.
1443
+
1444
+ All six resource options default to unset and are omitted from generated Compose when empty. See [Limiting Docker Compose resources](#17-limiting-docker-compose-resources) for a complete example.
1415
1445
 
1416
1446
  Example:
1417
1447
 
@@ -106,7 +106,7 @@ Then start the Docker Compose environment:
106
106
 
107
107
  ```bash
108
108
  cd ./odoo19
109
- docker compose up -d --build
109
+ docker compose up -d --build --wait
110
110
  ```
111
111
 
112
112
  Optionally initialize the Odoo database from the command line instead of through the web interface:
@@ -146,7 +146,7 @@ Sync the configured addon repositories and refresh the generated Docker workflow
146
146
 
147
147
  ```bash
148
148
  odoo-compose --sync-addons
149
- docker compose up -d --build
149
+ docker compose up -d --build --wait
150
150
  ```
151
151
 
152
152
  The addon repositories are cloned into `ROOT/odoo-addons/oca-web/` and `ROOT/odoo-addons/oca-helpdesk/` and bind-mounted into the Odoo container.
@@ -187,7 +187,7 @@ Sync the Odoo source and generate the Docker artifacts:
187
187
 
188
188
  ```bash
189
189
  odoo-compose --sync-all
190
- docker compose up -d --build
190
+ docker compose up -d --build --wait
191
191
  ```
192
192
 
193
193
  The resolved Odoo source is bind-mounted read-only at `/opt/odoo`. The Odoo Python source from the base image is removed while its installed dependencies remain available.
@@ -198,14 +198,14 @@ Enable Mailpit in the local Docker workflow when you need to inspect or test ema
198
198
 
199
199
  ```bash
200
200
  odoo-compose --set docker:mailpit_enabled=true
201
- docker compose up -d --build
201
+ docker compose up -d --build --wait
202
202
  ```
203
203
 
204
204
  Mailpit is available at http://localhost:8025 by default. When running multiple workspaces, override the host web UI port, for example:
205
205
 
206
206
  ```bash
207
207
  odoo-compose --set docker:mailpit_enabled=true --set docker:mailpit_webui_port=8026
208
- docker compose up -d --build
208
+ docker compose up -d --build --wait
209
209
  ```
210
210
 
211
211
  The generated local Odoo configuration sends outgoing email to `mailpit:1025`. When Mailpit is enabled, local Docker SMTP connection settings (`smtp_server`, `smtp_port`, `smtp_ssl`, `smtp_user`, and `smtp_password`) are intentionally overridden; the deploy configuration keeps the values from `[config]` unchanged.
@@ -228,7 +228,33 @@ target = /mnt/exchange
228
228
 
229
229
  `odoo-compose` does not create the source directory or change its ownership or permissions; host provisioning or the deployment process must prepare the directory before Docker starts.
230
230
 
231
- ### 1.5. Script reference
231
+ ### 1.5. Limiting Docker Compose resources
232
+
233
+ You can optionally set CPU and memory limits for each service in the generated `compose.yaml` using `[docker]` in `odoo-project.ini`:
234
+
235
+ ```ini
236
+ [docker]
237
+ odoo_cpus = 2.0
238
+ odoo_mem_limit = 4g
239
+
240
+ db_cpus = 1.0
241
+ db_mem_limit = 2g
242
+
243
+ mailpit_enabled = true
244
+ mailpit_cpus = 0.5
245
+ mailpit_mem_limit = 256m
246
+ ```
247
+
248
+ Each option is independent and optional. If an option is missing or empty, `odoo-compose` omits that limit from the Compose service; no CPU or memory limit is imposed by the generator. CPU values must be positive decimal numbers (such as `0.5` or `2.0`). Memory limits must be positive amounts (such as `256m`, `2g`, or `4g`); zero and negative values are rejected. A memory limit is a hard container limit: exceeding it may cause an out-of-memory termination, so size it for your workload.
249
+
250
+ Regenerate the Docker configuration after editing the INI, then start the services:
251
+
252
+ ```bash
253
+ odoo-compose
254
+ docker compose up -d --build --wait
255
+ ```
256
+
257
+ ### 1.6. Script reference
232
258
 
233
259
  The Docker workflow generates platform-specific helper scripts under `ROOT/docker/local/scripts/`. Unix-like systems generate `.sh` helpers; Windows generates the corresponding `.bat` helpers.
234
260
 
@@ -255,7 +281,7 @@ ODOO_DOCKER_SUDO=1 ./docker/local/scripts/run.sh --help
255
281
 
256
282
  Docker helpers use `[config].db_name` when configured, or `odoo` by default.
257
283
 
258
- #### 1.5.1. run
284
+ #### 1.6.1. run
259
285
 
260
286
  Runs Odoo in a one-off Docker container for the configured database:
261
287
 
@@ -269,7 +295,7 @@ Any extra arguments are passed through to Odoo:
269
295
  ./docker/local/scripts/run.sh -i base --stop-after-init
270
296
  ```
271
297
 
272
- #### 1.5.2. shell
298
+ #### 1.6.2. shell
273
299
 
274
300
  Opens an Odoo shell for the configured database:
275
301
 
@@ -279,7 +305,7 @@ Opens an Odoo shell for the configured database:
279
305
 
280
306
  Any extra arguments are passed through to the underlying Odoo shell command.
281
307
 
282
- #### 1.5.3. update
308
+ #### 1.6.3. update
283
309
 
284
310
  Updates installed addons using `click-odoo-update`:
285
311
 
@@ -293,7 +319,7 @@ Any extra arguments are passed through to `click-odoo-update`:
293
319
  ./docker/local/scripts/update.sh --update-all
294
320
  ```
295
321
 
296
- #### 1.5.4. Database backup
322
+ #### 1.6.4. Database backup
297
323
 
298
324
  Create a PostgreSQL custom-format dump under `ROOT/odoo-backups/`:
299
325
 
@@ -313,7 +339,7 @@ Pass an output path explicitly when needed:
313
339
  ./docker/local/scripts/backup-db.sh ./odoo-backups/pre-upgrade.dump
314
340
  ```
315
341
 
316
- #### 1.5.5. Database restore
342
+ #### 1.6.5. Database restore
317
343
 
318
344
  Restore a database dump with `pg_restore`:
319
345
 
@@ -321,7 +347,7 @@ Restore a database dump with `pg_restore`:
321
347
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
322
348
  ```
323
349
 
324
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
350
+ Database restores use copy semantics by default.
325
351
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
326
352
  database can coexist with the source database:
327
353
 
@@ -344,7 +370,7 @@ docker compose stop odoo
344
370
  docker compose up -d odoo
345
371
  ```
346
372
 
347
- #### 1.5.6. Database neutralization
373
+ #### 1.6.6. Database neutralization
348
374
 
349
375
  For development or test databases restored from production, run the generated neutralization helper explicitly:
350
376
 
@@ -379,7 +405,7 @@ docker compose stop odoo
379
405
  docker compose up -d odoo
380
406
  ```
381
407
 
382
- #### 1.5.7. Filestore backup
408
+ #### 1.6.7. Filestore backup
383
409
 
384
410
  Create a compressed tar archive of the selected database filestore:
385
411
 
@@ -410,7 +436,7 @@ docker compose stop odoo
410
436
  docker compose up -d odoo
411
437
  ```
412
438
 
413
- #### 1.5.8. Filestore restore
439
+ #### 1.6.8. Filestore restore
414
440
 
415
441
  Restore a filestore archive:
416
442
 
@@ -426,7 +452,7 @@ docker compose stop odoo
426
452
  docker compose up -d odoo
427
453
  ```
428
454
 
429
- #### 1.5.9. Restic filestore backup
455
+ #### 1.6.9. Restic filestore backup
430
456
 
431
457
  The generated Docker image also includes `restic`. Restic is an additional option intended especially for large filestores where incremental snapshots and deduplication are useful.
432
458
 
@@ -464,7 +490,7 @@ List or inspect snapshots through the generic wrapper:
464
490
  ./docker/local/scripts/restic.sh check
465
491
  ```
466
492
 
467
- #### 1.5.10. Restic filestore restore
493
+ #### 1.6.10. Restic filestore restore
468
494
 
469
495
  Restore the latest matching filestore snapshot:
470
496
 
@@ -486,7 +512,7 @@ docker compose up -d odoo
486
512
 
487
513
  With `--force`, the restore helper removes the target database filestore before restoring it. When `latest` is used, the snapshot selection is restricted to the current `RESTIC_HOST` and source filestore path. Use the same `RESTIC_HOST` value that was used when the snapshot was created if the restore is performed from another host.
488
514
 
489
- ### 1.6. Creating a Docker deploy build context
515
+ ### 1.7. Creating a Docker deploy build context
490
516
 
491
517
  Use `--create-docker-deploy` to generate a self-contained Docker build context for CI/CD, testing, staging, production, or another non-local deployment workflow.
492
518
 
@@ -759,7 +785,7 @@ Restore a database dump with `pg_restore`:
759
785
  ./odoo-scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
760
786
  ```
761
787
 
762
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
788
+ Database restores use copy semantics by default.
763
789
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
764
790
  database can coexist with the source database:
765
791
 
@@ -1384,6 +1410,10 @@ This section is optional.
1384
1410
  - `mailpit_enabled` — enables a persistent Mailpit service in the local Docker workflow for SMTP and POP3 mail testing. Default: `false`. It does not affect the deploy build context.
1385
1411
  - `mailpit_version` — version of `ghcr.io/axllent/mailpit` in local Compose. Default: `v1.31.3`. An explicitly set value must be a concrete version; empty values and `latest` are invalid.
1386
1412
  - `mailpit_webui_port` — host port used for the Mailpit web UI. Default: `8025`. The internal Mailpit web UI port remains `8025`.
1413
+ - `odoo_cpus`, `db_cpus`, `mailpit_cpus` — optional CPU limits for the respective Compose services; positive decimal CPU counts such as `0.5` or `2.0`.
1414
+ - `odoo_mem_limit`, `db_mem_limit`, `mailpit_mem_limit` — optional memory limits for the respective Compose services; positive amounts such as `256m` or `4g`.
1415
+
1416
+ All six resource options default to unset and are omitted from generated Compose when empty. See [Limiting Docker Compose resources](#17-limiting-docker-compose-resources) for a complete example.
1387
1417
 
1388
1418
  Example:
1389
1419
 
@@ -0,0 +1 @@
1
+ __version__ = "1.24.4"
@@ -25,6 +25,7 @@ import sys
25
25
  import tempfile
26
26
  import zipfile
27
27
  from dataclasses import asdict, dataclass, field, replace
28
+ from decimal import Decimal
28
29
  from datetime import datetime, timezone
29
30
  from pathlib import Path, PurePosixPath
30
31
  from typing import Any, Dict, Optional
@@ -153,6 +154,12 @@ _INI_SET_SUPPORTED_OPTIONS = {
153
154
  "mailpit_enabled",
154
155
  "mailpit_version",
155
156
  "mailpit_webui_port",
157
+ "odoo_cpus",
158
+ "odoo_mem_limit",
159
+ "db_cpus",
160
+ "db_mem_limit",
161
+ "mailpit_cpus",
162
+ "mailpit_mem_limit",
156
163
  },
157
164
  }
158
165
  _INI_SET_ADDON_SUPPORTED_OPTIONS = {
@@ -239,6 +246,12 @@ class DockerConfig:
239
246
  mailpit_enabled: bool = False
240
247
  mailpit_version: str = _DEFAULT_DOCKER_MAILPIT_VERSION
241
248
  mailpit_webui_port: int = _DEFAULT_DOCKER_MAILPIT_WEBUI_PORT
249
+ odoo_cpus: Optional[str] = None
250
+ odoo_mem_limit: Optional[str] = None
251
+ db_cpus: Optional[str] = None
252
+ db_mem_limit: Optional[str] = None
253
+ mailpit_cpus: Optional[str] = None
254
+ mailpit_mem_limit: Optional[str] = None
242
255
  shares: Dict[str, DockerShareSpec] = field(default_factory=dict)
243
256
 
244
257
 
@@ -1049,10 +1062,27 @@ def _parse_odoo_version(odoo_version: str) -> int:
1049
1062
 
1050
1063
  def _validate_docker_image(value: str, option: str) -> str:
1051
1064
  image = (value or "").strip()
1052
- if not image:
1065
+ # This value is embedded directly into Dockerfile FROM instructions.
1066
+ # Require a single Docker image reference (registry, tag and digest allowed).
1067
+ component = r"[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*"
1068
+ label = r"[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?"
1069
+ registry = rf"(?:{label}(?:\.{label})*(?::[0-9]+)?|\[[0-9A-Fa-f:.]+\](?::[0-9]+)?)"
1070
+ tag = r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}"
1071
+ digest = r"[A-Za-z][A-Za-z0-9_+.-]*:[A-Za-z0-9=_-]{32,}"
1072
+ if (
1073
+ not image
1074
+ or len(image) > 512
1075
+ or "\n" in (value or "")
1076
+ or "\r" in (value or "")
1077
+ or re.fullmatch(
1078
+ rf"(?:{registry}/)?{component}(?:/{component})*(?::{tag})?(?:@{digest})?",
1079
+ image,
1080
+ flags=re.ASCII,
1081
+ ) is None
1082
+ ):
1053
1083
  raise Exception(
1054
1084
  f"Invalid option '{option}' in section [docker] "
1055
- "(expected non-empty Docker image name/tag)."
1085
+ "(expected a single Docker image reference, optionally with registry, tag or digest)."
1056
1086
  )
1057
1087
 
1058
1088
  return image
@@ -1113,6 +1143,37 @@ def _validate_tcp_port(value: str, option: str) -> int:
1113
1143
  return port
1114
1144
 
1115
1145
 
1146
+ def _validate_docker_cpus(value: str, option: str) -> Optional[str]:
1147
+ raw = (value or "").strip()
1148
+ if not raw:
1149
+ return None
1150
+ if (
1151
+ len(raw) > 32
1152
+ or re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", raw, flags=re.ASCII) is None
1153
+ or Decimal(raw) <= 0
1154
+ ):
1155
+ raise Exception(
1156
+ f"Invalid option '{option}' in section [docker] "
1157
+ "(expected a positive CPU count, for example 0.5 or 2.0)."
1158
+ )
1159
+ return str(Decimal(raw))
1160
+
1161
+
1162
+ def _validate_docker_mem_limit(value: str, option: str) -> Optional[str]:
1163
+ raw = (value or "").strip()
1164
+ if not raw:
1165
+ return None
1166
+ # Docker Compose memory values are positive byte counts with optional units.
1167
+ if len(raw) > 32 or re.fullmatch(
1168
+ r"[1-9][0-9]*(?:b|[kmgpt](?:b|ib)?)?", raw, flags=re.ASCII | re.IGNORECASE
1169
+ ) is None:
1170
+ raise Exception(
1171
+ f"Invalid option '{option}' in section [docker] "
1172
+ "(expected a positive memory amount, for example 256m or 4g)."
1173
+ )
1174
+ return raw.lower()
1175
+
1176
+
1116
1177
  def _get_default_virtualenv_settings(odoo_version: str) -> tuple[str, list[str], list[str]]:
1117
1178
  odoo_major_version = _parse_odoo_version(odoo_version)
1118
1179
 
@@ -1271,6 +1332,13 @@ def load_project_config(
1271
1332
  for sec in cp.sections():
1272
1333
  if sec.startswith("addons."):
1273
1334
  name = sec.split(".", 1)[1]
1335
+ # Git addon destinations are ROOT/odoo-addons/<name>. Keep the
1336
+ # section name a single, portable path component (also on Windows).
1337
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", name) or name.endswith("."):
1338
+ raise Exception(
1339
+ f"Invalid addon section name [{sec}]: use letters, digits, '.', '_' or '-' "
1340
+ "without path separators or traversal components."
1341
+ )
1274
1342
 
1275
1343
  has_path = cp.has_option(sec, "path")
1276
1344
  has_repo = cp.has_option(sec, "repo")
@@ -1368,13 +1436,21 @@ def load_project_config(
1368
1436
  "mailpit_enabled",
1369
1437
  "mailpit_version",
1370
1438
  "mailpit_webui_port",
1439
+ "odoo_cpus",
1440
+ "odoo_mem_limit",
1441
+ "db_cpus",
1442
+ "db_mem_limit",
1443
+ "mailpit_cpus",
1444
+ "mailpit_mem_limit",
1371
1445
  }
1372
1446
  for key in cp._sections.get("docker", {}).keys():
1373
1447
  if key not in supported_docker_options:
1374
1448
  raise Exception(
1375
1449
  f"Unsupported option '{key}' in section [docker]. "
1376
1450
  "Supported options: base_image, mailpit_enabled, mailpit_version, "
1377
- "mailpit_webui_port, odoo_source, postgres_version, restic_version."
1451
+ "mailpit_webui_port, odoo_source, postgres_version, restic_version, "
1452
+ "odoo_cpus, odoo_mem_limit, db_cpus, db_mem_limit, "
1453
+ "mailpit_cpus, mailpit_mem_limit."
1378
1454
  )
1379
1455
 
1380
1456
  base_image = (
@@ -1414,6 +1490,30 @@ def load_project_config(
1414
1490
  if cp.has_option("docker", "mailpit_webui_port")
1415
1491
  else _DEFAULT_DOCKER_MAILPIT_WEBUI_PORT
1416
1492
  ),
1493
+ odoo_cpus=(
1494
+ _validate_docker_cpus(cp.get("docker", "odoo_cpus"), "odoo_cpus")
1495
+ if cp.has_option("docker", "odoo_cpus") else None
1496
+ ),
1497
+ odoo_mem_limit=(
1498
+ _validate_docker_mem_limit(cp.get("docker", "odoo_mem_limit"), "odoo_mem_limit")
1499
+ if cp.has_option("docker", "odoo_mem_limit") else None
1500
+ ),
1501
+ db_cpus=(
1502
+ _validate_docker_cpus(cp.get("docker", "db_cpus"), "db_cpus")
1503
+ if cp.has_option("docker", "db_cpus") else None
1504
+ ),
1505
+ db_mem_limit=(
1506
+ _validate_docker_mem_limit(cp.get("docker", "db_mem_limit"), "db_mem_limit")
1507
+ if cp.has_option("docker", "db_mem_limit") else None
1508
+ ),
1509
+ mailpit_cpus=(
1510
+ _validate_docker_cpus(cp.get("docker", "mailpit_cpus"), "mailpit_cpus")
1511
+ if cp.has_option("docker", "mailpit_cpus") else None
1512
+ ),
1513
+ mailpit_mem_limit=(
1514
+ _validate_docker_mem_limit(cp.get("docker", "mailpit_mem_limit"), "mailpit_mem_limit")
1515
+ if cp.has_option("docker", "mailpit_mem_limit") else None
1516
+ ),
1417
1517
  shares=docker_shares,
1418
1518
  )
1419
1519
 
@@ -2151,7 +2251,22 @@ def _validate_local_odoo_path(layout: Layout, spec: OdooSpec) -> Path:
2151
2251
 
2152
2252
 
2153
2253
  def _resolve_addon_path(layout: Layout, addon_name: str, spec: AddonSpec) -> Path:
2154
- return _resolve_workspace_path(layout, spec.path or "", layout.addons_root / addon_name)
2254
+ if spec.is_local:
2255
+ return _resolve_workspace_path(layout, spec.path or "", layout.addons_root / addon_name)
2256
+ dest = layout.addons_root / addon_name
2257
+ # Implicit Git destinations must not follow symlinks into another repo,
2258
+ # outside the workspace, or back onto the addons root itself.
2259
+ if (
2260
+ not _is_path_within(layout.addons_root, layout.root)
2261
+ or dest.is_symlink()
2262
+ or not _is_path_within(dest, layout.addons_root)
2263
+ or dest.resolve() == layout.addons_root.resolve()
2264
+ ):
2265
+ raise Exception(
2266
+ f"Unsafe addon destination for [addons.{addon_name}]: {dest} "
2267
+ f"must resolve inside {layout.addons_root}. Use an explicit 'path' for local addons."
2268
+ )
2269
+ return dest
2155
2270
 
2156
2271
 
2157
2272
  def _validate_local_addon_path(layout: Layout, addon_name: str, spec: AddonSpec) -> Path:
@@ -2498,11 +2613,44 @@ def _native_default_db_name(cfg: ProjectConfig) -> str:
2498
2613
  db_name = str(cfg.config.get("db_name") or "").strip()
2499
2614
  if not db_name:
2500
2615
  raise Exception("Native database helpers require [config].db_name.")
2501
- if db_name in (".", "..") or any(c in db_name for c in ("/", "\\", "\x00", "\r", "\n")):
2502
- raise Exception("Database helpers require a single database name, not a path.")
2616
+ # These names become PostgreSQL identifiers, filesystem path components and
2617
+ # Windows batch variables. Reject traversal, CMD metacharacters and names
2618
+ # that PostgreSQL could silently truncate to another database name.
2619
+ if len(db_name) > 63 or not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]*", db_name):
2620
+ raise Exception(
2621
+ "Invalid [config].db_name: expected 1-63 ASCII letters, digits, "
2622
+ "underscores, dots or hyphens, starting with a letter, digit or underscore."
2623
+ )
2503
2624
  return db_name
2504
2625
 
2505
2626
 
2627
+ def _native_filestore_data_dir_sh(layout: Layout) -> str:
2628
+ # Preserve workspace-relative paths when the workspace is moved.
2629
+ try:
2630
+ relative = layout.data_dir.relative_to(layout.root)
2631
+ except ValueError:
2632
+ return shlex.quote(str(layout.data_dir))
2633
+ return f'"${{ROOT_DIR}}"/{shlex.quote(relative.as_posix())}'
2634
+
2635
+
2636
+ def _native_filestore_data_dir_bat(layout: Layout) -> str:
2637
+ # Paths cannot contain double quotes on Windows; reject control characters
2638
+ # rather than generating unsafe batch assignments.
2639
+ if any(char in str(layout.data_dir) for char in ('"', '\r', '\n', '\x00')):
2640
+ raise Exception("Native filestore data_dir contains invalid Windows path characters.")
2641
+ try:
2642
+ relative = layout.data_dir.relative_to(layout.root)
2643
+ except ValueError:
2644
+ value = str(layout.data_dir)
2645
+ else:
2646
+ suffix = '' if str(relative) == '.' else '\\' + str(relative)
2647
+ value = '%ROOT_DIR%' + suffix
2648
+ # Keep %ROOT_DIR% expandable while escaping literal percent signs in paths.
2649
+ if value.startswith('%ROOT_DIR%'):
2650
+ return '%ROOT_DIR%' + value[len('%ROOT_DIR%'):].replace('%', '%%').replace('^', '^^')
2651
+ return value.replace('%', '%%').replace('^', '^^')
2652
+
2653
+
2506
2654
  def _native_pg_env(cfg: ProjectConfig) -> dict[str, str]:
2507
2655
  # libpq reads these environment variables, keeping passwords out of command
2508
2656
  # arguments. Missing/False options leave libpq defaults and .pgpass intact.
@@ -2857,6 +3005,7 @@ echo "INFO: Database neutralization completed: ${{DB_NAME}}"
2857
3005
 
2858
3006
  def write_native_backup_filestore_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2859
3007
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
3008
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2860
3009
  content = fr"""#!/usr/bin/env bash
2861
3010
  set -euo pipefail
2862
3011
 
@@ -2865,7 +3014,8 @@ ROOT_DIR="$(cd "${{SCRIPT_DIR}}/.." && pwd)"
2865
3014
 
2866
3015
  BACKUPS_DIR="${{ROOT_DIR}}/odoo-backups"
2867
3016
  DB_NAME={configured_db_name}
2868
- FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
3017
+ DATA_DIR={configured_data_dir}
3018
+ FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
2869
3019
  TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
2870
3020
  OUTPUT="${{1:-${{BACKUPS_DIR}}/${{DB_NAME}}_${{TIMESTAMP}}_filestore.tar.gz}}"
2871
3021
  TMP_OUTPUT="${{OUTPUT}}.tmp"
@@ -2887,6 +3037,7 @@ echo "INFO: Filestore backup created: ${{OUTPUT}}"
2887
3037
 
2888
3038
  def write_native_restore_filestore_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2889
3039
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
3040
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2890
3041
  content = fr"""#!/usr/bin/env bash
2891
3042
  set -euo pipefail
2892
3043
 
@@ -2894,7 +3045,8 @@ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
2894
3045
  ROOT_DIR="$(cd "${{SCRIPT_DIR}}/.." && pwd)"
2895
3046
 
2896
3047
  DB_NAME={configured_db_name}
2897
- FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
3048
+ DATA_DIR={configured_data_dir}
3049
+ FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
2898
3050
  FORCE=false
2899
3051
  SOURCE=""
2900
3052
 
@@ -2959,6 +3111,7 @@ echo "INFO: Filestore restore completed: ${{DB_NAME}}"
2959
3111
 
2960
3112
  def write_native_backup_filestore_restic_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2961
3113
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
3114
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2962
3115
  content = fr'''#!/usr/bin/env bash
2963
3116
  set -euo pipefail
2964
3117
 
@@ -2967,7 +3120,8 @@ ROOT_DIR="$(cd "${{SCRIPT_DIR}}/.." && pwd)"
2967
3120
  RESTIC="${{SCRIPT_DIR}}/restic.sh"
2968
3121
  DB_NAME={configured_db_name}
2969
3122
  RESTIC_HOST_NAME="${{RESTIC_HOST:-$(hostname -f 2>/dev/null || hostname)}}"
2970
- FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
3123
+ DATA_DIR={configured_data_dir}
3124
+ FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
2971
3125
 
2972
3126
  if [[ ! -x "${{RESTIC}}" ]]; then
2973
3127
  echo "ERROR: restic helper not found: ${{RESTIC}}" >&2
@@ -2994,6 +3148,7 @@ echo "INFO: Backing up filestore for database '${{DB_NAME}}' with restic."
2994
3148
 
2995
3149
  def write_native_restore_filestore_restic_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2996
3150
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
3151
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2997
3152
  restic_command = _native_restic_command_sh()
2998
3153
  content = fr'''#!/usr/bin/env bash
2999
3154
  set -euo pipefail
@@ -3008,8 +3163,9 @@ RESTIC_HOST_NAME="${{RESTIC_HOST:-$(hostname -f 2>/dev/null || hostname)}}"
3008
3163
  FORCE=false
3009
3164
  SNAPSHOT="latest"
3010
3165
  SNAPSHOT_SET=false
3011
- SOURCE_FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{SOURCE_DB_NAME}}"
3012
- TARGET_FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
3166
+ DATA_DIR={configured_data_dir}
3167
+ SOURCE_FILESTORE_PATH="${{DATA_DIR}}/filestore/${{SOURCE_DB_NAME}}"
3168
+ TARGET_FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
3013
3169
 
3014
3170
  usage() {{
3015
3171
  echo "Usage: $(basename "$0") [--force] [SNAPSHOT]" >&2
@@ -3371,6 +3527,7 @@ exit /b 0
3371
3527
 
3372
3528
  def write_native_backup_filestore_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3373
3529
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3530
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3374
3531
  content = rf"""@echo off
3375
3532
  setlocal DisableDelayedExpansion
3376
3533
 
@@ -3379,7 +3536,8 @@ if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
3379
3536
  for %%I in ("%SCRIPT_DIR%\..") do set "ROOT_DIR=%%~fI"
3380
3537
  set "BACKUPS_DIR=%ROOT_DIR%\odoo-backups"
3381
3538
  set "DB_NAME={configured_db_name}"
3382
- set "FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3539
+ set "DATA_DIR={configured_data_dir}"
3540
+ set "FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3383
3541
 
3384
3542
  for /f %%I in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set "TIMESTAMP=%%I"
3385
3543
  if "%~1"=="" (
@@ -3412,6 +3570,7 @@ endlocal
3412
3570
 
3413
3571
  def write_native_restore_filestore_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3414
3572
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3573
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3415
3574
  content = rf"""@echo off
3416
3575
  setlocal DisableDelayedExpansion
3417
3576
 
@@ -3419,7 +3578,8 @@ set "SCRIPT_DIR=%~dp0"
3419
3578
  if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
3420
3579
  for %%I in ("%SCRIPT_DIR%\..") do set "ROOT_DIR=%%~fI"
3421
3580
  set "DB_NAME={configured_db_name}"
3422
- set "FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3581
+ set "DATA_DIR={configured_data_dir}"
3582
+ set "FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3423
3583
  set "FORCE=false"
3424
3584
  set "SOURCE="
3425
3585
 
@@ -3484,6 +3644,7 @@ exit /b 2
3484
3644
 
3485
3645
  def write_native_backup_filestore_restic_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3486
3646
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3647
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3487
3648
  content = rf"""@echo off
3488
3649
  setlocal DisableDelayedExpansion
3489
3650
 
@@ -3497,7 +3658,8 @@ if defined RESTIC_HOST (
3497
3658
  ) else (
3498
3659
  for /f "delims=" %%H in ('hostname') do set "RESTIC_HOST_NAME=%%H"
3499
3660
  )
3500
- set "FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3661
+ set "DATA_DIR={configured_data_dir}"
3662
+ set "FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3501
3663
 
3502
3664
  if not exist "%RESTIC%" (
3503
3665
  echo ERROR: restic helper not found: %RESTIC%
@@ -3519,6 +3681,7 @@ exit /b %ERRORLEVEL%
3519
3681
 
3520
3682
  def write_native_restore_filestore_restic_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3521
3683
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3684
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3522
3685
  restic_command = _native_restic_command_bat()
3523
3686
  content = rf"""@echo off
3524
3687
  setlocal DisableDelayedExpansion
@@ -3541,8 +3704,9 @@ if defined RESTIC_HOST (
3541
3704
  set "FORCE=false"
3542
3705
  set "SNAPSHOT=latest"
3543
3706
  set "SNAPSHOT_SET=false"
3544
- set "SOURCE_FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%SOURCE_DB_NAME%"
3545
- set "TARGET_FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3707
+ set "DATA_DIR={configured_data_dir}"
3708
+ set "SOURCE_FILESTORE_PATH=%DATA_DIR%\filestore\%SOURCE_DB_NAME%"
3709
+ set "TARGET_FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3546
3710
 
3547
3711
  goto parse_args
3548
3712
 
@@ -4224,18 +4388,23 @@ def render_docker_compose(layout: Layout, cfg: ProjectConfig) -> str:
4224
4388
  )
4225
4389
 
4226
4390
  mailpit_service = ""
4227
- depends_on = [" - db"]
4391
+ depends_on = [" db:", " condition: service_healthy"]
4228
4392
  named_volumes = [
4229
4393
  " odoo-db-data:",
4230
4394
  " odoo-data:",
4231
4395
  ]
4232
4396
 
4233
4397
  if cfg.docker.mailpit_enabled:
4398
+ mailpit_resources = ""
4399
+ if cfg.docker.mailpit_cpus is not None:
4400
+ mailpit_resources += f" cpus: {cfg.docker.mailpit_cpus}\n"
4401
+ if cfg.docker.mailpit_mem_limit is not None:
4402
+ mailpit_resources += f" mem_limit: {cfg.docker.mailpit_mem_limit}\n"
4234
4403
  mailpit_service = f"""
4235
4404
  mailpit:
4236
4405
  image: ghcr.io/axllent/mailpit:{cfg.docker.mailpit_version}
4237
4406
  restart: unless-stopped
4238
- environment:
4407
+ {mailpit_resources} environment:
4239
4408
  MP_DATABASE: /data/mailpit.db
4240
4409
  MP_DISABLE_VERSION_CHECK: "true"
4241
4410
  MP_POP3_BIND_ADDR: "0.0.0.0:{_DEFAULT_DOCKER_MAILPIT_POP3_PORT}"
@@ -4245,18 +4414,26 @@ def render_docker_compose(layout: Layout, cfg: ProjectConfig) -> str:
4245
4414
  volumes:
4246
4415
  - mailpit-data:/data
4247
4416
  """
4248
- depends_on.append(" - mailpit")
4249
4417
  named_volumes.append(" mailpit-data:")
4250
4418
 
4251
4419
  depends_on_block = "\n".join(depends_on)
4252
4420
  named_volumes_block = "\n".join(named_volumes)
4253
-
4254
- return f"""# Generated by odoo-compose for local development.
4255
- services:
4421
+ db_resources = ""
4422
+ if cfg.docker.db_cpus is not None:
4423
+ db_resources += f" cpus: {cfg.docker.db_cpus}\n"
4424
+ if cfg.docker.db_mem_limit is not None:
4425
+ db_resources += f" mem_limit: {cfg.docker.db_mem_limit}\n"
4426
+ odoo_resources = ""
4427
+ if cfg.docker.odoo_cpus is not None:
4428
+ odoo_resources += f" cpus: {cfg.docker.odoo_cpus}\n"
4429
+ if cfg.docker.odoo_mem_limit is not None:
4430
+ odoo_resources += f" mem_limit: {cfg.docker.odoo_mem_limit}\n"
4431
+
4432
+ return f"""services:
4256
4433
  db:
4257
4434
  image: postgres:{cfg.docker.postgres_version}
4258
4435
  restart: unless-stopped
4259
- logging:
4436
+ {db_resources} logging:
4260
4437
  driver: local
4261
4438
  options:
4262
4439
  max-size: "50m"
@@ -4265,6 +4442,12 @@ services:
4265
4442
  POSTGRES_DB: postgres
4266
4443
  POSTGRES_USER: odoo
4267
4444
  POSTGRES_PASSWORD: odoo
4445
+ healthcheck:
4446
+ test: ["CMD-SHELL", "pg_isready -q -h 127.0.0.1 -U $${{POSTGRES_USER}} -d $${{POSTGRES_DB}}"]
4447
+ interval: 10s
4448
+ timeout: 5s
4449
+ retries: 10
4450
+ start_period: 30s
4268
4451
  volumes:
4269
4452
  - odoo-db-data:{postgres_data_path}
4270
4453
  {mailpit_service}
@@ -4272,13 +4455,19 @@ services:
4272
4455
  build:
4273
4456
  context: {build_context}
4274
4457
  restart: unless-stopped
4275
- logging:
4458
+ {odoo_resources} logging:
4276
4459
  driver: local
4277
4460
  options:
4278
4461
  max-size: "50m"
4279
4462
  max-file: "10"
4280
4463
  depends_on:
4281
4464
  {depends_on_block}
4465
+ healthcheck:
4466
+ test: ["CMD", "curl", "--fail", "--silent", "--show-error", "--max-time", "5", "--output", "/dev/null", "http://127.0.0.1:8069/web/login"]
4467
+ interval: 30s
4468
+ timeout: 10s
4469
+ retries: 5
4470
+ start_period: 90s
4282
4471
  ports:
4283
4472
  {ports}
4284
4473
  environment:
@@ -4294,23 +4483,66 @@ volumes:
4294
4483
  """
4295
4484
 
4296
4485
 
4297
- def write_docker_compose(layout: Layout, cfg: ProjectConfig) -> Path:
4298
- """Write ROOT/compose.yaml for local development, overwriting the generated file."""
4486
+ def write_docker_compose(
4487
+ layout: Layout,
4488
+ cfg: ProjectConfig,
4489
+ source_ini: Path,
4490
+ effective_ini_sha256: str,
4491
+ ) -> Path:
4492
+ """Write ROOT/compose.yaml, preserving its timestamp if the content is unchanged."""
4299
4493
  path = _docker_compose_path(layout)
4300
4494
 
4301
4495
  if path.exists() and not path.is_file():
4302
4496
  raise Exception(f"Docker Compose path exists but is not a file: {path}")
4303
4497
 
4304
- path.write_text(render_docker_compose(layout, cfg), encoding="utf-8")
4305
- _logger.info("Generated local Docker Compose file: %s", path)
4498
+ source_sha256 = hashlib.sha256(source_ini.read_bytes()).hexdigest()
4499
+ header = (
4500
+ "# =============================================================================\n"
4501
+ "# AUTO-GENERATED FILE - DO NOT EDIT MANUALLY\n"
4502
+ "# Generator: odoo-devops-tools (odoo-compose)\n"
4503
+ f"# Version: {__version__}\n"
4504
+ f"# Source: {source_ini.name}\n"
4505
+ f"# Source SHA256: {source_sha256}\n"
4506
+ f"# Effective SHA256: {effective_ini_sha256}\n"
4507
+ "# Effective SHA256 includes INI interpolation and CLI overrides.\n"
4508
+ "# Generated: __GENERATED_AT__\n"
4509
+ "# Update the source INI and regenerate this file.\n"
4510
+ "# =============================================================================\n\n"
4511
+ )
4512
+ compose_content = render_docker_compose(layout, cfg)
4513
+
4514
+ if path.is_file():
4515
+ existing = path.read_text(encoding="utf-8", errors="replace")
4516
+ previous_generated_at = re.search(
4517
+ r"(?m)^# Generated: (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)$",
4518
+ existing,
4519
+ )
4520
+ if previous_generated_at:
4521
+ unchanged = header.replace(
4522
+ "__GENERATED_AT__", previous_generated_at.group(1)
4523
+ ) + compose_content
4524
+ if existing == unchanged:
4525
+ _logger.info("Docker Compose file is unchanged: %s", path)
4526
+ return path
4527
+
4528
+ header = header.replace("__GENERATED_AT__", _utc_now_iso())
4529
+ path.write_text(header + compose_content, encoding="utf-8")
4530
+ _logger.info("Generated Docker Compose file: %s", path)
4306
4531
  return path
4307
4532
 
4308
4533
 
4309
4534
  def _docker_default_db_name(cfg: ProjectConfig) -> str:
4310
4535
  db_name = cfg.config.get("db_name")
4311
- if isinstance(db_name, str) and db_name.strip():
4312
- return db_name.strip()
4313
- return "odoo"
4536
+ if not isinstance(db_name, str) or not db_name.strip():
4537
+ return "odoo"
4538
+ db_name = db_name.strip()
4539
+ # The same name is used by database and destructive filestore helpers.
4540
+ if len(db_name) > 63 or not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]*", db_name):
4541
+ raise Exception(
4542
+ "Invalid [config].db_name: expected 1-63 ASCII letters, digits, "
4543
+ "underscores, dots or hyphens, starting with a letter, digit or underscore."
4544
+ )
4545
+ return db_name
4314
4546
 
4315
4547
 
4316
4548
  def _write_docker_local_script(
@@ -5714,6 +5946,8 @@ def create_local_docker_artifacts(
5714
5946
  layout: Layout,
5715
5947
  cfg: ProjectConfig,
5716
5948
  addon_requirement_files: list[Path],
5949
+ source_ini: Path,
5950
+ effective_ini_sha256: str,
5717
5951
  ) -> dict[str, Any]:
5718
5952
  """Generate the default local-development Docker context and ROOT/compose.yaml."""
5719
5953
  if layout.docker_local_dir.exists():
@@ -5741,7 +5975,7 @@ def create_local_docker_artifacts(
5741
5975
  include_addons=False,
5742
5976
  include_config=False,
5743
5977
  )
5744
- compose_path = write_docker_compose(layout, cfg)
5978
+ compose_path = write_docker_compose(layout, cfg, source_ini, effective_ini_sha256)
5745
5979
  script_paths = write_docker_local_scripts(layout, cfg)
5746
5980
 
5747
5981
  _logger.info("Generated local Docker build context: %s", layout.docker_local_dir)
@@ -6855,10 +7089,18 @@ def _sync_project_impl(
6855
7089
 
6856
7090
  if target == "docker":
6857
7091
  _logger.info("Generating local-development Docker artifacts.")
7092
+ effective_ini = _ini_for_effective_file(_read_ini(
7093
+ ini_path,
7094
+ vars_overrides=vars_overrides,
7095
+ ini_overrides=ini_overrides,
7096
+ log_overrides=False,
7097
+ ))
6858
7098
  local_docker_artifacts = create_local_docker_artifacts(
6859
7099
  layout=layout,
6860
7100
  cfg=cfg,
6861
7101
  addon_requirement_files=addon_req_files,
7102
+ source_ini=ini_path,
7103
+ effective_ini_sha256=hashlib.sha256(effective_ini.encode("utf-8")).hexdigest(),
6862
7104
  )
6863
7105
  else:
6864
7106
  _logger.info("Skipping Docker artifacts (target=venv).")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-devops-tools
3
- Version: 1.24.2
3
+ Version: 1.24.4
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -134,7 +134,7 @@ Then start the Docker Compose environment:
134
134
 
135
135
  ```bash
136
136
  cd ./odoo19
137
- docker compose up -d --build
137
+ docker compose up -d --build --wait
138
138
  ```
139
139
 
140
140
  Optionally initialize the Odoo database from the command line instead of through the web interface:
@@ -174,7 +174,7 @@ Sync the configured addon repositories and refresh the generated Docker workflow
174
174
 
175
175
  ```bash
176
176
  odoo-compose --sync-addons
177
- docker compose up -d --build
177
+ docker compose up -d --build --wait
178
178
  ```
179
179
 
180
180
  The addon repositories are cloned into `ROOT/odoo-addons/oca-web/` and `ROOT/odoo-addons/oca-helpdesk/` and bind-mounted into the Odoo container.
@@ -215,7 +215,7 @@ Sync the Odoo source and generate the Docker artifacts:
215
215
 
216
216
  ```bash
217
217
  odoo-compose --sync-all
218
- docker compose up -d --build
218
+ docker compose up -d --build --wait
219
219
  ```
220
220
 
221
221
  The resolved Odoo source is bind-mounted read-only at `/opt/odoo`. The Odoo Python source from the base image is removed while its installed dependencies remain available.
@@ -226,14 +226,14 @@ Enable Mailpit in the local Docker workflow when you need to inspect or test ema
226
226
 
227
227
  ```bash
228
228
  odoo-compose --set docker:mailpit_enabled=true
229
- docker compose up -d --build
229
+ docker compose up -d --build --wait
230
230
  ```
231
231
 
232
232
  Mailpit is available at http://localhost:8025 by default. When running multiple workspaces, override the host web UI port, for example:
233
233
 
234
234
  ```bash
235
235
  odoo-compose --set docker:mailpit_enabled=true --set docker:mailpit_webui_port=8026
236
- docker compose up -d --build
236
+ docker compose up -d --build --wait
237
237
  ```
238
238
 
239
239
  The generated local Odoo configuration sends outgoing email to `mailpit:1025`. When Mailpit is enabled, local Docker SMTP connection settings (`smtp_server`, `smtp_port`, `smtp_ssl`, `smtp_user`, and `smtp_password`) are intentionally overridden; the deploy configuration keeps the values from `[config]` unchanged.
@@ -256,7 +256,33 @@ target = /mnt/exchange
256
256
 
257
257
  `odoo-compose` does not create the source directory or change its ownership or permissions; host provisioning or the deployment process must prepare the directory before Docker starts.
258
258
 
259
- ### 1.5. Script reference
259
+ ### 1.5. Limiting Docker Compose resources
260
+
261
+ You can optionally set CPU and memory limits for each service in the generated `compose.yaml` using `[docker]` in `odoo-project.ini`:
262
+
263
+ ```ini
264
+ [docker]
265
+ odoo_cpus = 2.0
266
+ odoo_mem_limit = 4g
267
+
268
+ db_cpus = 1.0
269
+ db_mem_limit = 2g
270
+
271
+ mailpit_enabled = true
272
+ mailpit_cpus = 0.5
273
+ mailpit_mem_limit = 256m
274
+ ```
275
+
276
+ Each option is independent and optional. If an option is missing or empty, `odoo-compose` omits that limit from the Compose service; no CPU or memory limit is imposed by the generator. CPU values must be positive decimal numbers (such as `0.5` or `2.0`). Memory limits must be positive amounts (such as `256m`, `2g`, or `4g`); zero and negative values are rejected. A memory limit is a hard container limit: exceeding it may cause an out-of-memory termination, so size it for your workload.
277
+
278
+ Regenerate the Docker configuration after editing the INI, then start the services:
279
+
280
+ ```bash
281
+ odoo-compose
282
+ docker compose up -d --build --wait
283
+ ```
284
+
285
+ ### 1.6. Script reference
260
286
 
261
287
  The Docker workflow generates platform-specific helper scripts under `ROOT/docker/local/scripts/`. Unix-like systems generate `.sh` helpers; Windows generates the corresponding `.bat` helpers.
262
288
 
@@ -283,7 +309,7 @@ ODOO_DOCKER_SUDO=1 ./docker/local/scripts/run.sh --help
283
309
 
284
310
  Docker helpers use `[config].db_name` when configured, or `odoo` by default.
285
311
 
286
- #### 1.5.1. run
312
+ #### 1.6.1. run
287
313
 
288
314
  Runs Odoo in a one-off Docker container for the configured database:
289
315
 
@@ -297,7 +323,7 @@ Any extra arguments are passed through to Odoo:
297
323
  ./docker/local/scripts/run.sh -i base --stop-after-init
298
324
  ```
299
325
 
300
- #### 1.5.2. shell
326
+ #### 1.6.2. shell
301
327
 
302
328
  Opens an Odoo shell for the configured database:
303
329
 
@@ -307,7 +333,7 @@ Opens an Odoo shell for the configured database:
307
333
 
308
334
  Any extra arguments are passed through to the underlying Odoo shell command.
309
335
 
310
- #### 1.5.3. update
336
+ #### 1.6.3. update
311
337
 
312
338
  Updates installed addons using `click-odoo-update`:
313
339
 
@@ -321,7 +347,7 @@ Any extra arguments are passed through to `click-odoo-update`:
321
347
  ./docker/local/scripts/update.sh --update-all
322
348
  ```
323
349
 
324
- #### 1.5.4. Database backup
350
+ #### 1.6.4. Database backup
325
351
 
326
352
  Create a PostgreSQL custom-format dump under `ROOT/odoo-backups/`:
327
353
 
@@ -341,7 +367,7 @@ Pass an output path explicitly when needed:
341
367
  ./docker/local/scripts/backup-db.sh ./odoo-backups/pre-upgrade.dump
342
368
  ```
343
369
 
344
- #### 1.5.5. Database restore
370
+ #### 1.6.5. Database restore
345
371
 
346
372
  Restore a database dump with `pg_restore`:
347
373
 
@@ -349,7 +375,7 @@ Restore a database dump with `pg_restore`:
349
375
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
350
376
  ```
351
377
 
352
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
378
+ Database restores use copy semantics by default.
353
379
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
354
380
  database can coexist with the source database:
355
381
 
@@ -372,7 +398,7 @@ docker compose stop odoo
372
398
  docker compose up -d odoo
373
399
  ```
374
400
 
375
- #### 1.5.6. Database neutralization
401
+ #### 1.6.6. Database neutralization
376
402
 
377
403
  For development or test databases restored from production, run the generated neutralization helper explicitly:
378
404
 
@@ -407,7 +433,7 @@ docker compose stop odoo
407
433
  docker compose up -d odoo
408
434
  ```
409
435
 
410
- #### 1.5.7. Filestore backup
436
+ #### 1.6.7. Filestore backup
411
437
 
412
438
  Create a compressed tar archive of the selected database filestore:
413
439
 
@@ -438,7 +464,7 @@ docker compose stop odoo
438
464
  docker compose up -d odoo
439
465
  ```
440
466
 
441
- #### 1.5.8. Filestore restore
467
+ #### 1.6.8. Filestore restore
442
468
 
443
469
  Restore a filestore archive:
444
470
 
@@ -454,7 +480,7 @@ docker compose stop odoo
454
480
  docker compose up -d odoo
455
481
  ```
456
482
 
457
- #### 1.5.9. Restic filestore backup
483
+ #### 1.6.9. Restic filestore backup
458
484
 
459
485
  The generated Docker image also includes `restic`. Restic is an additional option intended especially for large filestores where incremental snapshots and deduplication are useful.
460
486
 
@@ -492,7 +518,7 @@ List or inspect snapshots through the generic wrapper:
492
518
  ./docker/local/scripts/restic.sh check
493
519
  ```
494
520
 
495
- #### 1.5.10. Restic filestore restore
521
+ #### 1.6.10. Restic filestore restore
496
522
 
497
523
  Restore the latest matching filestore snapshot:
498
524
 
@@ -514,7 +540,7 @@ docker compose up -d odoo
514
540
 
515
541
  With `--force`, the restore helper removes the target database filestore before restoring it. When `latest` is used, the snapshot selection is restricted to the current `RESTIC_HOST` and source filestore path. Use the same `RESTIC_HOST` value that was used when the snapshot was created if the restore is performed from another host.
516
542
 
517
- ### 1.6. Creating a Docker deploy build context
543
+ ### 1.7. Creating a Docker deploy build context
518
544
 
519
545
  Use `--create-docker-deploy` to generate a self-contained Docker build context for CI/CD, testing, staging, production, or another non-local deployment workflow.
520
546
 
@@ -787,7 +813,7 @@ Restore a database dump with `pg_restore`:
787
813
  ./odoo-scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
788
814
  ```
789
815
 
790
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
816
+ Database restores use copy semantics by default.
791
817
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
792
818
  database can coexist with the source database:
793
819
 
@@ -1412,6 +1438,10 @@ This section is optional.
1412
1438
  - `mailpit_enabled` — enables a persistent Mailpit service in the local Docker workflow for SMTP and POP3 mail testing. Default: `false`. It does not affect the deploy build context.
1413
1439
  - `mailpit_version` — version of `ghcr.io/axllent/mailpit` in local Compose. Default: `v1.31.3`. An explicitly set value must be a concrete version; empty values and `latest` are invalid.
1414
1440
  - `mailpit_webui_port` — host port used for the Mailpit web UI. Default: `8025`. The internal Mailpit web UI port remains `8025`.
1441
+ - `odoo_cpus`, `db_cpus`, `mailpit_cpus` — optional CPU limits for the respective Compose services; positive decimal CPU counts such as `0.5` or `2.0`.
1442
+ - `odoo_mem_limit`, `db_mem_limit`, `mailpit_mem_limit` — optional memory limits for the respective Compose services; positive amounts such as `256m` or `4g`.
1443
+
1444
+ All six resource options default to unset and are omitted from generated Compose when empty. See [Limiting Docker Compose resources](#17-limiting-docker-compose-resources) for a complete example.
1415
1445
 
1416
1446
  Example:
1417
1447
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odoo-devops-tools"
7
- version = "1.24.2"
7
+ version = "1.24.4"
8
8
  description = "Build reproducible Odoo workspaces from a single project definition."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "1.24.2"