Skip to content

fix: make the documented docker compose command work - #2182

Merged
netomi merged 5 commits into
mainfrom
fix/docker-compose-profiles
Sep 8, 2026
Merged

netomi merged 5 commits into
mainfrom
fix/docker-compose-profiles

Conversation

@netomi

@netomi netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Picks up the three findings still standing from #1258, which reported them in June 2025 against a very different version of this file.

The documented command does not work

doc/development.md offers this verbatim as the way to start a dev stack:

$ docker compose --profile openvsx --profile kibana up
service "server" depends on undefined service "postgres": invalid compose project

server is in openvsx/backend; postgres is in db/debug. The project fails to validate before a single container is created, so anyone following our own instructions hits this on their first command. postgres and elasticsearch now also belong to the profiles that select the service depending on them.

Two healthchecks could never have passed

Which is presumably why nobody noticed the profiles were broken — nothing downstream ever waited on health.

elasticsearch curled http://elasticsearch01:9200. There is no such host here; the service is elasticsearch. From inside the running container, while the cluster is status: green:

elasticsearch01 -> exit 6   (couldn't resolve host)
localhost       -> exit 0

So it stayed starting for the full 50 retries and then reported unhealthy, with a perfectly good cluster behind it.

server curled localhost:8081/actuator/health, and eclipse-temurin:25-jdk has no curl:

$ docker run --rm --entrypoint sh eclipse-temurin:25-jdk -c 'command -v curl wget nc python3'
(nothing)
$ docker run --rm --entrypoint sh eclipse-temurin:25-jdk -c 'curl --fail --silent localhost:8081/actuator/health'
sh: 1: curl: not found

bash is present, and its /dev/tcp can ask the actuator the same question with nothing to install:

test: ['CMD-SHELL', 'exec 3<>/dev/tcp/localhost/8081 && printf "GET /actuator/health HTTP/1.0\r\n\r\n" >&3 && grep -q UP <&3']

Single-quoted deliberately: in a double-quoted YAML scalar those \r\n become real control characters before bash sees them, which happens to work but only by accident of double interpretation.

The alternative was apt-get install curl on every container start, which is what #1258 proposed — reasonable when the base image was openjdk:17, but a network round trip on each up and no longer necessary.

Which lets depends_on do its job

With both checks working, depends_on can wait on readiness instead of only on container creation. That is the ordering problem #1258 originally reported: the server no longer races a Postgres still initialising or an Elasticsearch that has not opened its port. postgres gained a pg_isready check so it can be waited on at all.

What I did not take from #1258

Worth recording so the PR can be closed with a clear conscience. Two of its changes have since landed independently — the ES_JAVA_OPTS memory settings and the memlock ulimits are both in main already. The rest is overtaken or not worth it: the openjdk:17-bullseye switch (main is on temurin 25), SPRING_PROFILES_ACTIVE/ELASTICSEARCH_HOST/POSTGRES_HOST env vars (the repo uses generate-properties.sh --docker), /etc/localtime mounts on every service (cosmetic, and breaks on Docker Desktop hosts), "0.0.0.0:8080:8080" (identical to 8080:8080), removing the cli service, and docs/docker-deployment-fix.md, which is in Chinese and mostly describes the above.

Docs

backend now brings its dependencies, so its description says so. I also dropped the "Without profiles: docker compose up" line — every service in the file has a profile, so that command starts nothing:

$ docker compose config --services
(empty)

Verified

  • both profile commands resolve to a valid project
  • postgres healthy in ~12s, elasticsearch in ~36s, where elasticsearch previously never reached it
  • a cold up server creates the server container only once both report healthy — dependencies started 20:15:45, server 20:16:01
  • the /dev/tcp check exits 0 against a live endpoint and non-zero against a closed port, tested inside eclipse-temurin:25-jdk itself

What I did not run is a full --profile openvsx up through to a healthy server, since that builds the server with Gradle inside the container. The healthcheck command is verified against a real HTTP endpoint in the same image, but the first person to run the whole stack should confirm the server actually flips to healthy.

Closes #1258 — credit to @hbyecoding for the report.

doc/development.md offers `docker compose --profile openvsx --profile kibana up`
as the way to start a development stack. It does not start anything:

    service "server" depends on undefined service "postgres": invalid compose project

`server` is in the `openvsx` and `backend` profiles while `postgres` is in `db`
and `debug`, so the project fails to validate before a container is created.
Anyone following our own instructions hits this first. postgres and
elasticsearch now also belong to the profiles that select the service depending
on them.

Two healthchecks could never have passed either, which is why nothing noticed:

  - elasticsearch curled `http://elasticsearch01:9200`, a host that does not
    exist here - the service is `elasticsearch`. Inside the container that
    resolves to exit 6 while the cluster is green, so it sat in "starting" for
    the full 50 retries and then reported unhealthy.
  - server curled `localhost:8081/actuator/health`, and eclipse-temurin:25-jdk
    ships no curl, wget, nc or python. bash is there, so its /dev/tcp asks the
    actuator the same question with nothing to install. The alternative was
    apt-get on every start, which is what #1258 proposed and what its author
    was working around when the base image was openjdk:17.

With both fixed, `depends_on` can finally wait on readiness rather than only on
container creation, which is the ordering problem #1258 originally reported: the
server no longer races a Postgres that has not finished initialising or an
Elasticsearch that has not opened its port. postgres gained a pg_isready check
for the same reason.

Credit to @hbyecoding, who reported all of this in #1258 in June 2025 against
the file as it then was. Most of that PR has since been overtaken - the memory
settings and ulimits it added are in main already, and the base image it fixed
is gone - but the three findings underneath it were still here.

Verified with docker: both profile commands now resolve, postgres reports
healthy in ~12s and elasticsearch in ~36s where it previously never did, and a
cold `up server` creates the server container only after both report healthy
(dependencies at 20:15:45, server at 20:16:01).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new server healthcheck relies on bash-only /dev/tcp but is configured as CMD-SHELL, which runs under /bin/sh and can break readiness/depends_on ordering.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes the developer documentation and Docker Compose setup so the documented docker compose --profile ... up commands validate and the service readiness ordering works correctly via healthchecks.

Changes:

  • Adds healthchecks and aligns postgres/elasticsearch profiles so backend/openvsx stacks validate and can be selected as documented.
  • Fixes the Elasticsearch healthcheck host to use localhost (so it can actually pass).
  • Updates development docs to reflect that profiles are required and that backend brings its dependencies.
File summaries
File Description
docker-compose.yml Adds Postgres healthcheck, fixes Elasticsearch healthcheck, and updates profile/depends_on wiring for backend/openvsx.
doc/development.md Updates Docker Compose instructions to match the actual profile behavior and dependency startup.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docker-compose.yml Resolved
Adding postgres and elasticsearch to the `openvsx` and `backend` profiles made
those profiles claim services that are not theirs - postgres is not part of the
backend - and it only papered over one instance of a general problem. Selecting
a profile that holds a service but not that service's dependency is a hard
error, and four of the documented profiles were in that state:

    --profile frontend      service "webui" depends on undefined service "server"
    --profile commandline   service "cli" depends on undefined service "server"
    --profile kibana        service "kibana" depends on undefined service "elasticsearch"
    --profile openvsx       service "server" depends on undefined service "postgres"

`required: false` says the same thing without the ownership: each service
declares what it needs, and the profiles selected decide what is actually
present. Every profile now resolves to exactly the services it names, and
postgres and elasticsearch stay in `db`/`es`/`debug` where they belong.

Verified that this keeps the health gating rather than trading it away, which
was the reason for the conditions in the first place: with
`--profile openvsx --profile debug`, both dependencies are waited on and the
server container is created only afterwards (dependencies 20:45:32, server
20:45:47). An isolated compose project confirms the same for a dependency that
turns healthy on a delay - it is waited for whenever it is enabled, and skipped
silently when it is not.

The cost is that `--profile openvsx` on its own now starts a server with no
database instead of refusing to start at all, so the documentation says to
select the infrastructure too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi

netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

You are right, and it turned out the objection points at a better fix rather than just a tidier one. Reworked in 45a7608.

Why the profile membership was wrong

Two reasons. The one you gave — postgres is not part of "the backend" — and a second I only found when I went looking: adding it to openvsx/backend fixed one instance of a general problem. Selecting a profile that holds a service but not that service's dependency is a hard error, and four documented profiles were in that state:

--profile frontend      service "webui" depends on undefined service "server"
--profile commandline   service "cli" depends on undefined service "server"
--profile kibana        service "kibana" depends on undefined service "elasticsearch"
--profile openvsx       service "server" depends on undefined service "postgres"

Carrying on down the membership route would have meant adding frontend to server, commandline to server, and kibana to elasticsearch — each one less true than the last.

What it does now

depends_on: { <dep>: { condition: ..., required: false } } on every dependency in the file. That states the dependency without claiming ownership: the service says what it needs, and the selected profiles decide what is actually present. postgres and elasticsearch go back to db/es/debug.

Every profile now resolves to exactly the services it names:

db            postgres
es            elasticsearch
debug         elasticsearch postgres
backend       server
frontend      webui
commandline   cli
openvsx       server cli webui
kibana        kibana

The thing I checked before trusting it

required: false sounds like it might weaken the health conditions, which were the whole point of the earlier change. It does not — the dependency is waited for whenever it is enabled, and skipped silently when it is not. In an isolated compose project with a dependency that only turns healthy after 20s:

Container ctest-dep-1 Started
Container ctest-dep-1 Waiting
Container ctest-dep-1 Healthy
Container ctest-app-1 Started
dep 20:43:52   app 20:44:13

And on the real file with --profile openvsx --profile debug:

postgres 20:45:32   elasticsearch 20:45:32   server 20:45:47

I also confirmed Compose does not resolve this by itself — a profiled dependency of a selected service is not auto-enabled, it is a validation failure, on v5.5.0 at least. So required: false is the mechanism rather than a workaround for something that should have worked.

The trade

--profile openvsx alone now starts a server with no database instead of refusing to start at all. That is a real downside — a confusing runtime failure rather than an upfront error — so the docs now say to select the infrastructure as well:

Each profile starts only its own services, so pick the infrastructure too: docker compose --profile openvsx --profile debug up runs the whole stack, while --profile openvsx alone starts the server, web UI and CLI against a database that is not there.

If you would rather --profile openvsx be self-sufficient, the alternative is to drop the profiles key from postgres and elasticsearch entirely, so they are always started. That makes --profile openvsx work unqualified, at the cost of starting a database for --profile frontend too and making the db/es profile names meaningless. I preferred the version where every profile means what it says, but it is your call.

The healthcheck fixes in the first commit are unchanged.

…the profiles

Review feedback from Copilot, which was right and caught a check that would
have been as broken as the one it replaced. CMD-SHELL runs /bin/sh, and on
eclipse-temurin /bin/sh is dash:

    $ docker run --rm --entrypoint sh eclipse-temurin:25-jdk -c 'ls -l /bin/sh'
    /bin/sh -> dash
    $ ... -c 'exec 3<>/dev/tcp/127.0.0.1/22'
    sh: 1: cannot create /dev/tcp/127.0.0.1/22: Directory nonexistent

/dev/tcp is a bash feature. I had verified the command with `--entrypoint bash`
and not through the path the healthcheck actually takes, so I proved the wrong
thing. Now the exec form, invoking bash by name.

Re-verified through compose, since `docker run --health-cmd` takes a shell
string and so reproduces the dash bug rather than testing the fix: a container
serving {"status":"UP"} on 8081 reports healthy, and one serving nothing
reports unhealthy.

Also documents `minio`, `valkey` and `valkey-admin`, which had no entry in
development.md at all, along with the ports and credentials they come up on.
Every profile the compose file defines is now listed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi

netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Also added the three profiles that had no entry in doc/development.md — you spotted valkey and valkey-admin, and minio was missing too:

$ comm -23 <(docker compose config --profiles | sort) <(documented profiles)
minio
valkey
valkey-admin

All eleven are now listed, with the ports and credentials each comes up on (MinIO console on 9001 with minioadmin/minioadmin, Valkey nodes on 7001-7006 with openvsx/openvsx, the Valkey UI on 8090). The minio entry notes that its bucket and download policy are recreated on every start because that service keeps no volume, and the valkey entry notes the cluster is formed once and left alone afterwards — both are things the compose file explains in comments but the docs never mentioned.

kibana:8.7.1 against elasticsearch:9.2.8 does not work at all - Kibana refuses
a cluster on a different major version:

    [ERROR][elasticsearch-service] This version of Kibana (v8.7.1) is
    incompatible with the following Elasticsearch nodes in your cluster:
    v9.2.8 @ 172.19.0.2:9200

So the `kibana` profile has been non-functional since Elasticsearch moved to 9.
Both are now 9.2.8, and the comment says they move together.

While there: ELASTICSEARCH_URL has not been a Kibana setting since 7.0, where
`elasticsearch.url` was replaced by `elasticsearch.hosts`. The image carries no
such key at all, so that line was quietly doing nothing:

    $ docker run --rm kibana:9.2.8 grep elasticsearch config/kibana.yml
    elasticsearch.hosts: [ "http://elasticsearch:9200" ]

Renamed to ELASTICSEARCH_HOSTS. The image default happens to be the same URL,
so this had gone unnoticed - but relying on an image default matching our
service name is not something to leave to chance.

Kibana now also waits for the Elasticsearch healthcheck rather than merely for
its container, which is available since that check works again.

Verified: kibana 9.2.8 against elasticsearch 9.2.8 reports overall `available`
with `elasticsearch: available`, where 8.7.1 logs the incompatibility above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi

netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Good spot — it was worse than out of date, the kibana profile has been completely non-functional since Elasticsearch moved to 9. Fixed in 039b868.

Kibana refuses a cluster on a different major version. Running the old image against the current Elasticsearch:

[ERROR][elasticsearch-service] This version of Kibana (v8.7.1) is incompatible with the
following Elasticsearch nodes in your cluster: v9.2.8 @ 172.19.0.2:9200

Both are 9.2.8 now, with a comment saying they move together.

While in there I found a second thing wrong with that service: ELASTICSEARCH_URL has not been a Kibana setting since 7.0, where elasticsearch.url was replaced by elasticsearch.hosts. The image carries no such key at all —

$ docker run --rm kibana:9.2.8 grep elasticsearch /usr/share/kibana/config/kibana.yml
elasticsearch.hosts: [ "http://elasticsearch:9200" ]

— so that line was quietly doing nothing. It is ELASTICSEARCH_HOSTS now. Amusingly the image default is that exact URL because our service is called elasticsearch, which is presumably why nobody noticed, but relying on an image default matching our service name is not something to leave to chance.

Kibana also waits on the Elasticsearch healthcheck now rather than just its container, which only became possible once that check worked again.

Verified end to end: kibana 9.2.8 against elasticsearch 9.2.8 reports

overall: available
elasticsearch: available - Elasticsearch is available
version: 9.2.8

Worth noting these are the two versions that need to track each other by hand — nothing enforces it, so a future Elasticsearch bump has to bring Kibana along or the profile silently breaks again.

@netomi

netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Done in 11d9d5e, though not with an anchor — I need to correct my own suggestion. A YAML anchor substitutes a whole value, so image: elasticsearch:*elastic-version is not valid YAML; you cannot interpolate an anchor into part of a string. Compose's variable substitution is what actually gets the number into one place:

# .env
ELASTIC_VERSION=9.2.8
elasticsearch:
  image: elasticsearch:${ELASTIC_VERSION}
kibana:
  image: kibana:${ELASTIC_VERSION}

Verified it is genuinely one source and not just two things that happen to resolve the same:

$ ELASTIC_VERSION=8.15.0 docker compose --profile es --profile kibana config | grep image
    image: elasticsearch:8.15.0
    image: kibana:8.15.0

and the default pair still comes up talking to each other — kibana 9.2.8 available against a healthy elasticsearch, all eleven profiles still resolving.

Two trade-offs worth knowing before you take it:

  • .env is now tracked. That is the conventional home for local secrets, so there is a note at the top of the file saying it holds non-secret defaults only and that overrides belong in the shell environment, which takes precedence over the file. If you would rather not have a tracked .env at all, the alternative is to keep both literal tags but move them adjacent at the top of the file under a comment — the number appears twice, but a mismatch is then visible at a glance rather than thirty lines apart.
  • A version behind a variable is invisible to Dependabot. It only covers github-actions in this repo today, so nothing automated is lost — but if you ever add the docker-compose ecosystem, it would skip these two and keep managing the rest.

doc/development.md notes where the pair lives so the next person bumping Elasticsearch finds it.

@netomi
netomi force-pushed the fix/docker-compose-profiles branch from 11d9d5e to 039b868 Compare September 8, 2026 21:24
@netomi

netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Dropped 11d9d5e — the .env/ELASTIC_VERSION change is out, and the branch is back to 039b868.

Both images are literal tags again:

elasticsearch:
  image: elasticsearch:9.2.8
kibana:
  image: kibana:9.2.8

Everything from the earlier commits is untouched — the version alignment itself, the ELASTICSEARCH_HOSTS rename, and the comment on the kibana service saying the two versions move together. So the profile works; keeping the numbers in step is back to being a matter of noticing the comment.

I removed the commit rather than adding a revert on top, so the PR does not carry an add-then-undo pair. That needed a force push, done with --force-with-lease pinned to the previous head so it could not have overwritten anything else.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The updated docs include a relative link that points to a non-existent doc/docker-compose.yml path and should be corrected to reference the root compose file.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread doc/development.md Resolved
Review feedback. `[docker-compose.yml](docker-compose.yml)` is relative to
doc/, so it resolved to doc/docker-compose.yml, which does not exist.

The other mention of the same file, a few lines up, was broken differently:
`[docker-compose.yml]` is a shortcut reference with no definition anywhere in
the document, so it renders as literal brackets rather than a link.

Both predate this pull request, but this is the document it has been editing
and they point at the file it is about.

Audited the rest of the file's relative links while there; these two were the
only broken ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi
netomi merged commit a4e6a04 into main Sep 8, 2026
5 checks passed
@netomi
netomi deleted the fix/docker-compose-profiles branch September 8, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants