About Expertise Work Projects
Hosted Monitoring & Dashboards Self-Hosted Observability Stack Bulk Document Data Extraction Email Deliverability Diagnosis & Repair SEO Migration Recovery AWS Security Review VPS Hardening & ModSecurity Cloud Architecture & Resilience Review SSL & Server Configuration Container Security Review
Free Tools
Website Health Check Email Domain Health Check DNS Health Check SSL Certificate Checker Redirect Chain Checker Robots.txt Checker XML Sitemap Validator Docker Compose Checker WordPress Security Check AWS IAM / S3 Policy Checker Domain Registration Lookup Uptime Monitoring Trial Downtime Cost Calculator AWS Cost Estimator Cloud Architecture Self-Assessment DevOps Engagement Builder Self-Managed VPS vs Managed AWS
Blog Certifications Hire Me

Docker published the port. Your firewall never saw it.

Your Compose file publishes a database port, and your firewall denies everything you did not allow. On a ufw host those two facts do not combine the way you think, and Docker documents why. So I connect to every port your project publishes, from outside your network, and tell you which ones answered.

You probably got here from my own free checker. It was right.

So this page opens where that report ends, rather than reading your Compose file back to you.

The Docker Compose production checker on this site is free and needs no account and no email address. It finds all seven of the faults on this page, names the line, gives you the line number, and lists what passed as well as what failed. None of that is discovered here. It is already yours.

What the free checker already gives you
The verdict, the offending line, its line number, the evidence behind it and the checks that passed. Nothing on this page unlocks any of it, and nothing here is held back to make you pay for it.
And the obvious next thing is free too
Paste that file into a language model and ask for a hardened version. You will get one, in seconds, and most of it will be correct. I am not going to sell you a document you can generate while you read this sentence.

So the honest part first. If your file came back clean, or the only findings were the easy ones and you can already answer the question below, you do not need this page. Fix the lines the checker named and get on with your week.

The question is the one no file answers: that published port — does it actually answer, right now, to somebody who is not you?

Every free tool here reads a file. The fault is not in the file.

A Compose file records what you asked Docker to publish. Whether that port answers depends on the bind address, the host firewall, the packet filtering order and anything in front of the machine — and none of those live in the file.

This is where most people are confident, and it is where Docker itself says not to be. If you run ufw, the firewall you configured is not doing what you think for published container ports. That is not my claim. It is on Docker’s own packet filtering and firewalls page:

  • docs.docker.com — Docker and ufw Docker and ufw use firewall rules in ways that make them incompatible with each other.
  • docs.docker.com — Docker and ufw When you publish a container’s ports using Docker, traffic to and from that container gets diverted before it goes through the ufw firewall settings.
  • docs.docker.com — Docker and ufw Packets are routed before the firewall rules can be applied, effectively ignoring your firewall configuration.

Read that narrowly, because it is narrow. It is about ufw, and it is about the order packets are filtered in. Docker is not insecure by default, and I am not going to tell you it is. On a firewalld host the same page describes something else entirely — Docker creates a zone called docker with target ACCEPT and a forwarding policy to go with it — and it claims no bypass there. So firewalld gets checked rather than assumed, and if you sit behind a cloud security group the whole question may already be answered. Finding that out is a short engagement and a good outcome.

If you have had a server hardened — by me on the VPS hardening page or by anyone else — this is exactly the gap that survives it. That work configures the firewall. This work finds where Docker went around it.

The second half: a credential that has already left

The other thing a file cannot tell you is where a leaked credential went. If a password was passed to a build as an ARG, editing the Compose file changes nothing, because the value is no longer only in the file. Docker says so in the Dockerfile reference:

  • docs.docker.com — Dockerfile reference, ARG It isn’t recommended to use build arguments for passing secrets such as user credentials, API tokens, etc. Build arguments are visible in the docker history command and in max mode provenance attestations

Be precise about the mechanism, because the same page is precise about it. An ARG is not embedded in the image the way an ENV is — Docker says that too. It is in the image’s history, and the history travels with the image. Every registry copy, every pull, every build cache. So the question is never “is it still in the file”. It is: who has pulled that image since, and what does the credential still open?

What the free competition does, and does not, cover

Use all of these. They are good, they cost nothing, and none of them connects to your ports from outside.

The Compose production checker — mine, free
All seven faults, from a paste, with the line and the line number. Reads the file.
KICS — free, open source
Explicitly supports Compose — KICS supports scanning DockerCompose files with .yaml extension — as one of 22 platforms. Reads the file.
Trivy — free, open source
Excellent, and worth running. Its supported configuration list is Kubernetes, Docker, Terraform, Terraform Plan, CloudFormation, Azure ARM Template, Helm, YAML and JSON — no Docker Compose entry. It will still read the file as generic YAML; what it does not have is Compose-aware checks. Reads the file.
Docker Bench for Security — Docker’s own, Apache-2.0
Audits the host, the daemon, the running containers and the images against CIS Docker Benchmark v1.6.0. It reads no Compose file at all, and it runs on the host rather than from outside it.
Docker Scout — free tier, then paid
Image vulnerabilities and SBOMs, which this page does not do and does not try to. Docker Personal includes it for one Scout-enabled repository; the paid plans are billed per user per month. Reads the image.

Every one of them reads a file, or reads an image. None of them opens a connection to your published port from an address that is not yours — and until somebody does, whether that port answers is a belief rather than a fact.

Two tables. Both of them are records of something that happened.

This is what the review produces. Every row below is an example of the shape rather than anybody’s real stack — but the columns are the real columns, and the fifth one cannot be filled in by reading anything.

Published ports, tested from outside

Each port a Compose project publishes, what the firewall was believed to do, and what answered when a connection was made from outside the network.
Service Published Bind address What the firewall claims What answered from outside Evidence
postgres 5432 0.0.0.0 ufw — denied by the default policy Answered. Identified itself as PostgreSQL Observed
redis 6379 0.0.0.0 ufw — denied by the default policy Answered. Identified itself as Redis Observed
adminer 8080 0.0.0.0 ufw — denied by the default policy Answered. A database login page Observed
web 443 0.0.0.0 ufw — allowed, deliberately Answered. This one is meant to Observed
mysql — second stack 3306 127.0.0.1 not reached — bound to loopback Nothing. Connection refused Observed
rabbitmq — management 15672 0.0.0.0 a cloud security group, not the host firewall Nothing. Dropped before the host Observed
grafana 3000 0.0.0.0 ufw — denied by the default policy Not tested. This host was not named in the written permission Not tested

Three rows there say the same thing: the firewall says denied, and the port answered anyway. That is the product. It is also the row that does not exist until somebody connects from an address that is not yours, which is why no file-reading tool returns it and why the report is worth what it costs.

Note the other four, because they matter as much. One port answered and is meant to. Two were genuinely closed — one bound to 127.0.0.1, one covered by a cloud security group that was doing its job. And one was not tested at all, because that host was not named in the written permission. The report says so in the table rather than leaving you to assume it was covered.

Credentials, and how far each one has already travelled

Each credential found in the project, where it lives, who can already read it, where copies exist and whether it has been rotated.
Credential Where it lives Who can already read it Where copies exist Rotated
Database password environment: in the Compose file, line 34 Anyone with the file — the repository, CI, every clone Git history since the first commit, a CI variable, two developer laptops Rotated. Four services and one cron job updated together
Payment provider API token command: on the service line, line 71 Anyone who can run ps on the host, and every container using pid: host The process list, shell history, the deploy script Rotated at the provider, and moved out of the command line
Registry password A build ARG in the application image Anyone who can pull the image. It is in docker history Every registry copy, every pull, the CI build cache Rotated, and the image rebuilt without the ARG. The old value is still readable in every copy nobody deleted
Shared API key environment:, and compiled into a released mobile client Anyone with the file, and anyone who unpacks the app The repository, the app stores, every installed handset Not yet. It needs a client release first, which is its own piece of work

The last column is the state after the repair, which is why one row says not yet. A key compiled into a released mobile app cannot be rotated on the server alone, and a table that pretended otherwise would be selling you a tidier morning than you are going to get.

This is not a penetration test, and I want that in writing.

One connection per port you named. Enough to record that something answered and what it said it was — and then it stops.

What is tried:

  • Only the ports your own Compose file publishes. No scanning for others, no enumeration, no discovery sweep
  • Only against hosts you have named in writing
  • One connection per named port, recorded as answered or not answered, with whatever the service volunteered about itself

What is never tried:

  • No credentials. Not default ones, not leaked ones, not the ones found in your own file
  • No exploitation, and no container escape demonstration. That is a different kind of engagement and I do not sell it
  • Nothing is written, changed or deleted anywhere

So “reachable” on the report means it answered. It never means it was entered, and the report will not imply otherwise. If you need someone to prove exploitability, you want a penetration test from a firm that does them, and I will say so rather than take the work.

Seven things go wrong, and the first three are the same problem.

Host access first, because a container that reaches the host makes everything below it academic. The finding ids are the ones the free checker prints, so you can match a row on your report to a paragraph here.

compose.docker_socket A container can reach the host’s Docker daemon
The Docker socket mounted into a container is root on the host today, by design, with no escape required and no permission model in front of it. Anything that can talk to it can start a second container with the whole filesystem mounted. It is separated from everything below because it is not a weakened boundary — it is no boundary at all.
compose.privileged, compose.network_host, compose.pid_host, compose.ipc_host, compose.cap_dangerous, compose.security_unconfined, compose.userns_host A container shares the host’s isolation boundaries
Different settings, one outcome: the walls come down one at a time. privileged: true removes all of them at once. pid: host makes every process on the machine readable from inside, environment variables included. network_mode: host matters twice over here, because a container on the host network opens ports that never appear under ports: at all — so the file stops describing what is reachable, which is this page’s whole argument in one line. userns_mode: host gives back the remapping that makes container root harmless outside.
compose.host_path_mounted, compose.device_dangerous A container has a path or a device into the host
A bind mount of a sensitive host directory reaches the host without needing privileged or a capability to do it. Writable, it ends where the socket ends, by a slower route. Read-only is better and is not safe, because those directories carry service credentials and every account on the machine. A raw disk device is the host filesystem with the host’s permissions taken off.
compose.db_port_published, compose.no_internal_network A backend service is published where it should not be
A database or a cache with a ports: entry is reachable from outside the machine, not just from the application in front of it. This is the fault the drill exists for, because the file says the port is published and stops there. Whether it answers depends on the firewall, the bind address, the cloud security group and the packet filtering order — and that combination is not in any file.
compose.admin_ui_published An admin interface is published to every interface
Adminer, phpMyAdmin, a queue console, a metrics dashboard. They get published to 0.0.0.0 during setup and stay there, and unlike a database they are designed to be used by a human with a browser. Anyone who finds one needs no tooling at all.
compose.secret_in_env, compose.secret_in_command Credentials readable in the file, or in the process list
A literal password in environment: is readable by everyone with the repository, which is more people than the team. On the command: line it is worse: it is in the process list, so any container sharing the host process namespace can read it without touching the file at all.
compose.secret_in_build_arg A credential that has already travelled
This one is separated from the row above because the blast radius differs in kind. A credential passed as a build ARG is visible in docker history, so every copy of that image carries it — in the registry, on every machine that pulled it, in the build cache. Editing the Compose file does nothing about any of them. Only rotation does, and rotation is work rather than advice.

Faults four and five are the ones the drill settles. The rest are read off the file, ranked by what is actually reachable rather than by severity in the abstract — because a privileged container on a machine nothing can reach is a worse configuration and a smaller emergency than a database that answered on the first try.

Four ways in

Everyone starts with the drill, because until the ports have been tested nobody knows how much of the file actually matters — including me.

Docker Security Review

Everyone starts here. It also scopes everything else.

  • Every port your project publishes, connected to from outside your network and recorded as answered or not answered
  • The bind address and the firewall position for each one, so a port that answered has an explanation rather than just a verdict
  • The credential blast-radius map — every credential in the project, who can already read it, and where copies exist
  • The findings ranked by what is actually reachable, rather than by severity in the abstract
  • The free checker’s results triaged, not re-discovered, and what is already correct as well as what is not
  • Nothing is changed. One Compose project, one host, one registry, up to 20 services and 20 published ports
Recommended

Review + Repair

Not a starting point. Follows the drill.

The tier where the ports that answered stop answering.

  • What answered gets closed — backends moved behind an internal-only network, bindings corrected to the interface they should have been on, admin interfaces taken off every interface
  • Leaked credentials rotated, with the dependents found first so nothing breaks at three in the afternoon
  • The drill re-run from outside afterwards, proving the fix holds rather than asserting that it does
  • Everything staged and reversible, with a named rollback for each change

Review fee credited — you pay the difference, not both.

Hardened Baseline

The above, plus taking the host access back.

  • Everything in the repair
  • The socket mount replaced with whatever its dependent actually needs — a scoped proxy, a narrower credential, or a different mechanism entirely
  • Capabilities dropped to what the process uses, host namespaces given up, seccomp and AppArmor put back
  • Non-root users where the images allow it, and named where they do not
  • Staged and reversible, one service at a time, because these are the changes most likely to break something

Review fee credited — you pay the difference, not both.

Deploy Re-test

Because a one-line change can republish a port and nothing will say so.

  • The published-port surface re-tested from outside after deploys
  • The watch is for a port that should be closed becoming reachable — the drill in its recurring form, on the same named-hosts permission
  • New services and new published ports added to the list as they appear
  • A one-page record each month you can hand to anyone who asks
  • Monthly in advance, cancel anytime, never auto-renewing

Five things about the packages, before you ask

  • The cap is one Compose project on one host, with one registry — 20 services and 20 published ports. Services rather than files, on purpose: a twelve-line file publishing a database is a worse morning than a two-hundred-line file publishing nothing, and one project split across three files is still one project. Ports are capped separately because each one is a separate test and a separate conversation about what should sit in front of it.
  • The recommended tier is the repair, and that is deliberate. The drill tells you the database answered. The repair means it stops. Only one of those changes your position — but you cannot start there, because until the ports are tested nobody knows which findings are urgent and which are paperwork.
  • The second drill in the repair tier is included, not quoted. Charging extra to prove my own fix worked would be the exact failure this page is written against.
  • The review fee is credited for 30 days. Come back inside a month and you pay the difference. After that it lapses, and not as a sales tactic: deploys happen, and a drill from last quarter would have to be run again anyway.
  • What moves the size of the job is not the service count. It is whether the socket mount has a dependent — Traefik doing container discovery, a CI runner, a backup script — because removing it means replacing what needed it, and that is a build rather than an edit. After that: whether you build your own images, how many ports are published, whether the credentials can actually be rotated, and whether there is an outside vantage point and a window.

Deploy Re-test is not uptime monitoring, and it is not alerting. Uptime monitoring watches that something you want up is up, and it is free in places including the uptime trial on this site. If you want dashboards and somebody watching the whole stack, that is hosted monitoring and it is a different service. This watches the opposite thing: that a port which is supposed to be closed has not quietly become reachable again.

There are no figures on this page, and that is deliberate rather than coy. A fixed number would be too high for four services publishing one port and too low for a stack with a socket mount holding up three other things. Tell me the shape of it in the form — how many services, what is published, and whether anything mounts the Docker socket — and you get a figure back before anything is committed to.

What you have to supply

The first item is not negotiable and nothing starts without it.

Written permission naming every host and every port I may connect to from outside. Not a verbal yes, not “go ahead” in a chat window — in writing, from somebody entitled to give it, listing the hosts and the ports. The boundary above is part of that permission: named ports only, one connection each, no credentials tried, nothing written. If a host is not on the list, it is not tested, and the report says so in the table.

And then the ordinary things

  • The Compose file or files, and which host actually runs them. A file in a repository is not necessarily what is running, and the difference between the two is occasionally the whole finding.
  • Which host firewall is in use — ufw, firewalld, nftables directly, or a cloud security group. The answer changes the question, and “not sure” is a legitimate answer that I will find out.
  • A window, or written permission to test live. The test is light, but a database answering a connection it has never seen before can log it or alert somebody, and I would rather that be expected.
  • Registry access, if any image is built in-house, so build arguments and layer history can be checked.
  • What depends on the socket mount, if there is one. “We don’t know” is a valid answer and becomes part of the finding rather than a problem.
  • Somebody who can actually rotate a credential, or the name of who can. A map of leaked credentials nobody is able to change is half a deliverable.
  • Somebody reachable while the test runs. If anything is disturbed despite the precautions, the work stops and you are told inside the hour.

What this cannot promise

Every one of these is on the page rather than in an email after you have paid.

  • A reachability test proves what answered, from one vantage point, on one day. It is evidence, not a guarantee — and it is not proof that nothing else is reachable, because only the ports you named were tried.
  • “Reachable” means it answered. It does not mean it was entered. No credentials are tried and nothing is written. I am saying that in those words because the alternative reading is worse, and this page should not profit from letting you assume it.
  • The ufw statement is Docker’s, and it is about ufw. firewalld is handled differently by Docker’s own documentation and gets checked rather than assumed. A cloud security group may already make the whole question moot.
  • Rotating a leaked credential does not un-leak it. Anyone who pulled that image still holds the old value, and any use of it before the rotation is outside what this engagement can see. This is the uncomfortable one and it belongs here rather than in a footnote.
  • Removing host access can break what depended on it. A socket mount is usually load-bearing. Every change is staged and reversible with a named rollback, and if something has to break to be fixed you hear that before it happens rather than after.
  • It is a point in time. The next docker compose up can republish a port and nothing will announce it. That is what the monthly tier is for, and it is a limitation of the one-off review rather than a reason to buy the subscription.
  • This is not a Kubernetes review and not an image vulnerability service. Trivy and Docker Scout do image scanning free and better.
  • If nothing published is reachable and no credential has travelled, that is the finding. The fee is earned, the engagement ends, and you get a report saying you were right — which is worth having and is not a failed review.

Four things this is not

Named up front, so none of them turns into silent scope halfway through.

  • Kubernetes. Different orchestrator, different failure modes, different tooling. Compose and Swarm are in scope; Kubernetes is quoted separately, if at all.
  • Image vulnerability remediation. Trivy and Docker Scout find known vulnerabilities free and do it better than anything I would build. Upgrading a base image across a running application is application work with its own testing, and it is quoted separately.
  • Incident response. If the drill finds that a published port answered and something else got there first, you are told immediately rather than at the end of the week, the engagement pauses, and what comes next is quoted separately.
  • Rebuilding what the socket mount was holding up. Removing the mount is in the hardened tier; replacing a container-discovery mechanism, a CI runner or a backup path with something that does not need root on the host is a build. Naming it here stops it becoming silent scope inside tier 3.

How payment works

Plainly, so nothing about it is a surprise later. There is no account to create, no portal to log into and no card stored anywhere.

  1. You apply through the form

    No payment at this point, and no commitment. Tell me how many services, what is published, and what prompted this.

  2. I review it and confirm the scope

    If it is not worth doing, I say so here and it costs you nothing. If it is, you get the scope, the host and port list and the window in writing before any invoice exists.

  3. An invoice arrives, payable within 48 hours

    It carries an invoice number and a due date. Unpaid past 48 hours, the application lapses and the slot is released to somebody else.

  4. Work begins once payment is received, and once the permission is signed

    Not before, and the written permission is the second half of that sentence rather than a formality. For one-off work there is nothing to suspend after delivery, so payment comes first — that is the standard arrangement and it runs both ways.

  5. Deploy Re-test, if you take it, is monthly in advance

    Non-payment simply stops the work. Nothing is billed silently in the background and you can stop whenever you like.

If you are still building

Everything above assumes a stack that grew over years. Before launch the same work is a fraction of it, and here the gap is unusually large.

What that involves

  • An internal-only network chosen while there are three services rather than twelve. Putting the backends behind one now is a few lines. Retrofitting it later means touching every service at once, on a live system, which is most of the repair tier.
  • Nothing published that does not need to be — decided once, at the point where changing it costs nothing and nobody is depending on the current behaviour.
  • Secrets kept out of the file and out of build arguments from the first commit, which is the only moment the credential blast radius is genuinely zero. Once an image with a build argument in it has been pushed, that is permanent.
  • The first drill run before there is a customer to notice it. Then you know what answers from outside on a day nobody cares about, which is the cheapest that test will ever be.

Mention that you are pre-launch in the form and I will quote it as its own piece of work. It is cheaper, and it is the version of this engagement I would rather sell.

Run the free checker first. Then tell me what is published.

It costs nothing, needs no signup and no email address, and it prints the finding id along with the line it found. If it comes back clean, or you have already connected to those ports from outside your own network and know what answers, you have saved a fee and I would rather tell you that than take it. If nobody has ever tried, that is exactly what this is for.

Prefer to talk? Book a free call ↗  ·  Or hire me on Upwork ↗  ·  Typical reply within one business day.

Questions

Your own free checker already told me what is wrong. Why pay you?
It did, it was right, and everything it prints stays free — no signup, no email address, no locked rows, no hidden findings. What it cannot do is read your file and know whether the port it flagged actually answers. Nothing that reads a file can. If the checker came back clean, or you have already tested those ports from outside your own network, you do not need this page.
Can I not just ask a language model to harden my Compose file?
Yes, and you should. It will do a decent job and it costs you nothing, which is exactly why this page does not sell you a hardened file as its headline. Paste the file in, take the corrected version, apply it. Then you still will not know whether the database port that was published for the last two years answered to anyone. That is the part nobody can write for you, because it is not a document — it is a connection.
Is this a penetration test?
No, and I will not let it be described as one. I connect once to each port you have named in writing, record whether something answered and what it said it was, and stop. No credentials are tried — not default ones, not the ones found in your own file. Nothing is exploited, nothing is written and no container escape is demonstrated. If you need proof of exploitability, that is a penetration test from a firm that does them, and I will tell you so rather than take the work.
I use firewalld, not ufw. Does any of this apply to me?
Possibly not, and I am not going to pretend otherwise to sell a review. The same Docker page that documents the ufw problem describes firewalld differently: Docker creates a zone called docker with target ACCEPT and a forwarding policy, and no bypass is claimed. So on firewalld it gets checked rather than assumed, and the drill is what settles it either way.
Everything sits behind a cloud security group. Am I fine?
You might well be, and that is a perfectly good result. A security group that drops the traffic before it reaches the host makes the whole packet filtering question moot for those ports. It is also the one thing people are most often wrong about, usually because the rule was written for one port and the stack has grown since. The drill takes an afternoon and either confirms it or does not.
A password leaked through a build argument. I rotated it. Is it over?
The new value is safe. The old one is not, and rotating does not un-leak it. A build argument is visible in docker history, so anyone who pulled that image still holds the old value, and any use of it before the rotation is outside what this engagement can see. What the review gives you is the map: where copies of that image exist, who could have pulled it, and what the credential still opens if somebody did.
Do you do Kubernetes?
Not on this page. Different orchestrator, different failure modes, different tooling — and a page that quietly let you assume coverage would be doing you no favours. Compose and Swarm are in scope. If you are on Kubernetes, say so in the form and I will tell you honestly whether it is worth quoting.
Will you find vulnerabilities in my images?
No, and you should not pay anyone to do that badly when it is free and done well. Trivy and Docker Scout scan images for known vulnerabilities better than anything I would build. Run them. Upgrading a base image across a live application is a different job — it is application work with its own testing — and it is quoted separately if you want it.
What happens if the drill finds that somebody else got there first?
You hear about it immediately, in the reply I notice it in, rather than in a report at the end of the week. The engagement pauses at that point. Evidence that a published database has been reached by somebody unexpected changes what needs doing and in what order, and incident response is not this page. I will tell you what I saw, what I did not touch, and what I would do next.