A dev service provisions a Testcontainers container — PostgreSQL, Keycloak — and wires its coordinates into your application as plain configuration, with no docker run and no hand-copied JDBC URL. Providers run outside the application, in the host that starts them: vidocq:dev, vidocq:run (opt-in), or a JUnit test run. Whichever host it is, the application sees the same thing — a devservices section of the startup report, and, in a dev launch, a dev console panel of the same name — without depending on Testcontainers or Docker itself.
This page covers where dev services run, how the application sees them, and how a test gets one. DEV_SERVICES.md at the repository root covers the providers themselves — PostgreSQL, Keycloak, multi-datasource, stable ports — in full.
|
Where they run NEW
Three hosts can start dev services, all through the same vidocq-runtime-devservices-host machinery:
| Host | Starts | Default |
|---|---|---|
|
Every provider whose |
On |
|
The same providers, in the same forked-JVM launch the production launcher uses. |
Off — opt in with |
A JUnit test run |
The same providers, for the whole test run, before any test class. |
Off — opt in by adding the dependencies (see In tests NEW) |
Testcontainers and the Docker client never reach the application’s own module path: they live in
vidocq-runtime-devservices-host and the providers, consumed only by the host. The application
depends on nothing but vidocq-runtime-devservices-extension, which reads a small state file and
has no Testcontainers, no Maven API and no JSON library of its own.
Every dev services jar is a named module NEW, as every published Vidocq jar is: none of them
reaches a module path as an automatic module. The providers name Testcontainers by the name its jar’s
file gives it (testcontainers), which is fine where they run, the plugin’s class path, and they need
Testcontainers' core only: the PostgreSQL provider configures a plain container rather than
Testcontainers' postgresql module. vidocq-runtime-devservices-junit still lands on a test run’s
class path, where its META-INF/services entry finds it; on a module path, its provides does.
Configuration sources NEW
A provider’s keys are one of two kinds:
-
Opt-out keys decide whether a service starts at all —
vidocq.pool[.<name>].url,mp.jwt.verify.issuer. Only an explicit-D, an environment variable, or the dev-goal configuration switches a service off through them, nevervidocq.properties/application.properties: a production default baked into the application’s own file must never silently switch a dev container off. A provider may still read that file to learn what the application is configured for (A PostgreSQL container only for a PostgreSQL application NEW). -
Tuning keys, everything under
vidocq.dev.—vidocq.dev.postgres.port,vidocq.dev.postgres.datasources,vidocq.dev.reuse,vidocq.dev.devServices— say how to start a service that is already going to start. These are read from the application’s own files too, in the runtime’s own order (target/classes, after resource filtering), so a stable port pinned once invidocq.propertiesnow works without repeating a-Don every invocation:
# src/main/resources/vidocq.properties
vidocq.dev.postgres.port=55432
vidocq.dev.reuse=true
vidocq.pool.url in that same file is still no opt-out: a PostgreSQL URL there never disables the
PostgreSQL dev service. What the file tells the provider is which database the application uses.
A PostgreSQL container only for a PostgreSQL application NEW
With vidocq-runtime-devservice-postgres among the plugin’s dependencies, the PostgreSQL dev
service decides, for the @Default datasource (vidocq.pool.url) and then for each name of
vidocq.dev.postgres.datasources (vidocq.pool.<name>.url), in this order:
| When | Container | What it says |
|---|---|---|
The URL is given explicitly: a |
No |
Nothing in the report; the log keeps |
The application’s file gives a |
No |
|
The file gives a |
Yes, and its URL replaces the file’s under the dev host |
The service, as before |
No URL at all, or a value that is not a readable |
Only when |
Without the driver: |
The class path is the one the application will run with: its runtime dependencies under
vidocq:dev and vidocq:run, its test dependencies under vidocq:test and a JUnit run. An
application on PostgreSQL at runtime and on H2 in its tests, whose file names PostgreSQL, still gets
its container in a test run. The driver is looked up as a .class entry, never loaded.
The file is read where the build put it, target/classes; on a tree not built yet, vidocq:dev
reads src/main/resources instead. A vidocq.properties edited since the last build is read as
built: run process-resources (or any build) after changing the URL. vidocq:test resolves the
test dependencies at start, dev services on or off, as the tests it forks need them anyway.
When one datasource gets a container and another does not, the log still says why for the other:
Postgres dev service: datasource 'audit' not started: vidocq.pool.audit.url is jdbc:h2, not PostgreSQL.
An application that relied on the container with no URL anywhere and the PostgreSQL
driver in test scope only no longer gets one under vidocq:dev and vidocq:run, which look at
the runtime class path. Write its production URL in vidocq.properties
(vidocq.pool.url=jdbc:postgresql://…): the container is back, its URL replacing that one.
|
A reason names the key and the URL’s scheme — jdbc:h2, as written — never the rest of the URL,
which may carry a host, a user or a password. When no datasource gets a container, the reasons of
all of them are joined with ; `. The log says `DevService 'postgres' not started: <reason>, the
state file keeps it, and the report shows it (The startup report section and dev console panel NEW).
Container names and labels NEW
A container a dev service starts is named after Vidocq, the application and the service, so
docker ps tells it apart from the rest:
vidocq-dev-mcp-tasks-server-postgres-3f9a2c01 vidocq-dev-mcp-tasks-server-postgres-analytics-7b04e1d9 vidocq-dev-shop-keycloak-c2a18f5e
The application is the name of the project’s directory, and a named datasource follows the
service. A Docker name is unique on the machine, so the last part keeps two applications, two
datasources, or vidocq:dev and the tests of the same project apart. It is random for a
container that is not reused. For a reused one (vidocq.dev.reuse=true), it is a digest of the
application’s path and the container’s configuration: the name stays the same from one session to
the next while the configuration does, and changes with it. Testcontainers finds a reused container
by a hash that covers its name.
vidocq.dev.container-prefix replaces vidocq-dev-, as any tuning key, from a -D or from
vidocq.properties. It must start a valid Docker name, letters, digits, _, . and -, and
start with a letter or a digit; otherwise the start fails with a message that names the key.
Each container also carries three labels: io.vidocq.dev=true, io.vidocq.dev.app=<application>
and io.vidocq.dev.service=<service>. docker ps --filter label=io.vidocq.dev lists them all,
whatever their prefix. The testcontainers-ryuk-… container that removes them at the end keeps its
own name.
vidocq:run (opt-in) NEW
vidocq:run starts the application the way the production launcher does, so it starts no
container unless asked — the same vidocq.dev.devServices tuning key turns it on, as a -D or in
vidocq.properties:
mvn vidocq:run -Dvidocq.dev.devServices=true
Once on, vidocq:run and vidocq:dev share every other piece: the providers, the
"Connection information" console block, target/vidocq-dev-services.properties, the state file,
and the extension added to the child’s module path. Ctrl+C, or the process exiting on its own,
stops the containers exactly once.
Switching dev services on and off NEW
Every host — vidocq:dev, vidocq:run and the JUnit listener — reads vidocq.dev.devServices
the same way, first match wins:
-
the explicit value: a
-Dvidocq.dev.devServices=…, the goal’s<devServices>configuration, or (under the JUnit host) the system property; -
then the application’s files,
vidocq.propertiesorapplication.properties; -
then the host’s default: on for
vidocq:devand tests, off forvidocq:run.
The value is true or false, trimmed, in any case; anything else fails the goal, or the test
run, with a message naming the key and the value. A vidocq.properties that says true opts
every vidocq:run into dev services, and mvn vidocq:run -Dvidocq.dev.devServices=false still
turns them off for that one run.
In tests NEW
A test that only needs a database adds the JUnit host’s listener and the provider it wants, both
test scope:
<dependency>
<groupId>io.vidocq.runtime</groupId>
<artifactId>vidocq-runtime-devservices-junit</artifactId>
<version>0.4.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.vidocq.runtime</groupId>
<artifactId>vidocq-runtime-devservice-postgres</artifactId>
<version>0.4.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
No annotation is needed in the tests themselves: vidocq-runtime-devservices-junit registers a
JUnit Platform LauncherSessionListener, discovered through META-INF/services, that starts the
providers found on the test class path once, before any test class, injects their coordinates as
system properties (vidocq.pool.url, .username, .password, …), and stops them once the whole
test run ends — one set of containers for the whole suite. -Dvidocq.dev.devServices=false, or
that same key in the application’s own files, skips it entirely; without Docker or Podman, the
listener fails the run fast, naming the runtime it looked for and the key that switches it off, so
a test that needed the database does not time out silently.
The listener — and any LauncherSessionListener a project adds the same way — must
ship as its own jar, a test-scope dependency the consuming module’s module-info.java does not
requires, never compiled straight into that module’s own src/test/java. Surefire’s default
module-path --patch-module folds a module’s entire target/test-classes tree into its own named
module; a listener class that landed there would be invisible to ServiceLoader, which only
honours an explicit provides … with … in a named module, never a META-INF/services file. Kept
as a separate jar, it lands on the class path instead, in the unnamed module, where
ServiceLoader finds it — which is also why no <useModulePath>false</useModulePath> is needed
in a consuming project’s pom.xml.
|
The startup report section and dev console panel NEW
Whichever host started them, the application reads one state file,
<basedir>/target/vidocq-dev-services.json — <basedir> is always the project’s own pom.xml
directory, unaffected by a custom project.build.directory — through
vidocq-runtime-devservices-extension, which the host adds to the child’s module path (the
Maven-plugin hosts) or which arrives on the class path (the JUnit host, transitively with
vidocq-runtime-devservices-junit). An application declares nothing itself.
A real run of this repository’s own end-to-end integration test
(vidocq-runtime-it-devservices, -Pdocker) shows what the detailed report prints:
devservices Dev services | 0 ms
1 service: postgres (postgres:16-alpine at localhost:32851) — vidocq:run
started 2026-09-24T20:11:15.781794Z by vidocq:run
postgres postgres:16-alpine, default localhost:32851
postgres vidocq.pool.password configured
postgres vidocq.pool.url jdbc:postgresql://localhost:32851/vidocq?loggerLevel=OFF
postgres vidocq.pool.username vidocq
The summary names every service, its image and its first endpoint, and the host that started it;
the detailed report adds a row per service and, in a dev launch, every key it injected — a
secret (vidocq.pool.password above) always as configured, never its value, whatever the launch
mode. Without a state file or the vidocq.devservices.state system property, the summary reads
no dev service: not started by vidocq:dev, vidocq:run or the test launcher — not an anomaly, the
ordinary state of an application nobody has started with a dev service yet. The extension never
scans target/ for a stray file on its own: a killed session’s leftover file is read again only
when a later host overwrites vidocq.devservices.state, never merely because the file exists.
A provider that did not start and said why (A PostgreSQL container only for a PostgreSQL application NEW) gets a row of its own,
at every verbosity: the row postgres, its value
not started: vidocq.pool.url is jdbc:h2, not PostgreSQL. When no service started and at least one
said why, the summary reads no dev service started. The dev console’s Dev services panel shows
the same rows. The state file keeps them as "skipped": [{"id": "postgres", "reason": "…"}]; a file
written before that key existed reads as none.
VIDOCQ-DEVS-001 NEW
Raised when the state file exists but could not be read — a corrupt or half-written file, most
often from a very old, incompatible Vidocq version. The boot goes on; the section reads
state file unreadable, and the anomaly’s message names the file’s path. Rerun the goal that starts dev services: the Maven plugin, or the JUnit
listener, rewrites the file cleanly.
One state file per project NEW
The state file’s path is the project’s, not the host’s: every host of the same project shares
it, and target/vidocq-dev-services.properties too. Running mvn test while vidocq:dev runs in
another terminal starts the test run’s own containers and overwrites both files; when the test run
ends, it marks the state file stopped, host test, although vidocq:dev’s containers still run.
The running application keeps what it read at boot, but a reload reads the file again. Restart
`vidocq:dev, or avoid running both at once, when the files must describe it.
The dev console panel of the same name shows the same boot facts, live: which service, its image
and endpoints, and the keys it injected, a secret always as configured. The application cannot
reach Docker itself, so the panel has no live samples of its own — boot facts only, no chart.
Continuous testing NEW
Continuous testing reuses the dev containers: under vidocq:dev every test run
gets the session’s connection keys, and vidocq:test opens a session of its own, host vidocq:test. Each run
gets -Dvidocq.dev.devServices=false, so the tests never start a second set of containers.
| The tests then share the dev database: a test that deletes rows deletes your dev data. |
No secret leaves the application NEW
A key whose last dot-separated segment names a secret — password, passwd, pwd, secret,
token, key, credential(s), apikey, api-key, private-key, in any case — is written to
the state file as "configured": true, never with its value; a URL loses its user:password@
part and any secret query parameter is masked as * before anything is written. Only
target/vidocq-dev-services.properties, documented in DEV_SERVICES.md’s "Connection
information", keeps a password in clear text, on purpose: it is the local, machine-only file an
external tool — `psql, DataGrip, a migration runner — reads to connect to the dev database
without a JDBC URL copied by hand.