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
| File | What it is |
|---|---|
Dockerfile | Multi-stage build: golang:1.25-trixie to distroless static-debian13:nonroot, both pinned by digest |
.dockerignore | Keeps .git, secrets and local config out of the build context |
compose.hardening.yml | An x-hardened block to merge into every compose service |
Adopting in a service repo
Copy
Dockerfileand.dockerignoreto the repo root. SetMAINto the package to build (./cmd/server), or pass--build-arg MAIN=....Create
certs/: put AIOZ Internal-Root-CA there as a.crtif the module pulls private modules fromgitlab.internal, or an empty.gitkeepif not.Build in CI with the job token as a BuildKit secret, never a build argument:
bashdocker 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> .The HEALTHCHECK runs
service healthcheck. Wire aioz-common'sstats/debug.HealthCommandinto the service's root command, and pin the debug server's address:debug.addrdefaults to127.0.0.1:0, a random port no health check can find. Loopback is enough, because the check runs inside the container:goroot.AddCommand(debug.HealthCommand("http://127.0.0.1:9002/health")) // debug.addr: 127.0.0.1:9002Until the service has it, delete the
HEALTHCHECKline rather than point it at something that always fails.In each compose file, paste the
x-hardenedblock and add<<: *hardenedto every service. Publish ports on127.0.0.1, give the stack a named network, and move every password fromenvironment:tosecrets:.Run
hadolint Dockerfilelocally; the ci-security template runs it on every merge request.
Cheat sheet rule → where it is enforced
| OWASP Docker Security rule | Enforced by |
|---|---|
| #1 Do not expose the Docker daemon socket | No /var/run/docker.sock mount in compose.hardening.yml; review compose diffs for one |
| #2 Set a non-root user | USER 65532:65532 in Dockerfile; user: "65532:65532" in x-hardened |
| #3 Limit capabilities | cap_drop: [ALL]; add one back only with a comment saying why |
| #4 Prevent in-container privilege escalation | security_opt: [no-new-privileges:true] |
| #5 Be mindful of inter-container connectivity | A named network per stack, never network_mode: host |
| #5a Firewalls and published ports | Ports published on 127.0.0.1; the reverse proxy publishes to the world |
| #7 Limit resources | pids_limit, deploy.resources.limits, ulimits |
| #8 Read-only filesystem | read_only: true with a size-capped /tmp tmpfs |
| #9 Scan in CI | ci-security: trivy (vulnerabilities, secrets, misconfiguration), gitleaks, hadolint |
| #12 Secrets | BuildKit --secret at build time; compose secrets: at runtime; .dockerignore keeps key files out of the context |
| #13 Supply chain | Bases 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:
| Repository | Issue |
|---|---|
| hub (4), stream (3), stream-core (2) | USER root stated explicitly |
pinning, crawler, depin cmd/worker cmd/keytool dev/fleet, go-aioz-media, stream-core | No USER |
| every repo | No HEALTHCHECK |
go-aioz-media scripts/docker-go-compiler, pinning | ubuntu:16.04, ubuntu:20.04 (past end of standard support) |
stream-core Dockerfile.worker | FROM tuanaioz/ffmpeg:latest: personal account, mutable tag |
depin dev/fleet/Dockerfile.fleet, cmd/worker/Dockerfile | Fleet bearer tokens through ARG into ENV |
parser-service docker-compose.stag.yml | network_mode: host |
| pinning grafana, hub, aioz-map | Passwords hard-coded in compose environment: |