Files
computer-playbook/roles/web-app-nextcloud/templates/compose.yml.j2
kevinveenbirkenbachandClaude Opus 5 b036f60039 refactor(container_ports): express the remaining published ports through the lookup
The list form could publish a host:container pair and nothing else, so thirteen blocks stayed hand-written: the ones needing a protocol suffix, a port range, the swarm long syntax with mode host, or a container-port-only publish. Each of those duplicated numbers the role already declares in meta/services.yml, which is the drift the lookup exists to remove.

The lookup keeps the list form unchanged for its 93 existing call sites and gains a dict form: transport emits one item per protocol, container 'same' serves the services that cannot remap (STUN/TURN, media relays), mode 'host' emits the long block an edge role needs in swarm so the client address survives the hop, publish 'ephemeral' emits the container port alone, and key names the block for Discourse's launcher file, whose expose: list is what becomes docker run -p there. A declared {start, end} mapping renders as start-end on both sides, so a relay range needs no term of its own.

svc-prx-openresty had no ports block at all and bound 80 and 443 as literals in its template, while torrc.j2 carried a third literal 80 for the hidden service. None of the three referenced the others. openresty now declares the ports, the compose template reads them, and the onion's port 80 comes from ports.onion.http through TOR_ONION_EXTRA_PORTS.

Three blocks stay raw, each with the reason it cannot route through one call: keycloak's sso_proxy maps a published port owned by the consuming role to a container port owned by web-app-keycloak, and matrix's ansible flavor takes element on 8080 and federation on 8048 from the upstream playbook rather than from this repo's ports.internal.

The enforcing lint scanned only files with compose in the name and only double-quoted list items, so nine of the thirteen blocks were invisible to it. It now reads every role template that can carry a ports block, catches unquoted items, protocol suffixes, ranges, flow style and the long syntax, and tracks the block by the indentation of its key: keying on the next line closed the block on anything shaped like a key, which target, published, protocol and mode all are. Under expose: only a real mapping counts, because there the single-segment form documents a container port and publishes nothing.

Verified by rendering all 110 container_ports call sites through the real templar, the real lookup and the real merged applications view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 14:55:25 +02:00

192 lines
9.3 KiB
Django/Jinja

{% include 'roles/sys-svc-compose/templates/base.yml.j2' %}
{% set service_name = NEXTCLOUD_PROXY_SERVICE %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/deploy.yml.j2' %}
{{ lookup('container_image', application_id, 'proxy') }}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_PROXY_CONTAINER) }}
logging:
driver: journald
{{ lookup('compose_restart') }}
{{ lookup('container_ports', ['nextcloud', 'http', DOCKER_BIND_HOST]) | indent(4) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
{{ lookup('container_healthcheck', service_name, samples=(groups[application_id] | default([]) | length) if DEPLOYMENT_MODE == 'swarm' else 1) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_extra_hosts', extra_hosts=(
['host.docker.internal:host-gateway'] if NEXTCLOUD_HPB_NETWORK_MODE == 'host' else []
)) | indent(4) }}
{% set service_name = NEXTCLOUD_SERVICE %}
{% set container_healthcheck_start_period = '20m' %}
{{ service_name }}:
{{ lookup('container_image', application_id, 'nextcloud') }}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_CONTAINER) }}
{{ lookup('template', 'roles/sys-svc-container/templates/build.yml.j2') | indent(4) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
{% if DEPLOYMENT_MODE == 'swarm' %}
environment:
TASK_SLOT: "{% raw %}{{.Task.Slot}}{% endraw %}"
{% endif %}
sysctls:
net.ipv6.conf.all.disable_ipv6: "{{ (not NETWORK_IPV6_ENABLED | bool) | int }}"
healthcheck:
test: ["CMD", "su", "{{ NEXTCLOUD_DOCKER_USER }}", "-s", "/bin/sh", "-c", "php -r 'exit(@fsockopen(\"127.0.0.1\", {{ NEXTCLOUD_FPM_PORT }}) ? 0 : 1);' && php {{ NEXTCLOUD_INTERNAL_OCC_COMMAND }} status"]
# nocheck: healthcheck-timing-override occ boots the whole Nextcloud stack per probe, so it stays off a 30s loop
interval: 1m
# nocheck: healthcheck-timing-override occ regularly needs more than the 5s default to answer
timeout: 30s
retries: 10
# nocheck: healthcheck-timing-override a role variable the linter cannot compare, tuned per deployment size
start_period: {{ container_healthcheck_start_period }}
start_interval: 5s
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_extra_hosts') | indent(4) }}
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% if NEXTCLOUD_MCP_ENABLED | bool %}
{% set service_name = NEXTCLOUD_CONTEXT_AGENT_SERVICE %}
{% set container_port = NEXTCLOUD_CONTEXT_AGENT_PORT %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_image', application_id, 'context_agent') }}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_CONTEXT_AGENT_CONTAINER) }}
environment:
APP_ID: {{ NEXTCLOUD_CONTEXT_AGENT_SERVICE }}
APP_VERSION: "{{ NEXTCLOUD_CONTEXT_AGENT_VERSION }}"
APP_HOST: 0.0.0.0
APP_PORT: "{{ container_port }}"
APP_SECRET: "{{ NEXTCLOUD_CONTEXT_AGENT_SECRET }}"
NEXTCLOUD_URL: "{{ NEXTCLOUD_CONTEXT_AGENT_NEXTCLOUD_URL }}"
{{ lookup('container_healthcheck', service_name) | indent(4) }}
expose:
- "{{ container_port }}"
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% endif %}
{% if NEXTCLOUD_HPB_SIGNALING_ENABLED | bool %}
{% set service_name = NEXTCLOUD_HPB_SERVICE %}
{% set container_port = NEXTCLOUD_HPB_SIGNALING_PORT %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_healthcheck', service_name) | indent(4) }}
{{ lookup('container_image', application_id, 'talk') }}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_HPB_CONTAINER) }}
init: true
{% if NEXTCLOUD_HPB_NETWORK_MODE == 'bridge' %}
{% if NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED | bool and NEXTCLOUD_HPB_TURN_ONBOARD_PUBLISH_PORTS | bool %}
{{ lookup('container_ports',
{'service': 'nextcloud', 'protocol': 'stun_turn',
'ip': NEXTCLOUD_HPB_TURN_ONBOARD_BIND_HOST,
'transport': ['tcp', 'udp'], 'container': 'same'},
{'service': 'nextcloud', 'protocol': 'relay',
'ip': NEXTCLOUD_HPB_TURN_ONBOARD_BIND_HOST,
'transport': ['udp', 'tcp'], 'container': 'same'}) | indent(4) }}
{% endif %}
expose:
- "{{ container_port }}"
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% else %}
network_mode: {{ NEXTCLOUD_HPB_NETWORK_MODE }}
{% endif %}
{% endif %}
{% if NEXTCLOUD_WHITEBOARD_ENABLED %}
{% set service_name = NEXTCLOUD_WHITEBOARD_SERVICE %}
{% set container_port = NEXTCLOUD_WHITEBOARD_PORT_INTERNAL %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
build:
context: .
dockerfile: {{ NEXTCLOUD_WHITEBOARD_SERVICE_DOCKERFILE }}
{{ lookup('compose_only', 'pull_policy', 'never') }}
{{ lookup('container_image', application_id, 'whiteboard') }}
{{ lookup('container_healthcheck', service_name) | indent(4) }}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_WHITEBOARD_CONTAINER) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
environment:
- NODE_OPTIONS=--max-old-space-size={{ NEXTCLOUD_WHITEBOARD_MAX_OLD_SPACE_SIZE }}
expose:
- "{{ container_port }}"
shm_size: 1g
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{%+ if lookup('engine', 'redis', application_id, 'local') %}{{ lookup('depends_on', {'redis': 'service_healthy'}) }}{% endif %}
{% endif %}
{% set service_name = NEXTCLOUD_CRON_SERVICE %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/deploy.yml.j2' %}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_CRON_CONTAINER) }}
{{ lookup('container_image', application_id, 'nextcloud') }}
{{ lookup('compose_restart') }}
logging:
driver: journald
{{ lookup('container_volumes', service_name) | indent(4) }}
entrypoint: /cron.sh
sysctls:
net.ipv6.conf.all.disable_ipv6: "{{ (not NETWORK_IPV6_ENABLED | bool) | int }}"
healthcheck:
test: ["CMD", "su", "{{ NEXTCLOUD_DOCKER_USER }}", "-s", "/bin/sh", "-c", "php {{ NEXTCLOUD_INTERNAL_OCC_COMMAND }} status"]
# nocheck: healthcheck-timing-override occ boots the whole Nextcloud stack per probe, so it stays off a 30s loop
interval: 1m
# nocheck: healthcheck-timing-override occ regularly needs more than the 5s default to answer
timeout: 30s
retries: 10
# nocheck: healthcheck-timing-override a role variable the linter cannot compare, tuned per deployment size
start_period: {{ container_healthcheck_start_period }}
start_interval: 5s
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% if NEXTCLOUD_RECORDING_ENABLED | bool %}
{% set service_name = 'talk_recording' %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_image', application_id, 'talk_recording') }}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_RECORDING_CONTAINER) }}
environment:
- HPB_DOMAIN={{ NEXTCLOUD_HPB_DOMAIN }}
- NC_DOMAIN={{ NEXTCLOUD_DOMAIN }}
- RECORDING_SECRET={{ NEXTCLOUD_RECORDING_SECRET }}
- INTERNAL_SECRET={{ NEXTCLOUD_HPB_INTERNAL_SECRET }}
- TZ={{ HOST_TIMEZONE }}
expose:
- "{{ NEXTCLOUD_RECORDING_PORT }}"
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
{% endif %}
{% if NEXTCLOUD_MCP_ENABLED | bool %}
{% set service_name = NEXTCLOUD_MCP_ADAPTER_SERVICE %}
{% set container_port = NEXTCLOUD_MCP_ADAPTER_PORT %}
{% set docker_compose_env = [NEXTCLOUD_MCP_ADAPTER_INSTANCE_DIR, 'upstream.env'] | path_join %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_image', application_id, NEXTCLOUD_MCP_ADAPTER_SERVICE) }}
build:
context: {{ NEXTCLOUD_MCP_ADAPTER_INSTANCE_DIR }}
dockerfile: Dockerfile
args:
MCP_ADAPTER_BASE_IMAGE: {{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_MCP_ADAPTER_SERVICE ~ '.image') }}
MCP_ADAPTER_BASE_VERSION: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_MCP_ADAPTER_SERVICE ~ '.version') }}"
{{ lookup('compose_only', 'container_name', NEXTCLOUD_MCP_ADAPTER_CONTAINER) }}
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment:
ADAPTER_CONTRACT: {{ NEXTCLOUD_MCP_ADAPTER_CONTRACT | to_json | to_json | replace('$', '$$') }}
ADAPTER_BEARER: {{ NEXTCLOUD_MCP_ADAPTER_BEARER | to_json }}
ADAPTER_PORT: "{{ container_port }}"
{{ lookup('container_healthcheck', service_name) | indent(4) }}
expose:
- "{{ container_port }}"
{{ lookup('container_networks', own_network_only=True, provider_self_alias=False) | indent(4) }}
{% endif %}
{{ lookup('compose_volumes', extra_volumes=lookup('nextcloud_extra_volumes')) }}
{{ lookup('compose_networks') }}