Automate priviledge preparation for Docker volume mountpoints

Satisfy container preconditions with `depends_on` in Docker Compose

Geschrieben von Timo Rieber am 24. März 2024

Yet another container for our deployment

For the new multi-functional search capabilities in our Cloudapps product, we rely on Elasticsearch as a search engine. The production deployment is solely based on Docker containers controlled by Docker Compose. So we were to add a new service to our docker-compose.yml file. Luckily, there is an official Elasticsearch Docker image available, so we just had to add a new service to our configuration.

But wait, there's a catch

Years ago Elasticsearch removed the option to run as root, which is a good thing from a security perspective. But each decision is always a trade-off, here between security and operations convenience, as you can read in the controversial discussion. Before this change one could simply add es.insecure.allow.root to the configuration to run Elasticsearch as root. But now the Elasticsearch container runs as the user elasticsearch with UID 1000 and GID 0 by default.

Basically it means that the deployment needs special preparation. For the Elasticsearch container to be able to write as non-root to its data directories, mounted as Docker volumes, the host directories need correct ownership and priviledges. You could do this manually, but we generally want to automate the deployment as much as possible and rely on our CI/CD pipelines to do the heavy lifting.

Docker Compose depends_on to the rescue

So, how can we prepare the host directories before the Elasticsearch container starts? The answer is simple: we can use the depends_on option in Docker Compose] to control the startup order. This allows us to specify dependencies between services, so that one service starts only after another service has started (or finished in our case). With the following configuration snippet we spin up a throw-away container that prepares the host directories for the volumes before the Elasticsearch container starts:

volumes:
  elasticsearch-data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /srv/data/elasticsearch

services:
  elasticsearch-volume-init:
    image: alpine
    volumes:
      - elasticsearch-data:/tmp/change-ownership
    command: chown --recursive 1000:0 /tmp/change-ownership

  elasticsearch:
    image: elasticsearch:8.12.2
    depends_on:
      elasticsearch-volume-init:
        condition: service_completed_successfully
    environment:
      - bootstrap.memory_lock=true
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - ELASTIC_PASSWORD=changeit
      - xpack.security.enabled=true
    ulimits:
      memlock:
        soft: -1
        hard: -1
    expose:
      - 9200
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
Yaml

First the elasticsearch-volume-init service is a simple container that changes the ownership of the volume mounted to /tmp/change-ownership to the user with UID 1000 and GID 0. Then the elasticsearch service depends on the elasticsearch-volume-init and will only start after the dependency has completed successfully. The condition: service_completed_successfully option ensures that the elasticsearch service will only start if the elasticsearch-volume-init service has exited with a zero exit code.

That's it! Now the Elasticsearch container can write to its data directories and we can enjoy the new search capabilities in our Cloudapps product. At this point I want to thank Pratik Chaudhari for his clever workaround to use depends_on for this purpose.