From d00a7d741a27dbdd3c83cdd8488328cf1e43ca54 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 12 May 2026 22:05:50 -0400 Subject: [PATCH 1/2] feat: attach vector container to additional docker networks Adds a new global `vector-networks` property on the logs plugin that takes a comma-separated list of Docker networks. When set, the rendered compose file declares each network plus `bridge` as external and joins them on the vector service, so `docker compose up` reconciles attachments on every `logs:vector-start`. When unset, the existing `network_mode: bridge` template is preserved unchanged. The value is validated against `docker network inspect` at set time, rejects the reserved `bridge` entry, and is surfaced in `dokku logs:report` via `--logs-vector-global-networks`. --- docs/deployment/logs.md | 31 ++++++ plugins/logs/functions.go | 39 +++++-- plugins/logs/logs.go | 1 + plugins/logs/report.go | 6 + plugins/logs/set.go | 34 ++++++ plugins/logs/templates/compose.yml.tmpl | 20 ++++ tests/unit/logs.bats | 141 +++++++++++++++++++++++- 7 files changed, 262 insertions(+), 10 deletions(-) diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index ee58053f9..3bd658f24 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -178,6 +178,37 @@ Setting this to an empty string will reset the version to the version currently dokku logs:set --global vector-image ``` +#### Attaching Vector to additional Docker networks + +By default, the Vector container runs with `network_mode: bridge` and can only reach app containers that are also on the default bridge network. Apps deployed onto a per-app network or a custom network - typically via `dokku network:set initial-network ` - are not reachable from Vector over Docker's internal DNS, so sinks that need to talk to those apps directly (for example, an in-host log search service such as [Logpond](https://github.com/dokku/logpond)) would have to route traffic out through the external proxy. + +The global `vector-networks` property accepts a comma-separated list of Docker networks that Vector should additionally join. + +```shell +dokku logs:set --global vector-networks dokku-logs +``` + +Multiple networks may be specified by separating them with a comma. + +```shell +dokku logs:set --global vector-networks dokku-logs,observability +``` + +Each network must already exist; setting a non-existent network or the reserved `bridge` value will fail. The list can be cleared by setting an empty value, which restores the default `network_mode: bridge` configuration. + +```shell +dokku logs:set --global vector-networks +``` + +Network attachments are reconciled by `docker compose` on every `logs:vector-start`, so after changing the value the Vector container must be cycled. + +```shell +dokku logs:vector-stop +dokku logs:vector-start +``` + +Once attached, an app on `dokku-logs` (for example via `dokku network:set node-js-app initial-network dokku-logs`) is reachable from Vector at `.:` over the shared network without round-tripping through the external proxy. + #### Configuring a log sink Vector uses the concept of log "sinks" to send logs to a given endpoint. Log sinks may be configured globally or on a per-app basis by specifying a `vector-sink` in DSN form with the `logs:set` command. Specifying a sink value will reload any running vector container. diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go index f091d4939..95b522329 100644 --- a/plugins/logs/functions.go +++ b/plugins/logs/functions.go @@ -24,9 +24,10 @@ type vectorSource struct { } type vectorTemplateData struct { - DokkuLibRoot string - DokkuLogsDir string - VectorImage string + DokkuLibRoot string + DokkuLogsDir string + VectorImage string + VectorNetworks []string } const vectorContainerName = "vector-vector-1" @@ -89,9 +90,10 @@ func startVectorContainer(vectorImage string) error { } data := vectorTemplateData{ - DokkuLibRoot: dokkuLibRoot, - DokkuLogsDir: dokkuLogsDir, - VectorImage: vectorImage, + DokkuLibRoot: dokkuLibRoot, + DokkuLogsDir: dokkuLogsDir, + VectorImage: vectorImage, + VectorNetworks: getVectorNetworks(), } if err := tmpl.Execute(tmpFile, data); err != nil { @@ -104,6 +106,24 @@ func startVectorContainer(vectorImage string) error { }) } +func getVectorNetworks() []string { + value := common.PropertyGet("logs", "--global", "vector-networks") + if value == "" { + return nil + } + + networks := []string{} + for _, name := range strings.Split(value, ",") { + name = strings.TrimSpace(name) + if name == "" { + continue + } + networks = append(networks, name) + } + + return networks +} + func getComputedVectorImage() string { return common.PropertyGetDefault("logs", "--global", "vector-image", getDefaultVectorImage()) } @@ -151,9 +171,10 @@ func stopVectorContainer() error { } data := vectorTemplateData{ - DokkuLibRoot: dokkuLibRoot, - DokkuLogsDir: dokkuLogsDir, - VectorImage: getComputedVectorImage(), + DokkuLibRoot: dokkuLibRoot, + DokkuLogsDir: dokkuLogsDir, + VectorImage: getComputedVectorImage(), + VectorNetworks: getVectorNetworks(), } if err := tmpl.Execute(tmpFile, data); err != nil { diff --git a/plugins/logs/logs.go b/plugins/logs/logs.go index 8a21bbc84..6753e0d1f 100644 --- a/plugins/logs/logs.go +++ b/plugins/logs/logs.go @@ -35,6 +35,7 @@ var ( "app-label-alias": true, "max-size": true, "vector-image": true, + "vector-networks": true, "vector-sink": true, } ) diff --git a/plugins/logs/report.go b/plugins/logs/report.go index 714f09470..af7953b26 100644 --- a/plugins/logs/report.go +++ b/plugins/logs/report.go @@ -24,6 +24,7 @@ func ReportSingleApp(appName string, format string, infoFlag string) error { "--logs-global-max-size": reportGlobalMaxSize, "--logs-global-vector-sink": reportGlobalVectorSink, "--logs-vector-global-image": reportVectorGlobalImage, + "--logs-vector-global-networks": reportVectorGlobalNetworks, } } else { flags = map[string]common.ReportFunc{ @@ -35,6 +36,7 @@ func ReportSingleApp(appName string, format string, infoFlag string) error { "--logs-app-label-alias": reportAppLabelAlias, "--logs-max-size": reportMaxSize, "--logs-vector-global-image": reportVectorGlobalImage, + "--logs-vector-global-networks": reportVectorGlobalNetworks, "--logs-vector-sink": reportVectorSink, } } @@ -84,6 +86,10 @@ func reportVectorGlobalImage(appName string) string { return getComputedVectorImage() } +func reportVectorGlobalNetworks(appName string) string { + return common.PropertyGet("logs", "--global", "vector-networks") +} + func reportGlobalVectorSink(appName string) string { value := common.PropertyGet("logs", "--global", "vector-sink") if value == "" { diff --git a/plugins/logs/set.go b/plugins/logs/set.go index 11e4945b8..436a67da8 100644 --- a/plugins/logs/set.go +++ b/plugins/logs/set.go @@ -4,6 +4,9 @@ import ( "errors" "fmt" "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" ) func validateSetValue(appName string, key string, value string) error { @@ -11,6 +14,10 @@ func validateSetValue(appName string, key string, value string) error { return validateMaxSize(appName, value) } + if key == "vector-networks" { + return validateVectorNetworks(appName, value) + } + if key == "vector-sink" { return validateVectorSink(appName, value) } @@ -56,3 +63,30 @@ func validateVectorSink(appName string, value string) error { return nil } + +func validateVectorNetworks(appName string, value string) error { + if value == "" { + return nil + } + + for _, name := range strings.Split(value, ",") { + name = strings.TrimSpace(name) + if name == "" { + return errors.New("Invalid vector-networks value, empty entry in comma-separated list") + } + + if name == "bridge" { + return errors.New("Invalid vector-networks value, \"bridge\" is attached by default and must not be listed") + } + + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: common.DockerBin(), + Args: []string{"network", "inspect", name}, + }) + if err != nil || result.ExitCode != 0 { + return fmt.Errorf("Network %q does not exist", name) + } + } + + return nil +} diff --git a/plugins/logs/templates/compose.yml.tmpl b/plugins/logs/templates/compose.yml.tmpl index 70f033667..1bd7b1bb1 100644 --- a/plugins/logs/templates/compose.yml.tmpl +++ b/plugins/logs/templates/compose.yml.tmpl @@ -13,7 +13,15 @@ services: org.label-schema.schema-version: "1.0" org.label-schema.vendor: dokku + {{- if $.VectorNetworks }} + networks: + bridge: {} + {{- range $name := $.VectorNetworks }} + {{ $name }}: {} + {{- end }} + {{- else }} network_mode: bridge + {{- end }} restart: unless-stopped @@ -21,3 +29,15 @@ services: - "{{ $.DokkuLibRoot }}/data/logs:/etc/vector" - "/var/run/docker.sock:/var/run/docker.sock:ro" - "{{ $.DokkuLogsDir }}/apps:/var/log/dokku/apps" +{{- if $.VectorNetworks }} + +networks: + bridge: + name: bridge + external: true + {{- range $name := $.VectorNetworks }} + {{ $name }}: + name: {{ $name }} + external: true + {{- end }} +{{- end }} diff --git a/tests/unit/logs.bats b/tests/unit/logs.bats index b7fc1c2ae..2958e83af 100644 --- a/tests/unit/logs.bats +++ b/tests/unit/logs.bats @@ -9,6 +9,8 @@ setup() { teardown() { destroy_app + docker network rm test-vector-net-a >/dev/null || true + docker network rm test-vector-net-b >/dev/null || true global_teardown } @@ -54,7 +56,7 @@ teardown() { echo "status: $status" assert_failure assert_output_contains "$TEST_APP logs information" 0 - assert_output_contains "Invalid flag passed, valid flags: --logs-app-label-alias, --logs-computed-app-label-alias, --logs-computed-max-size, --logs-global-app-label-alias, --logs-global-max-size, --logs-global-vector-sink, --logs-max-size, --logs-vector-global-image, --logs-vector-sink" + assert_output_contains "Invalid flag passed, valid flags: --logs-app-label-alias, --logs-computed-app-label-alias, --logs-computed-max-size, --logs-global-app-label-alias, --logs-global-max-size, --logs-global-vector-sink, --logs-max-size, --logs-vector-global-image, --logs-vector-global-networks, --logs-vector-sink" run /bin/bash -c "dokku logs:report $TEST_APP --logs-vector-sink 2>&1" echo "output: $output" @@ -458,6 +460,143 @@ teardown() { assert_output "10m" } +@test "(logs) logs:set --global vector-networks" { + docker network create test-vector-net-a >/dev/null + docker network create test-vector-net-b >/dev/null + + run create_app + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:set $TEST_APP vector-networks test-vector-net-a 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "dokku logs:set --global vector-networks does-not-exist 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "Network \"does-not-exist\" does not exist" + + run /bin/bash -c "dokku logs:set --global vector-networks bridge 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "\"bridge\" is attached by default and must not be listed" + + run /bin/bash -c "dokku logs:set --global vector-networks 'test-vector-net-a,' 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "empty entry in comma-separated list" + + run /bin/bash -c "dokku logs:set --global vector-networks test-vector-net-a 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting vector-networks" + + run /bin/bash -c "dokku logs:report --global --logs-vector-global-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "test-vector-net-a" + + run /bin/bash -c "dokku logs:set --global vector-networks test-vector-net-a,test-vector-net-b 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting vector-networks" + + run /bin/bash -c "dokku logs:report --global --logs-vector-global-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "test-vector-net-a,test-vector-net-b" + + run /bin/bash -c "dokku logs:set --global vector-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Unsetting vector-networks" + + run /bin/bash -c "dokku logs:report --global --logs-vector-global-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists +} + +@test "(logs) logs:vector-start attaches configured networks" { + docker network create test-vector-net-a >/dev/null + docker network create test-vector-net-b >/dev/null + + run /bin/bash -c "dokku logs:set --global vector-networks test-vector-net-a,test-vector-net-b 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-start 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Vector container is running" + + run /bin/bash -c "sudo docker inspect --format='{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}' vector-vector-1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "bridge" + assert_output_contains "test-vector-net-a" + assert_output_contains "test-vector-net-b" + + run /bin/bash -c "dokku logs:vector-stop 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-start 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Vector container is running" + + run /bin/bash -c "sudo docker inspect --format='{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}' vector-vector-1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "test-vector-net-a" + assert_output_contains "test-vector-net-b" + + run /bin/bash -c "dokku logs:set --global vector-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-stop 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-start 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "sudo docker inspect --format='{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}' vector-vector-1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "bridge" + + run /bin/bash -c "dokku logs:vector-stop 2>&1" + echo "output: $output" + echo "status: $status" + assert_success +} + @test "(logs) logs:set app-label-alias" { run create_app echo "output: $output" From a4e6d06fd11fd185650954b9ebe1de73cd7cd42c Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 12 May 2026 23:45:10 -0400 Subject: [PATCH 2/2] fix: reject app-level vector-networks and vector-image, drop bridge from joined nets The Docker daemon refuses any endpoint settings on the default bridge network and `docker compose` unconditionally attaches the service name as an alias on every joined network, so combining bridge with user-defined networks via compose's `networks:` block is impossible. When `vector-networks` is set, the compose template now joins only the configured networks; outbound to external sinks still works through user-defined network NAT. Additionally, `vector-image` and `vector-networks` are both global-only but `common.CommandPropertySet` silently accepts them at app level by merging global-only keys into the valid-property set, so both now reject explicitly in `validateSetValue`. --- docs/deployment/logs.md | 4 +++- plugins/logs/set.go | 18 +++++++++++++++++- plugins/logs/templates/compose.yml.tmpl | 4 ---- tests/unit/logs.bats | 17 +++++++++++++---- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index 3bd658f24..2e6ed0772 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -182,7 +182,7 @@ dokku logs:set --global vector-image By default, the Vector container runs with `network_mode: bridge` and can only reach app containers that are also on the default bridge network. Apps deployed onto a per-app network or a custom network - typically via `dokku network:set initial-network ` - are not reachable from Vector over Docker's internal DNS, so sinks that need to talk to those apps directly (for example, an in-host log search service such as [Logpond](https://github.com/dokku/logpond)) would have to route traffic out through the external proxy. -The global `vector-networks` property accepts a comma-separated list of Docker networks that Vector should additionally join. +The global `vector-networks` property accepts a comma-separated list of Docker networks for Vector to join. ```shell dokku logs:set --global vector-networks dokku-logs @@ -194,6 +194,8 @@ Multiple networks may be specified by separating them with a comma. dokku logs:set --global vector-networks dokku-logs,observability ``` +Setting this property **replaces** the default bridge attachment: the Vector container will be on the configured user-defined networks only, not the default Docker `bridge` network. Outbound traffic continues to work through the user-defined networks' NAT, so external sinks such as Datadog or hosted HTTP endpoints remain reachable. + Each network must already exist; setting a non-existent network or the reserved `bridge` value will fail. The list can be cleared by setting an empty value, which restores the default `network_mode: bridge` configuration. ```shell diff --git a/plugins/logs/set.go b/plugins/logs/set.go index 436a67da8..f0b5c0a43 100644 --- a/plugins/logs/set.go +++ b/plugins/logs/set.go @@ -14,6 +14,10 @@ func validateSetValue(appName string, key string, value string) error { return validateMaxSize(appName, value) } + if key == "vector-image" { + return validateVectorImage(appName, value) + } + if key == "vector-networks" { return validateVectorNetworks(appName, value) } @@ -64,7 +68,19 @@ func validateVectorSink(appName string, value string) error { return nil } +func validateVectorImage(appName string, value string) error { + if appName != "--global" { + return errors.New("vector-image may only be set globally with --global") + } + + return nil +} + func validateVectorNetworks(appName string, value string) error { + if appName != "--global" { + return errors.New("vector-networks may only be set globally with --global") + } + if value == "" { return nil } @@ -76,7 +92,7 @@ func validateVectorNetworks(appName string, value string) error { } if name == "bridge" { - return errors.New("Invalid vector-networks value, \"bridge\" is attached by default and must not be listed") + return errors.New("Invalid vector-networks value, \"bridge\" is not a valid entry for vector-networks") } result, err := common.CallExecCommand(common.ExecCommandInput{ diff --git a/plugins/logs/templates/compose.yml.tmpl b/plugins/logs/templates/compose.yml.tmpl index 1bd7b1bb1..9f27e1772 100644 --- a/plugins/logs/templates/compose.yml.tmpl +++ b/plugins/logs/templates/compose.yml.tmpl @@ -15,7 +15,6 @@ services: {{- if $.VectorNetworks }} networks: - bridge: {} {{- range $name := $.VectorNetworks }} {{ $name }}: {} {{- end }} @@ -32,9 +31,6 @@ services: {{- if $.VectorNetworks }} networks: - bridge: - name: bridge - external: true {{- range $name := $.VectorNetworks }} {{ $name }}: name: {{ $name }} diff --git a/tests/unit/logs.bats b/tests/unit/logs.bats index 2958e83af..c0802531e 100644 --- a/tests/unit/logs.bats +++ b/tests/unit/logs.bats @@ -9,6 +9,7 @@ setup() { teardown() { destroy_app + dokku logs:set --global vector-networks >/dev/null 2>/dev/null || true docker network rm test-vector-net-a >/dev/null || true docker network rm test-vector-net-b >/dev/null || true global_teardown @@ -101,13 +102,19 @@ teardown() { echo "output: $output" echo "status: $status" assert_failure - assert_output_contains "Invalid property specified, valid properties include: app-label-alias, max-size, vector-image, vector-sink" + assert_output_contains "Invalid property specified, valid properties include: app-label-alias, max-size, vector-image, vector-networks, vector-sink" run /bin/bash -c "dokku logs:set $TEST_APP invalid value" 2>&1 echo "output: $output" echo "status: $status" assert_failure - assert_output_contains "Invalid property specified, valid properties include: app-label-alias, max-size, vector-image, vector-sink" + assert_output_contains "Invalid property specified, valid properties include: app-label-alias, max-size, vector-image, vector-networks, vector-sink" + + run /bin/bash -c "dokku logs:set $TEST_APP vector-image timberio/vector:latest-debian 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "vector-image may only be set globally with --global" } @test "(logs) logs:set app" { @@ -473,6 +480,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_failure + assert_output_contains "vector-networks may only be set globally with --global" run /bin/bash -c "dokku logs:set --global vector-networks does-not-exist 2>&1" echo "output: $output" @@ -484,7 +492,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_failure - assert_output_contains "\"bridge\" is attached by default and must not be listed" + assert_output_contains "\"bridge\" is not a valid entry for vector-networks" run /bin/bash -c "dokku logs:set --global vector-networks 'test-vector-net-a,' 2>&1" echo "output: $output" @@ -548,9 +556,9 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - assert_output_contains "bridge" assert_output_contains "test-vector-net-a" assert_output_contains "test-vector-net-b" + assert_output_contains "bridge" 0 run /bin/bash -c "dokku logs:vector-stop 2>&1" echo "output: $output" @@ -569,6 +577,7 @@ teardown() { assert_success assert_output_contains "test-vector-net-a" assert_output_contains "test-vector-net-b" + assert_output_contains "bridge" 0 run /bin/bash -c "dokku logs:set --global vector-networks 2>&1" echo "output: $output"