Skip to content

docker init source ​

Copy-paste-ready Dockerfile, .dockerignore and compose hardening for a Go service, built from the OWASP Docker Security cheat sheet and the team's container image policy.

Why: on 2026-09-12, 21 of the team's 33 runtime images ran as root, none of 39 Dockerfiles had a HEALTHCHECK, two images were built on Ubuntu releases past end of support, one on a personal Docker Hub account's :latest, and two passed bearer tokens through ARG into ENV, where docker inspect shows them. Every one of those is a line in these files.

Contents ​

FileWhat it is
DockerfileMulti-stage build: golang:1.25-trixie to distroless static-debian13:nonroot, both pinned by digest
.dockerignoreKeeps .git, secrets and local config out of the build context
compose.hardening.ymlAn x-hardened block to merge into every compose service

Adopting in a service repo ​

  1. Copy Dockerfile and .dockerignore to the repo root. Set MAIN to the package to build (./cmd/server), or pass --build-arg MAIN=....

  2. Create certs/: put AIOZ Internal-Root-CA there as a .crt if the module pulls private modules from gitlab.internal, or an empty .gitkeep if not.

  3. Build in CI with the job token as a BuildKit secret, never a build argument:

    bash
    docker build --secret id=gitlab_token,env=CI_JOB_TOKEN \
      --build-arg MAIN=./cmd/server \
      --build-arg VERSION="$(git describe --tags --abbrev=0)" \
      --build-arg COMMIT="$CI_COMMIT_SHA" \
      --build-arg BUILD_TIMESTAMP="$(date -u +%s)" \
      -t registry:5000/<service>:<version> .
  4. The HEALTHCHECK runs service healthcheck. Wire aioz-common's stats/debug.HealthCommand into the service's root command, and pin the debug server's address: debug.addr defaults to 127.0.0.1:0, a random port no health check can find. Loopback is enough, because the check runs inside the container:

    go
    root.AddCommand(debug.HealthCommand("http://127.0.0.1:9002/health")) // debug.addr: 127.0.0.1:9002

    Until the service has it, delete the HEALTHCHECK line rather than point it at something that always fails.

  5. In each compose file, paste the x-hardened block and add <<: *hardened to every service. Publish ports on 127.0.0.1, give the stack a named network, and move every password from environment: to secrets:.

  6. Run hadolint Dockerfile locally; the ci-security template runs it on every merge request.

Cheat sheet rule → where it is enforced ​

OWASP Docker Security ruleEnforced by
#1 Do not expose the Docker daemon socketNo /var/run/docker.sock mount in compose.hardening.yml; review compose diffs for one
#2 Set a non-root userUSER 65532:65532 in Dockerfile; user: "65532:65532" in x-hardened
#3 Limit capabilitiescap_drop: [ALL]; add one back only with a comment saying why
#4 Prevent in-container privilege escalationsecurity_opt: [no-new-privileges:true]
#5 Be mindful of inter-container connectivityA named network per stack, never network_mode: host
#5a Firewalls and published portsPorts published on 127.0.0.1; the reverse proxy publishes to the world
#7 Limit resourcespids_limit, deploy.resources.limits, ulimits
#8 Read-only filesystemread_only: true with a size-capped /tmp tmpfs
#9 Scan in CIci-security: trivy (vulnerabilities, secrets, misconfiguration), gitleaks, hadolint
#12 SecretsBuildKit --secret at build time; compose secrets: at runtime; .dockerignore keeps key files out of the context
#13 Supply chainBases pinned by digest; images only from the approved list

Not covered here, because they are host or platform settings rather than image or compose settings: #0 keep the host and Docker updated, #6 seccomp and AppArmor profiles (Docker's default seccomp profile stays on unless someone passes seccomp:unconfined), #10 daemon log level, #11 rootless mode. Image signing and SBOMs (#13) come in a later revision.

Migrating the existing images ​

Found in the 2026-09-12 survey:

RepositoryIssue
hub (4), stream (3), stream-core (2)USER root stated explicitly
pinning, crawler, depin cmd/worker cmd/keytool dev/fleet, go-aioz-media, stream-coreNo USER
every repoNo HEALTHCHECK
go-aioz-media scripts/docker-go-compiler, pinningubuntu:16.04, ubuntu:20.04 (past end of standard support)
stream-core Dockerfile.workerFROM tuanaioz/ffmpeg:latest: personal account, mutable tag
depin dev/fleet/Dockerfile.fleet, cmd/worker/DockerfileFleet bearer tokens through ARG into ENV
parser-service docker-compose.stag.ymlnetwork_mode: host
pinning grafana, hub, aioz-mapPasswords hard-coded in compose environment: