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
| Layer | What it is | Production rule |
|---|---|---|
| Image | A read-only application template | Use trusted sources, explicit tags, and digest pinning when repeatability matters |
| Container | A running instance with its own process tree | Do not store unique state in the writable layer |
| Volume | Docker-managed persistent storage | Use it for databases, then back it up separately |
| Network | Service discovery and isolation boundary | Use service names, not dynamic container IPs |
| Compose file | The single-host deployment contract | Review 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
| Control | Why it matters | Example |
|---|---|---|
| Healthcheck | running does not mean ready | HTTP health endpoint or pg_isready |
| Restart policy | Recover from process exits | unless-stopped for simple single-server services |
| Logging limits | Avoid full disks | json-file max-size and max-file |
| Resource limits | Contain noisy failures | mem_limit, cpus, and Java heap headroom |
| Reverse proxy | Keep app ports private | bind 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.
| Risk | Default answer | Exception handling |
|---|---|---|
| privileged: true | No | Only for a documented low-level need |
| /var/run/docker.sock | No | Isolate and audit any automation that needs daemon access |
| host networking | Avoid | Use only when port and namespace tradeoffs are understood |
| root user | Avoid for app code | Create a dedicated UID and assign only needed write paths |
| latest tag | Avoid in production | Use 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
| Symptom | Commands | Likely cause |
|---|---|---|
| Container exits immediately | docker ps -a; docker logs; docker inspect | Bad command, missing env, permissions, completed foreground process, OOM |
| Port does not respond | docker ps; firewall and cloud security group checks | Wrong host:container mapping or app listens on 127.0.0.1 inside the container |
| App cannot reach database | compose network, service name, healthcheck, credentials | Using localhost instead of db, or changing Postgres env after the volume was initialized |
| Disk fills up | df -h; docker system df -v | Unbounded logs, old images, build cache, unused containers |