kgyrtkirk commented on code in PR #18302:
URL: https://github.com/apache/druid/pull/18302#discussion_r2235755695


##########
.github/scripts/distribution_checks_env.sh:
##########
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+export DRUID_DIST_IMAGE_NAME=apache/druid:35.0.0-SNAPSHOT

Review Comment:
   doesn't feel right - you could use the workflow level `env` to declare 
exports
   
   ....or move the full logic into a script and run that...



##########
.github/workflows/ci.yml:
##########
@@ -27,7 +27,7 @@ jobs:
         pattern: [ "C*", "H*,U*,V*", "N*,Q*,S*", "B*,O*,R*", "G*,J*,K*", 
"F*,L*,M*", "A*,D*,I*,X*,Y*,Z*", "E*,P*,T*,W*"]
     uses: ./.github/workflows/worker.yml
     with:
-      script: .github/scripts/run_unit-tests -Dtest=!QTest,'${{ matrix.pattern 
}}' -Dmaven.test.failure.ignore=true
+      script: .github/scripts/run_unit-tests -Dtest=!QTest,!*DockerTest*,'${{ 
matrix.pattern }}' -Dmaven.test.failure.ignore=true

Review Comment:
   I'm quilty of not moving the `QTest` into the `validation` phase ; which 
seem to have given rise to this pattern which could be considered bad practice
   
   this is not really serious...it could be fixed separaetly - but nicely 
outlines your requirement that you just wanted to add 1 more test....



##########
.github/workflows/docker-tests.yml:
##########
@@ -0,0 +1,97 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "Docker Tests using Distribution Image"
+on:
+  workflow_call:
+
+jobs:
+  run-docker-tests:
+    name: Run Docker tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Retrieve Docker container
+        id: docker-restore
+        uses: actions/cache/restore@v4
+        with:
+          key: druid-dist-container.tar.gz-${{ github.sha }}
+          path: |
+            ./druid-dist-container.tar.gz
+      - name: Stop and remove Druid Docker containers
+        run: |
+          echo "Force stopping all Druid containers and pruning"
+          docker ps -aq --filter "ancestor=apache/druid" | xargs -r docker rm 
-f
+          docker system prune -af --volumes
+      - name: Load Docker image
+        run: |
+          docker load --input druid-dist-container.tar.gz
+          docker images
+      - name: Setup Java
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'zulu'
+          java-version: 17
+          cache: 'maven'
+      - name: Run *DockerTest*
+        id: run-it
+        run: |
+          source .github/scripts/distribution_checks_env.sh
+          .github/scripts/run_unit-tests -Dtest=*DockerTest* 
-Ddruid.testing.docker.image=$DRUID_DIST_IMAGE_NAME
+        timeout-minutes: 60
+
+      - name: Collect docker logs on failure
+        if: ${{ failure() && steps.run-it.conclusion == 'failure' }}
+        run: |
+          mkdir docker-logs
+          for c in $(docker ps -a --format="{{.Names}}")
+          do
+            docker logs $c > ./docker-logs/$c.log
+          done
+
+      - name: Tar docker logs
+        if: ${{ failure() && steps.run-it.conclusion == 'failure' }}
+        run: tar cvzf ./docker-logs.tgz ./docker-logs
+
+      - name: Upload docker logs to GitHub
+        if: ${{ failure() && steps.run-it.conclusion == 'failure' }}
+        uses: actions/upload-artifact@v4
+        with:
+          name: failure-docker-logs
+          path: docker-logs.tgz
+
+      - name: Collect service logs on failure
+        if: ${{ failure() && steps.run-it.conclusion == 'failure' }}
+        run: |
+          tar cvzf ./service-logs.tgz /tmp/embedded-tests

Review Comment:
   I wonder why these are under `/tmp` and not under a `target` directory? 



##########
.github/workflows/docker-tests.yml:
##########
@@ -0,0 +1,97 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "Docker Tests using Distribution Image"
+on:
+  workflow_call:
+
+jobs:
+  run-docker-tests:
+    name: Run Docker tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Retrieve Docker container
+        id: docker-restore
+        uses: actions/cache/restore@v4
+        with:
+          key: druid-dist-container.tar.gz-${{ github.sha }}
+          path: |
+            ./druid-dist-container.tar.gz
+      - name: Stop and remove Druid Docker containers
+        run: |
+          echo "Force stopping all Druid containers and pruning"
+          docker ps -aq --filter "ancestor=apache/druid" | xargs -r docker rm 
-f
+          docker system prune -af --volumes
+      - name: Load Docker image
+        run: |
+          docker load --input druid-dist-container.tar.gz
+          docker images
+      - name: Setup Java
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'zulu'
+          java-version: 17
+          cache: 'maven'
+      - name: Run *DockerTest*
+        id: run-it
+        run: |
+          source .github/scripts/distribution_checks_env.sh
+          .github/scripts/run_unit-tests -Dtest=*DockerTest* 
-Ddruid.testing.docker.image=$DRUID_DIST_IMAGE_NAME

Review Comment:
   instead of trying to reuse the `run_unit-tests` script; you could possibly 
make a separate one for this; and run that (you could still invode 
`run_unit-tests` from there if you want) 
   
   you should preferrably also use the `worker.yml` to run it....that way if 
you want to test the workflow you could just try to run that script on your own 
machine!
   



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",
+        containerDirectory + "/log"
+    );
+
+    // Create the log directory upfront to avoid permission issues
+    createLogDirectory(new File(containerDirectory, "log"));
+  }
+
+  @Override
+  protected DruidContainer createContainer()
+  {
+    final List<Integer> exposedPorts = new ArrayList<>(taskPorts);
+    exposedPorts.add(servicePort);
+    log.info(
+        "Starting Druid container[%s] on port[%d] with mounted directory[%s] 
and exposed ports[%s].",
+        name, servicePort, containerDirectory, exposedPorts
+    );
+
+    final DruidContainer container = new DruidContainer(command, imageName)
+        .withExposedPorts(exposedPorts.toArray(new Integer[0]))
+        .withCommonProperties(getCommonProperties())
+        .withServiceProperties(getServerProperties())
+        .withFileSystemBind(
+            segmentDeepStorageDirectory.hostPath,
+            segmentDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )

Review Comment:
   you could make the `MountedDir` a first class citizen and not extract its 
belly...



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",
+        containerDirectory + "/log"
+    );
+
+    // Create the log directory upfront to avoid permission issues
+    createLogDirectory(new File(containerDirectory, "log"));
+  }
+
+  @Override
+  protected DruidContainer createContainer()
+  {
+    final List<Integer> exposedPorts = new ArrayList<>(taskPorts);
+    exposedPorts.add(servicePort);
+    log.info(
+        "Starting Druid container[%s] on port[%d] with mounted directory[%s] 
and exposed ports[%s].",
+        name, servicePort, containerDirectory, exposedPorts
+    );
+
+    final DruidContainer container = new DruidContainer(command, imageName)
+        .withExposedPorts(exposedPorts.toArray(new Integer[0]))
+        .withCommonProperties(getCommonProperties())
+        .withServiceProperties(getServerProperties())
+        .withFileSystemBind(
+            segmentDeepStorageDirectory.hostPath,
+            segmentDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            indexerLogsDeepStorageDirectory.hostPath,
+            indexerLogsDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            serviceLogsDirectory.hostPath,
+            serviceLogsDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withEnv(
+            Map.of(
+                "DRUID_SET_HOST_IP", "0",
+                "DRUID_SET_HOST", "0"
+            )
+        )
+        .waitingFor(Wait.forHttp("/status/health").forPort(servicePort));
+
+    // Bind the ports statically (rather than using a mapped port) to the same
+    // value used in `druid.plaintextPort` to make this node discoverable
+    // by other services (both embedded and dockerized).
+    List<String> portBindings = exposedPorts.stream().map(
+        port -> StringUtils.format("%d:%d", port, port)
+    ).collect(Collectors.toList());
+    container.setPortBindings(portBindings);
+
+    return container;
+  }
+
+  /**
+   * Writes the common properties to a file.
+   *
+   * @return Name of the file.
+   */
+  private Properties getCommonProperties()

Review Comment:
   methodname and apidoc mismatch



##########
.github/workflows/docker-tests.yml:
##########
@@ -0,0 +1,97 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "Docker Tests using Distribution Image"
+on:
+  workflow_call:
+
+jobs:
+  run-docker-tests:
+    name: Run Docker tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Retrieve Docker container
+        id: docker-restore
+        uses: actions/cache/restore@v4
+        with:
+          key: druid-dist-container.tar.gz-${{ github.sha }}
+          path: |
+            ./druid-dist-container.tar.gz
+      - name: Stop and remove Druid Docker containers
+        run: |
+          echo "Force stopping all Druid containers and pruning"
+          docker ps -aq --filter "ancestor=apache/druid" | xargs -r docker rm 
-f
+          docker system prune -af --volumes
+      - name: Load Docker image
+        run: |
+          docker load --input druid-dist-container.tar.gz
+          docker images
+      - name: Setup Java
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'zulu'
+          java-version: 17
+          cache: 'maven'
+      - name: Run *DockerTest*
+        id: run-it
+        run: |
+          source .github/scripts/distribution_checks_env.sh
+          .github/scripts/run_unit-tests -Dtest=*DockerTest* 
-Ddruid.testing.docker.image=$DRUID_DIST_IMAGE_NAME
+        timeout-minutes: 60
+
+      - name: Collect docker logs on failure
+        if: ${{ failure() && steps.run-it.conclusion == 'failure' }}
+        run: |
+          mkdir docker-logs
+          for c in $(docker ps -a --format="{{.Names}}")
+          do
+            docker logs $c > ./docker-logs/$c.log
+          done

Review Comment:
   can you integrate this into the `worker.yml` somehow: put it inside a script 
and configure the artifact collector to save that as well..
   
   



##########
.github/workflows/distribution-checks.yml:
##########
@@ -38,4 +38,23 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Build the Docker image
-        run: DOCKER_BUILDKIT=1 docker build -t apache/druid:tag -f 
distribution/docker/Dockerfile .
+        run: |

Review Comment:
   I feel like what you are doing in this PR is superior to what this workflow 
was ever doing.
   
   I think we could just remove this whole workflow...along with the 
`save/load` of the image and run those dockerized tests right after the image 
is built!



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;

Review Comment:
   its a directory - why it needs to be flattened out to be a `String` ?



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",

Review Comment:
   not matters much - but I feel like `container_` doesn't really gives a hint 
what this is...would be better if it would hint that its being created by 
Druid's embedded test framework....don't have a good suggestion...maybe its not 
that important
   



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",

Review Comment:
   why it needs to be under `tmp` if its inside the container? it could even be 
`/deep-storage` 



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",
+        containerDirectory + "/log"
+    );
+
+    // Create the log directory upfront to avoid permission issues
+    createLogDirectory(new File(containerDirectory, "log"));
+  }
+
+  @Override
+  protected DruidContainer createContainer()
+  {
+    final List<Integer> exposedPorts = new ArrayList<>(taskPorts);
+    exposedPorts.add(servicePort);
+    log.info(
+        "Starting Druid container[%s] on port[%d] with mounted directory[%s] 
and exposed ports[%s].",
+        name, servicePort, containerDirectory, exposedPorts
+    );
+
+    final DruidContainer container = new DruidContainer(command, imageName)
+        .withExposedPorts(exposedPorts.toArray(new Integer[0]))
+        .withCommonProperties(getCommonProperties())
+        .withServiceProperties(getServerProperties())
+        .withFileSystemBind(
+            segmentDeepStorageDirectory.hostPath,
+            segmentDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            indexerLogsDeepStorageDirectory.hostPath,
+            indexerLogsDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            serviceLogsDirectory.hostPath,
+            serviceLogsDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withEnv(
+            Map.of(
+                "DRUID_SET_HOST_IP", "0",
+                "DRUID_SET_HOST", "0"
+            )
+        )
+        .waitingFor(Wait.forHttp("/status/health").forPort(servicePort));
+
+    // Bind the ports statically (rather than using a mapped port) to the same
+    // value used in `druid.plaintextPort` to make this node discoverable
+    // by other services (both embedded and dockerized).
+    List<String> portBindings = exposedPorts.stream().map(
+        port -> StringUtils.format("%d:%d", port, port)
+    ).collect(Collectors.toList());
+    container.setPortBindings(portBindings);

Review Comment:
   there could be an `exposePorts` which accepts the list...so no `map` and 
other stuff is at the place its being used



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",
+        containerDirectory + "/log"
+    );
+
+    // Create the log directory upfront to avoid permission issues
+    createLogDirectory(new File(containerDirectory, "log"));
+  }
+
+  @Override
+  protected DruidContainer createContainer()
+  {
+    final List<Integer> exposedPorts = new ArrayList<>(taskPorts);
+    exposedPorts.add(servicePort);
+    log.info(
+        "Starting Druid container[%s] on port[%d] with mounted directory[%s] 
and exposed ports[%s].",
+        name, servicePort, containerDirectory, exposedPorts
+    );
+
+    final DruidContainer container = new DruidContainer(command, imageName)
+        .withExposedPorts(exposedPorts.toArray(new Integer[0]))
+        .withCommonProperties(getCommonProperties())
+        .withServiceProperties(getServerProperties())
+        .withFileSystemBind(
+            segmentDeepStorageDirectory.hostPath,
+            segmentDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            indexerLogsDeepStorageDirectory.hostPath,
+            indexerLogsDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            serviceLogsDirectory.hostPath,
+            serviceLogsDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withEnv(
+            Map.of(
+                "DRUID_SET_HOST_IP", "0",
+                "DRUID_SET_HOST", "0"
+            )
+        )
+        .waitingFor(Wait.forHttp("/status/health").forPort(servicePort));
+
+    // Bind the ports statically (rather than using a mapped port) to the same
+    // value used in `druid.plaintextPort` to make this node discoverable
+    // by other services (both embedded and dockerized).
+    List<String> portBindings = exposedPorts.stream().map(
+        port -> StringUtils.format("%d:%d", port, port)
+    ).collect(Collectors.toList());
+    container.setPortBindings(portBindings);
+
+    return container;
+  }
+
+  /**
+   * Writes the common properties to a file.
+   *
+   * @return Name of the file.
+   */
+  private Properties getCommonProperties()
+  {
+    final Properties commonProperties = new Properties();
+    commonProperties.putAll(cluster.getCommonProperties());
+    FORBIDDEN_PROPERTIES.forEach(commonProperties::remove);
+
+    commonProperties.setProperty(
+        "druid.storage.storageDirectory",
+        segmentDeepStorageDirectory.containerPath
+    );
+    commonProperties.setProperty(
+        "druid.indexer.logs.directory",
+        indexerLogsDeepStorageDirectory.containerPath
+    );
+
+    log.info(
+        "Writing common properties for Druid container[%s]: [%s]",
+        name, commonProperties
+    );
+
+    return commonProperties;
+  }
+
+  /**
+   * Writes the server properties to a file.
+   *
+   * @return Name of the file.
+   */
+  private Properties getServerProperties()
+  {
+    FORBIDDEN_PROPERTIES.forEach(properties::remove);
+    addProperty("druid.host", EmbeddedDruidCluster.getDefaultHost());
+    addProperty("druid.plaintextPort", String.valueOf(servicePort));
+
+    final Properties serverProperties = new Properties();
+    serverProperties.putAll(properties);
+
+    log.info(
+        "Writing runtime properties for Druid container[%s]: [%s]",
+        name, serverProperties
+    );
+
+    return serverProperties;
+  }
+
+  private static void createLogDirectory(File dir)
+  {
+    try {
+      FileUtils.mkdirp(dir);
+      Files.setPosixFilePermissions(dir.toPath(), 
PosixFilePermissions.fromString("rwxrwxrwx"));
+    }
+    catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public String toString()
+  {
+    return name;
+  }
+
+  private static class MountedDir
+  {
+    final String hostPath;
+    final String containerPath;

Review Comment:
   these seems like paths - throwing away `File` and `Path` will make it harder 
to work with them....



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",
+        containerDirectory + "/log"
+    );
+
+    // Create the log directory upfront to avoid permission issues
+    createLogDirectory(new File(containerDirectory, "log"));
+  }
+
+  @Override
+  protected DruidContainer createContainer()
+  {
+    final List<Integer> exposedPorts = new ArrayList<>(taskPorts);
+    exposedPorts.add(servicePort);
+    log.info(
+        "Starting Druid container[%s] on port[%d] with mounted directory[%s] 
and exposed ports[%s].",
+        name, servicePort, containerDirectory, exposedPorts
+    );
+
+    final DruidContainer container = new DruidContainer(command, imageName)
+        .withExposedPorts(exposedPorts.toArray(new Integer[0]))

Review Comment:
   there could be a method which accepts `List` ?



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainers.java:
##########
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.testing.DruidCommand;
+
+/**
+ * Factory for {@link DruidContainerResource} that can run specific services.
+ *
+ * @see #newOverlord()
+ * @see #newCoordinator()
+ */
+public final class DruidContainers
+{
+  private DruidContainers()
+  {
+    // no instantiation
+  }
+
+  /**
+   * Creates a new {@link DruidContainerResource} to run a Coordinator node.
+   */
+  public static DruidContainerResource newCoordinator()
+  {
+    return new DruidContainerResource(DruidCommand.COORDINATOR)
+        .addProperty("druid.coordinator.startDelay", "PT0.1S")
+        .addProperty("druid.coordinator.period", "PT0.5S")
+        .addProperty("druid.manager.segments.pollDuration", "PT0.1S");
+  }
+
+  /**
+   * Creates a new {@link DruidContainerResource} to run an Overlord node.
+   */
+  public static DruidContainerResource newOverlord()
+  {
+    // Keep a small sync timeout so that Peons and Indexers are not stuck
+    // handling a change request when Overlord has already shutdown
+    return new DruidContainerResource(DruidCommand.OVERLORD)
+        .addProperty("druid.indexer.storage.type", "metadata")
+        .addProperty("druid.indexer.queue.startDelay", "PT0S")
+        .addProperty("druid.indexer.queue.restartDelay", "PT0S")
+        .addProperty("druid.indexer.runner.syncRequestTimeout", "PT1S");
+  }
+
+  /**
+   * Creates a new {@link DruidContainerResource} to run an Indexer node.
+   */
+  public static DruidContainerResource newIndexer()
+  {
+    return new DruidContainerResource(DruidCommand.INDEXER)
+        .addProperty("druid.lookup.enableLookupSyncOnStartup", "false")
+        .addProperty("druid.processing.buffer.sizeBytes", "50MiB")
+        .addProperty("druid.processing.numMergeBuffers", "2")
+        .addProperty("druid.processing.numThreads", "5");
+  }

Review Comment:
   feels like the `DruidCommand`  was over and underused at the same time...
   * it contains some stuff so `new 
DruidContainerResource(DruidCommand.INDEXER)` could create a valid object
   * its underused as a good one will have these 4 properties set...
   
   feels like there should be a `DruidOverlordContainer` / 
`DruidIndexerContainer` and similar....it could still be an innerclass here... 
   



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",

Review Comment:
   not sure about this folder layout....at first time I would never look at 
`/opt/` for logs...



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",
+        containerDirectory + "/log"
+    );
+
+    // Create the log directory upfront to avoid permission issues
+    createLogDirectory(new File(containerDirectory, "log"));
+  }
+
+  @Override
+  protected DruidContainer createContainer()
+  {
+    final List<Integer> exposedPorts = new ArrayList<>(taskPorts);
+    exposedPorts.add(servicePort);
+    log.info(
+        "Starting Druid container[%s] on port[%d] with mounted directory[%s] 
and exposed ports[%s].",
+        name, servicePort, containerDirectory, exposedPorts
+    );
+
+    final DruidContainer container = new DruidContainer(command, imageName)
+        .withExposedPorts(exposedPorts.toArray(new Integer[0]))
+        .withCommonProperties(getCommonProperties())
+        .withServiceProperties(getServerProperties())
+        .withFileSystemBind(
+            segmentDeepStorageDirectory.hostPath,
+            segmentDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            indexerLogsDeepStorageDirectory.hostPath,
+            indexerLogsDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            serviceLogsDirectory.hostPath,
+            serviceLogsDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withEnv(
+            Map.of(
+                "DRUID_SET_HOST_IP", "0",
+                "DRUID_SET_HOST", "0"
+            )
+        )
+        .waitingFor(Wait.forHttp("/status/health").forPort(servicePort));
+
+    // Bind the ports statically (rather than using a mapped port) to the same
+    // value used in `druid.plaintextPort` to make this node discoverable
+    // by other services (both embedded and dockerized).
+    List<String> portBindings = exposedPorts.stream().map(
+        port -> StringUtils.format("%d:%d", port, port)
+    ).collect(Collectors.toList());
+    container.setPortBindings(portBindings);
+
+    return container;
+  }
+
+  /**
+   * Writes the common properties to a file.
+   *
+   * @return Name of the file.
+   */
+  private Properties getCommonProperties()
+  {
+    final Properties commonProperties = new Properties();
+    commonProperties.putAll(cluster.getCommonProperties());
+    FORBIDDEN_PROPERTIES.forEach(commonProperties::remove);
+
+    commonProperties.setProperty(
+        "druid.storage.storageDirectory",
+        segmentDeepStorageDirectory.containerPath
+    );
+    commonProperties.setProperty(
+        "druid.indexer.logs.directory",
+        indexerLogsDeepStorageDirectory.containerPath
+    );
+
+    log.info(
+        "Writing common properties for Druid container[%s]: [%s]",
+        name, commonProperties
+    );
+
+    return commonProperties;
+  }
+
+  /**
+   * Writes the server properties to a file.
+   *
+   * @return Name of the file.
+   */

Review Comment:
   the apidoc doesn't match the function's mission; and its also feels like its 
irrelevant



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainers.java:
##########
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.testing.DruidCommand;
+
+/**
+ * Factory for {@link DruidContainerResource} that can run specific services.
+ *
+ * @see #newOverlord()
+ * @see #newCoordinator()
+ */
+public final class DruidContainers
+{
+  private DruidContainers()
+  {
+    // no instantiation
+  }
+
+  /**
+   * Creates a new {@link DruidContainerResource} to run a Coordinator node.
+   */

Review Comment:
   nit: this apidoc doesn't add any value beyond the function signature:
   ```
   public static DruidContainerResource newCoordinator()
   ```
   
   this is true for a lot of places....I think adding apidoc like that could be 
harmfull as they will become stale after some time.
   



##########
embedded-tests/src/test/java/org/apache/druid/testing/embedded/docker/DruidContainerResource.java:
##########
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.testing.embedded.docker;
+
+import org.apache.druid.java.util.common.FileUtils;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.DruidCommand;
+import org.apache.druid.testing.DruidContainer;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.TestcontainerResource;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * {@link TestcontainerResource} to run Druid services.
+ * Currently, only core extensions can be used out-of-the-box with these 
containers
+ * such as {@code druid-s3-extensions} or {@code postgresql-metadata-storage},
+ * simply by adding them to {@code druid.extensions.loadList}.
+ * <p>
+ * {@link DruidContainers} should be used only for testing backward 
compatiblity
+ * or a Docker-specific feature. For all other testing needs, use plain old
+ * {@code EmbeddedDruidServer} as they are much faster, allow easy debugging 
and
+ * do not require downloading any images.
+ */
+public class DruidContainerResource extends 
TestcontainerResource<DruidContainer>
+{
+  /**
+   * Java system property to specify the name of the Docker test image.
+   */
+  public static final String PROPERTY_TEST_IMAGE = 
"druid.testing.docker.image";
+
+  private static final Logger log = new Logger(DruidContainerResource.class);
+
+  /**
+   * Forbidden server properties that may be used by EmbeddedDruidServers but
+   * interfere with the functioning of DruidContainer-based services.
+   */
+  private static final Set<String> FORBIDDEN_PROPERTIES = Set.of(
+      "druid.extensions.modulesForEmbeddedTests",
+      "druid.emitter"
+  );
+
+  /**
+   * A static incremental ID is used instead of a random number to ensure that
+   * tests are more deterministic and easier to debug.
+   */
+  private static final AtomicInteger SERVER_ID = new AtomicInteger(0);
+
+  private final String name;
+  private final DruidCommand command;
+  private final Map<String, String> properties = new HashMap<>();
+  private final List<Integer> taskPorts = new ArrayList<>();
+
+  private int servicePort;
+  private DockerImageName imageName;
+  private EmbeddedDruidCluster cluster;
+
+  private String containerDirectory;
+
+  private MountedDir indexerLogsDeepStorageDirectory;
+  private MountedDir serviceLogsDirectory;
+  private MountedDir segmentDeepStorageDirectory;
+
+  DruidContainerResource(DruidCommand command)
+  {
+    this.name = StringUtils.format(
+        "container_%s_%d",
+        command.getName(),
+        SERVER_ID.incrementAndGet()
+    );
+    this.command = command;
+
+    Integer[] exposedPorts = command.getExposedPorts();
+    servicePort = exposedPorts[0];
+    taskPorts.addAll(Arrays.asList(exposedPorts).subList(1, 
exposedPorts.length));
+  }
+
+  public DruidContainerResource withPort(int port)
+  {
+    this.servicePort = port;
+    return this;
+  }
+
+  /**
+   * Used to bind the task ports on middle managers.
+   */
+  public DruidContainerResource withTaskPorts(int startPort, int 
workerCapacity)
+  {
+    taskPorts.clear();
+    for (int i = 0; i < workerCapacity; ++i) {
+      taskPorts.add(startPort + i);
+    }
+    addProperty("druid.worker.capacity", String.valueOf(workerCapacity));
+    addProperty("druid.indexer.runner.startPort", String.valueOf(startPort));
+    addProperty("druid.indexer.runner.endPort", String.valueOf(startPort + 
workerCapacity));
+
+    return this;
+  }
+
+  public DruidContainerResource withImage(DockerImageName imageName)
+  {
+    this.imageName = imageName;
+    return this;
+  }
+
+  /**
+   * Uses the Docker test image specified by the system property
+   * {@link #PROPERTY_TEST_IMAGE} for this container.
+   */
+  public DruidContainerResource withTestImage()
+  {
+    String imageName = Objects.requireNonNull(
+        System.getProperty(PROPERTY_TEST_IMAGE),
+        StringUtils.format("System property[%s] is not set", 
PROPERTY_TEST_IMAGE)
+    );
+    return withImage(DockerImageName.parse(imageName));
+  }
+
+  public DruidContainerResource addProperty(String key, String value)
+  {
+    properties.put(key, value);
+    return this;
+  }
+
+  @Override
+  public void beforeStart(EmbeddedDruidCluster cluster)
+  {
+    this.cluster = cluster;
+
+    // Mount directories used by the entire cluster (including embedded 
servers)
+    this.segmentDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/deep-store",
+        
cluster.getTestFolder().getOrCreateFolder("deep-store").getAbsolutePath()
+    );
+    this.indexerLogsDeepStorageDirectory = new MountedDir(
+        "/tmp/druid/indexer-logs",
+        
cluster.getTestFolder().getOrCreateFolder("indexer-logs").getAbsolutePath()
+    );
+
+    // Mount directories used by this container for easier debugging with 
service logs
+    this.containerDirectory = 
cluster.getTestFolder().getOrCreateFolder(name).getAbsolutePath();
+    this.serviceLogsDirectory = new MountedDir(
+        "/opt/druid/log",
+        containerDirectory + "/log"
+    );
+
+    // Create the log directory upfront to avoid permission issues
+    createLogDirectory(new File(containerDirectory, "log"));
+  }
+
+  @Override
+  protected DruidContainer createContainer()
+  {
+    final List<Integer> exposedPorts = new ArrayList<>(taskPorts);
+    exposedPorts.add(servicePort);
+    log.info(
+        "Starting Druid container[%s] on port[%d] with mounted directory[%s] 
and exposed ports[%s].",
+        name, servicePort, containerDirectory, exposedPorts
+    );
+
+    final DruidContainer container = new DruidContainer(command, imageName)
+        .withExposedPorts(exposedPorts.toArray(new Integer[0]))
+        .withCommonProperties(getCommonProperties())
+        .withServiceProperties(getServerProperties())
+        .withFileSystemBind(
+            segmentDeepStorageDirectory.hostPath,
+            segmentDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            indexerLogsDeepStorageDirectory.hostPath,
+            indexerLogsDeepStorageDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withFileSystemBind(
+            serviceLogsDirectory.hostPath,
+            serviceLogsDirectory.containerPath,
+            BindMode.READ_WRITE
+        )
+        .withEnv(
+            Map.of(
+                "DRUID_SET_HOST_IP", "0",
+                "DRUID_SET_HOST", "0"
+            )
+        )
+        .waitingFor(Wait.forHttp("/status/health").forPort(servicePort));
+
+    // Bind the ports statically (rather than using a mapped port) to the same
+    // value used in `druid.plaintextPort` to make this node discoverable
+    // by other services (both embedded and dockerized).
+    List<String> portBindings = exposedPorts.stream().map(
+        port -> StringUtils.format("%d:%d", port, port)
+    ).collect(Collectors.toList());
+    container.setPortBindings(portBindings);
+
+    return container;
+  }
+
+  /**
+   * Writes the common properties to a file.
+   *
+   * @return Name of the file.
+   */
+  private Properties getCommonProperties()
+  {
+    final Properties commonProperties = new Properties();
+    commonProperties.putAll(cluster.getCommonProperties());
+    FORBIDDEN_PROPERTIES.forEach(commonProperties::remove);
+
+    commonProperties.setProperty(
+        "druid.storage.storageDirectory",
+        segmentDeepStorageDirectory.containerPath
+    );
+    commonProperties.setProperty(
+        "druid.indexer.logs.directory",
+        indexerLogsDeepStorageDirectory.containerPath
+    );
+
+    log.info(
+        "Writing common properties for Druid container[%s]: [%s]",
+        name, commonProperties
+    );
+
+    return commonProperties;
+  }
+
+  /**
+   * Writes the server properties to a file.
+   *
+   * @return Name of the file.
+   */
+  private Properties getServerProperties()
+  {
+    FORBIDDEN_PROPERTIES.forEach(properties::remove);
+    addProperty("druid.host", EmbeddedDruidCluster.getDefaultHost());
+    addProperty("druid.plaintextPort", String.valueOf(servicePort));
+
+    final Properties serverProperties = new Properties();
+    serverProperties.putAll(properties);
+
+    log.info(
+        "Writing runtime properties for Druid container[%s]: [%s]",
+        name, serverProperties
+    );
+
+    return serverProperties;
+  }
+
+  private static void createLogDirectory(File dir)
+  {
+    try {
+      FileUtils.mkdirp(dir);
+      Files.setPosixFilePermissions(dir.toPath(), 
PosixFilePermissions.fromString("rwxrwxrwx"));
+    }
+    catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }

Review Comment:
   couldn't the `MountedDir` provide a place for this method? that way if 
someone needs similar in the future it might got reused...



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to