N Noer

Docker security and supply-chain boundaries: rootless mode, non-root images, ports, sockets, and secrets

A security-focused Docker guide covering rootless mode, non-root containers, privileged mode, docker.sock, port exposure, image pinning, and secret handling.

Docker is useful in production only when it becomes an operating contract, not when it is treated as a magic packaging command. For security teams, the question is not whether Docker is safe in the abstract; the question is which host powers were delegated to which container and which image supplied the code.. The practical boundary is simple: containers may be destroyed and recreated, images must be traceable, data must live outside the writable container layer, and every deployment must have a rollback path.

This guide follows the project date 2026-07-22 and links to Docker documentation for the facts that change over time. It avoids made-up version promises and focuses on durable operating rules.

The production mental model

LayerWhat it isProduction rule
ImageA read-only application templateUse trusted sources, explicit tags, and digest pinning when repeatability matters
ContainerA running instance with its own process treeDo not store unique state in the writable layer
VolumeDocker-managed persistent storageUse it for databases, then back it up separately
NetworkService discovery and isolation boundaryUse service names, not dynamic container IPs
Compose fileThe single-host deployment contractReview the rendered config before running it

Install and verify before deploying

On Linux servers, install Docker Engine from the official Docker repository for the distribution, then verify the CLI, Compose plugin, daemon access, and a test container. Adding a user to the docker group is convenient, but access to the Docker daemon is effectively host-level power. Shared servers should treat that permission as privileged access.

docker version
docker compose version
sudo docker run --rm hello-world

Container networking is not localhost

Inside a container, localhost means that same container. A Java service that talks to PostgreSQL in another container should use jdbc:postgresql://db:5432/app, where db is the Compose service name. Only publish ports that external clients actually need. Internal databases should stay on the private Docker network.

docker network create app-network
docker run -d --name db --network app-network postgres:17

Build images that are easy to rebuild

FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /workspace
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B clean package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app
RUN apt-get update  && apt-get install -y --no-install-recommends curl  && rm -rf /var/lib/apt/lists/*  && useradd --system --uid 10001 appuser
COPY --from=builder /workspace/target/*.jar app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

The multi-stage build keeps Maven and source files out of the runtime image. The JSON-form ENTRYPOINT lets stop signals reach the Java process. The dedicated app user avoids running application code as root by default.

Compose contract for a Java service and PostgreSQL

name: demo-app
services:
  app:
    image: registry.example.com/demo-app:2026-07-22
    restart: unless-stopped
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/app
      SPRING_DATASOURCE_USERNAME: app
      SPRING_DATASOURCE_PASSWORD_FILE: /run/secrets/db_password
      JAVA_TOOL_OPTIONS: -XX:MaxRAMPercentage=70
    secrets:
      - db_password
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 40s
    mem_limit: 1g
    cpus: 1.5
    networks: [backend]
  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks: [backend]
secrets:
  db_password:
    file: ./secrets/db_password.txt
volumes:
  postgres-data:
networks:
  backend:

The short form of depends_on only expresses creation order. If startup ordering matters, combine a database healthcheck with condition: service_healthy, and still keep retry logic in the application. Runtime dependencies can fail after startup too.

Operational controls

ControlWhy it mattersExample
Healthcheckrunning does not mean readyHTTP health endpoint or pg_isready
Restart policyRecover from process exitsunless-stopped for simple single-server services
Logging limitsAvoid full disksjson-file max-size and max-file
Resource limitsContain noisy failuresmem_limit, cpus, and Java heap headroom
Reverse proxyKeep app ports privatebind app to 127.0.0.1 or proxy over a Docker network
services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"
    mem_limit: 1g
    cpus: 1.5

Backup, restore, release, rollback

docker compose exec -T db pg_dump -U app -d app -Fc > app-$(date +%F).dump
docker compose exec -T db pg_restore -U app -d app --clean --if-exists < app-2026-07-22.dump

docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail 200 app

A volume is persistence, not a backup. Keep multiple historical backups, store at least one copy away from the host, encrypt sensitive dumps, and rehearse restoration. A release is not complete until health, logs, and rollback instructions have been checked.

Security boundaries that should be explicit

Do not run arbitrary Compose files from the internet. A Compose file can mount host directories, publish ports, grant privileges, and attach the Docker socket. Treat it as executable infrastructure.

RiskDefault answerException handling
privileged: trueNoOnly for a documented low-level need
/var/run/docker.sockNoIsolate and audit any automation that needs daemon access
host networkingAvoidUse only when port and namespace tradeoffs are understood
root userAvoid for app codeCreate a dedicated UID and assign only needed write paths
latest tagAvoid in productionUse explicit tags; pin digests when repeatability is required
services:
  app:
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL

Rootless mode can reduce the impact of daemon and container breakouts in appropriate environments, but it is not a reason to ignore patching, least privilege, network exposure, or secret handling.

Troubleshooting map

SymptomCommandsLikely cause
Container exits immediatelydocker ps -a; docker logs; docker inspectBad command, missing env, permissions, completed foreground process, OOM
Port does not responddocker ps; firewall and cloud security group checksWrong host:container mapping or app listens on 127.0.0.1 inside the container
App cannot reach databasecompose network, service name, healthcheck, credentialsUsing localhost instead of db, or changing Postgres env after the volume was initialized
Disk fills updf -h; docker system df -vUnbounded logs, old images, build cache, unused containers

Official Docker references used