Dev Service Template - lukislp/mowdeck GitHub Wiki
Every MowDeck service is generated from the Copier template in templates/service. The template gives a new service the layers, the operational endpoints, tests, a Dockerfile, a Compose block and a README in the agreed structure, so a new service starts compliant with the operating rules (Twelve-Factor, non-root, read-only file system, health endpoints, graceful shutdown). The Device-Gateway was the first service generated from it.
uv run copier copy --trust templates/service services/<service-name>| Question | Default | Meaning |
|---|---|---|
service_name |
none | kebab-case, e.g. reports; distribution name, Compose service and HTTP path /api/v1/<service_name>
|
package_name |
service_name in snake_case |
Python package under src/
|
description |
MowDeck <name> service. |
one sentence |
needs_db |
false |
own PostgreSQL database with SQLAlchemy and Alembic |
http_port |
8000 |
port inside the container (1024 or higher) |
permissions |
<package>.view: View <name> data |
YAML mapping of permission key to description; the first one protects the example endpoint |
--trust is needed because the template computes derived values; --defaults with -d key=value answers everything on the command line. Then:
- Add the service to the root
pyproject.toml: the distribution name to thedevdependency group and<name> = { workspace = true }to[tool.uv.sources], souv syncinstalls it editable for tests and mypy. -
uv sync,uv run poe lint,uv run pytest services/<name>. - Paste
compose.snippet.ymlintoinfra/compose/docker-compose.yml(for a database service also add the database and role toinfra/compose/init/01-databases.sql).
services/<name>/
pyproject.toml workspace member, AGPL-3.0-only, depends on mowdeck-service-kit and mowdeck-contracts
README.md purpose, interfaces, events, settings, local start, tests
Dockerfile multi-stage uv build, non-root, read-only friendly, HEALTHCHECK
Dockerfile.dockerignore allow list for the repository root build context
compose.snippet.yml Compose block with Traefik labels for /api/v1/<name>
alembic.ini (needs_db) for creating revisions during development
src/<package>/
main.py build_app(settings, bus=..., principal_resolver=...) and main()
settings.py Settings(ServiceSettings) with the service defaults
module.py ModuleDescriptor with the declared permissions
domain/ models and rules, no I/O
application/ use cases and ports (Protocols)
adapters/api/ FastAPI routers; GET /api/v1/<name>/ping requires the first permission
adapters/bus/ subscriptions and publishers
adapters/db/ (needs_db) tables, migrate entry point, Alembic migrations
tests/
test_<package>_api.py health, module descriptor, 401, 403, 200
test_<package>_app.py startup and shutdown with the in-memory bus
| Layer | May import | Must not |
|---|---|---|
domain |
standard library, mowdeck_contracts
|
do I/O, import FastAPI, NATS, SQLAlchemy or adapters |
application |
domain, its own ports, libraries without I/O (mowdeck_control) |
import adapters or main; it talks to the outside only through ports |
adapters |
application, domain, frameworks |
contain business rules; they translate and implement ports |
main |
everything | contain logic beyond wiring |
Ports are typing.Protocol classes in application/ports.py. Tests replace adapters with fakes through build_app arguments, never by patching modules.
-
Configuration only from
MOWDECK_*variables orMOWDECK_<NAME>_FILEsecret files (Settings.from_env()); every field has a docstring, which is the settings reference. -
Operations:
/healthz,/readyz,/metricsand/.well-known/mowdeck-modulefrom the kit; JSON logs on stdout; graceful shutdown hooks withinMOWDECK_SHUTDOWN_GRACE_SECONDS. -
Image: built from the repository root with
uv sync --frozen --no-dev --no-editable --package <name>, so only the service and the workspace libraries it depends on are installed. Runs as userapp(uid 10001), setsPYTHONDONTWRITEBYTECODE=1and writes nothing at runtime: Compose runs it withread_only: trueand a tmpfs at/tmp, Helm withreadOnlyRootFilesystem. No architecture specific steps, so the same Dockerfile builds amd64 and arm64. The ignore file isDockerfile.dockerignorebecause the build context is the repository root (BuildKit reads the ignore file next to the Dockerfile). -
Migrations (needs_db) run as a separate step before the service:
python -m <package>.adapters.db.migrate(Compose: one-shot<name>-migrateservice, Helm: a Job). The Alembic configuration is built in code, so the image needs noalembic.ini. Migrations must stay backward compatible for rolling updates. -
Tests have unique file names (
test_<package>_*.py) and noconftest.py(mypy treats equally named modules of different packages as duplicates); shared helpers go into a uniquely named module next to the tests. -
Permissions: every endpoint uses
require_permission(...); endpoints touching account data filter by the principal's account and need a cross-tenant test (see the kit README).
- README in the template structure (purpose, interfaces, events, settings, local start, tests) and kept current.
- Tests: the generated ones plus tests for every use case; cross-tenant tests for every endpoint and handler touching account data; coverage of at least 85 %.
-
uv run poe lintanduv run poe testgreen; the service is listed in the rootpyproject.toml. - Module descriptor declares every permission and every produced event.
-
compose.snippet.ymladded toinfra/compose/docker-compose.yml,uv run poe compose-checkgreen, anddocker compose up -d --waitreports the service healthy. - A template in the umbrella chart
infra/helm/mowdeck(templates/<name>.yamlcallingmowdeck.workloadand, with a database,mowdeck.migrationJob; settings invalues.yamlunderconfig, secrets as_FILEfiles),sh infra/helm/check.shgreen (Helm operations).tools/repo-checks/tests/test_helm.pyfails until the Compose service has its chart template. - Docs: a line in building blocks, runtime scenarios in runtime view where the service takes part, contracts documented in
mowdeck-contracts. - An ADR for every architecture decision the service makes.
Change the template and the services generated from it together. tools/repo-checks/tests/test_template.py renders both variants (with and without database) on every test run; for larger changes also render into services/, run lint and the generated tests, and delete the render again. Services keep their answers in .copier-answers.yml.
Mirrored from docs/dev/service-template.md at commit 6a87264c6123f07c8474e153053525dc1816feb7. Edit the file in the repository, not this wiki page.