Dockerfile & docker-compose Validator
Paste a Dockerfile or a docker-compose.yml to catch syntax errors, broken references and common best-practice mistakes before you build. Every issue has a line number and an explanation. Click an issue to jump to its line.
Dockerfile
Issues
Annotated file
docker-compose.yml
Issues
Annotated file
What this validator checks
The Dockerfile tab uses a hand-written parser that follows the Dockerfile reference: parser directives (# syntax=, # escape=), line continuations, comments inside continued lines, heredocs (RUN <<EOF) and the difference between the JSON exec form and the shell form. On top of the parser runs a linter in the spirit of hadolint and BuildKit's built-in build checks. Where a rule matches a hadolint rule, its code (such as DL3007) is shown so you can look it up or silence it in a real CI pipeline.
The docker-compose tab parses YAML with js-yaml, so syntax errors show the exact line and column. It then checks the structure against a curated list of keys from the Compose Specification: top-level sections, service keys, port and volume syntax, references between services, networks, volumes and secrets, and a few security warnings. Misspelled keys get a "did you mean" suggestion.
Severity levels
- Error: Docker will refuse the file, or the file will not do what it says (an invalid JSON array silently becomes a shell command, for example).
- Warning: the file works, but it is insecure, not reproducible, or makes images bigger or builds slower than they need to be.
- Info: style and modernisation hints. Safe to ignore when you know why you did it.
Dockerfile rules explained
Untagged or :latest base image (DL3006, DL3007) | latest moves. The same Dockerfile can build a different image next week, and a major version bump can break your build without any change on your side. Pin a version such as python:3.12-slim, or a digest (@sha256:…) for fully reproducible builds. |
|---|---|
apt-get install without -y (DL3014) | Builds are non-interactive. Without -y apt waits for a confirmation that never comes and the build aborts. |
No --no-install-recommends (DL3015) | Debian and Ubuntu install "recommended" packages by default, which often adds tens of megabytes you never use. |
| apt lists not removed (DL3009) | apt-get update downloads package indexes into /var/lib/apt/lists. Unless you delete them in the same RUN, they stay in the layer forever. |
apt-get update in its own RUN | Docker caches each layer. A lone RUN apt-get update is cached, so a later apt-get install can use an index that is months old and fail or install outdated packages. Always chain update && install. |
ADD for local files (DL3020) | ADD also downloads URLs and auto-extracts tar archives, which is surprising when you only meant to copy. COPY does just one thing. |
cd in RUN (DL3003) | A cd only lasts for that one RUN. WORKDIR sets the directory for every following instruction and documents it. |
| Shell-form CMD/ENTRYPOINT (DL3025) | The shell form runs your app as a child of /bin/sh -c. The shell is PID 1 and does not forward SIGTERM, so docker stop waits 10 seconds and then kills the process. The JSON form runs your program directly. |
| Multiple CMD/ENTRYPOINT (DL4003, DL4004) | Only the last one in a stage takes effect. The earlier ones are dead code that misleads readers. |
No USER (DL3002) | Containers run as root unless told otherwise. If an attacker breaks out of your app, root inside the container makes every other weakness worse. Create an unprivileged user and switch to it in the final stage. |
| Secrets in ENV/ARG | Values of ENV and ARG are stored in the image metadata and visible to anyone with docker history or docker inspect. Use RUN --mount=type=secret at build time and inject runtime secrets from your orchestrator. |
sudo (DL3004) | Build steps already run as root (or the current USER). sudo adds an extra package, a setuid binary and unpredictable TTY behaviour. Switch USER instead. |
COPY . . before installing dependencies | Any change to any source file invalidates the cache for every following layer, so dependencies are reinstalled on every build. Copy only the manifest (package.json, requirements.txt, go.mod…) first, install, then copy the rest. |
MAINTAINER (DL4000) | Deprecated since Docker 1.13. Use LABEL org.opencontainers.image.authors="…". |
Relative WORKDIR (DL3000) | A relative path is resolved against the previous WORKDIR, which depends on the base image. Absolute paths are unambiguous. |
Compose rules explained
versionis obsolete. Docker Compose v2 ignores it and prints a warning. Delete the line.- Every service needs
imageorbuild(orextends), otherwise Compose has nothing to run. - Ports use
[HOST_IP:][HOST:]CONTAINER[/PROTOCOL], for example"127.0.0.1:8080:80/tcp". Always quote them. Binding to127.0.0.1keeps a database off your public network interface, which matters because Docker's port publishing bypasses most host firewalls. - References must exist.
depends_onmust name real services. Named volumes, networks, secrets and configs used by a service must be declared in the matching top-level section. Circulardepends_onchains can never start. condition: service_healthyonly works if the dependency has a healthcheck. Without it, "started" does not mean "ready to accept connections".privileged: truegives the container almost full access to the host kernel and devices. Prefer specificcap_addentries.container_namemust be unique, so a service that sets it cannot be scaled to more than one replica.- Hard-coded secrets in
environmentend up in Git. Use variable interpolation (${DB_PASSWORD}) with an untracked.envfile, or top-levelsecrets.
Best practices for small, secure images
- Start small.
-slim,-alpineor distroless base images are a fraction of the size of full distributions, and have fewer packages that can carry vulnerabilities. - Order layers from least to most frequently changed. System packages, then dependency manifests and install, then your source code. Cache hits make rebuilds take seconds.
- Clean up in the same layer. Deleting files in a later
RUNdoes not shrink the image, because the earlier layer still contains them. - Use a
.dockerignore. Keep.git,node_modules, local.envfiles and build output out of the build context. - Run as a non-root user and make the root filesystem read-only where you can (
read_only: truein Compose). - Add a
HEALTHCHECKso Docker, Compose and orchestrators know when the app is actually ready.
Multi-stage builds
A multi-stage Dockerfile has several FROM lines. Early stages contain compilers, dev dependencies and build tools. The final stage starts from a small runtime image and copies only the finished artefacts with COPY --from=build. The result has no compiler, no source code and no build secrets, and is often ten times smaller. The "Good example" button loads a three-stage Node.js build that shows the pattern. Name your stages with AS so COPY --from stays readable, and build a specific stage with docker build --target.
Limitations
This is a static linter. It reads the text you paste and never runs Docker. It does not pull images, check that files in COPY exist, run your shell commands, expand ${VARIABLES} or resolve extends, include and YAML merge keys across files. A clean result here is a good sign, not a guarantee. Run docker build --check . and docker compose config for the authoritative answer, and add hadolint to CI for a full rule set.
Is my file uploaded?
No. Parsing and linting happen in your browser with JavaScript. Your Dockerfile and Compose file, including any secrets in them, never leave your machine.