Broken links are boring until they cost you trust.
For a static site, documentation portal, personal blog, or self-hosted dashboard collection, they are also easy to miss. You move a page, rename a post, delete an image, or paste a link that worked once and quietly stops working later.
LinkChecker is one of the old, practical tools for catching that class of problem.
What is LinkChecker?
LinkChecker is a GPL Python website validator for checking links in documents or full websites.
I inspected:
repo: https://github.com/linkchecker/linkchecker
commit: 9b178bdafba75334626c0c8c32a5f544cdbe7ffd
version: v10.6.0-53-g9b178bda
latest release in changelog: 10.6.0, released 2025-07-28
license: GPL-2.0-or-later
The project supports Python >=3.10 and ships a linkchecker CLI entrypoint.
What It Checks
LinkChecker can check:
- local HTML files;
- static-site output folders;
- public websites;
- HTTP and HTTPS URLs;
- FTP links;
mailto:links;- local file links;
- sitemap URLs;
- anchors inside HTML pages;
- selected plugin-based checks.
It can crawl recursively and run with multiple threads. It also respects robots.txt by default.
Output Formats
The reporting side is a big reason to use LinkChecker instead of a quick custom script.
It can output:
- normal text;
- colored text;
- HTML;
- SQL;
- CSV;
- XML;
- sitemap XML;
- DOT;
- GML;
- failures-only output.
For CI, the most useful pattern is usually:
linkchecker --output=none --file-output=html/utf-8/reports/report.html ./public/
That gives you a persistent report without flooding the job log.
Architecture
The codebase is a Python CLI application.
The rough flow is:
CLI arguments / config file
-> configuration object
-> URL aggregation queue
-> checker workers
-> protocol-specific checkers
-> parsers and plugins
-> result cache
-> output loggers
Important pieces:
linkcheck/command/linkchecker.py: CLI entrypoint, setup, and exit-code handling.linkcheck/checker/: protocol-specific URL checkers.linkcheck/htmlutil/: HTML parsing and link extraction.linkcheck/parser/: sitemap parsing.linkcheck/plugins/: extra checks such as anchors, Markdown, PDF/Word parsers, HTTP headers, and SSL certificate checks.linkcheck/logger/: report formats.
Dependencies are intentionally modest:
requests;dnspython;beautifulsoup4.
Using LinkChecker with Docker
There was already a Home-Lab LinkChecker folder, but it needed cleanup.
I updated it to:
- use the current GitHub Container Registry image;
- pin the image to
ghcr.io/linkchecker/linkchecker:10.6.0; - remove stale
wummel.github.iodocumentation links; - remove hardcoded personal paths;
- treat the container as a one-shot CLI runner, not a daemon;
- ignore local smoke-test
reports/andfixtures/.
The reusable Home-Lab compose file is here:
The site includes the same Compose file from the local snippets folder:
assets/snippets/linkchecker/docker-compose.yml
Pre-Requisites - Docker
Install Docker on your system before proceeding:
- Linux: Official Docker Engine install guide
- Windows / Mac: Docker Desktop
Verify installation: docker --version && docker compose version
services:
linkchecker:
image: ghcr.io/linkchecker/linkchecker:10.6.0
container_name: linkchecker
user: "${UID:-1000}:${GID:-1000}"
working_dir: /work
volumes:
- ./:/work
stdin_open: true
tty: true
restart: "no"
Check the installed version:
docker compose run --rm linkchecker --version
Check a website:
docker compose run --rm linkchecker --verbose https://example.com
Write an HTML report:
mkdir -p reports
docker compose run --rm linkchecker \
--no-status \
--output=none \
--file-output=html/utf-8/reports/example.html \
https://example.com
Check a local static build:
docker compose run --rm linkchecker --verbose ./public/index.html
Useful Options
Start with these:
| Option | Purpose |
|---|---|
--recursion-level=2 |
Limit crawl depth before pointing it at large sites. |
--check-extern |
Check external links too. |
--ignore-url=REGEX |
Ignore matching URLs after syntax checks. |
--no-follow-url=REGEX |
Check matching URLs but do not recurse into them. |
--threads=5 |
Reduce concurrency. |
--timeout=30 |
Avoid waiting a full minute on slow links. |
--output=none |
Quiet console result output. |
--file-output=html/utf-8/reports/report.html |
Write a report file. |
For a big site, do not start with infinite recursion and external checks.
Use a bounded first pass:
docker compose run --rm linkchecker \
--recursion-level=2 \
--timeout=30 \
https://example.com
Then widen the scan once the configuration is sane.
CI/CD Usage
For a static site generator, the natural place to run LinkChecker is after the build step.
Example shape:
hugo --minify
linkchecker --output=none --file-output=html/utf-8/linkchecker.html ./public/
In CI, decide whether warnings should fail the build.
LinkChecker exits:
0when there are no errors or printed warnings;1when errors or warnings are found;2on internal errors.
For early adoption, I usually prefer collecting the report first without failing deployment. Once the ignore rules are tuned, make it stricter.
Field Test
I smoke-tested the Home-Lab runner with the pinned image:
docker compose run --rm linkchecker --version
It returned:
LinkChecker 10.6.0 released 2025-07-28
Then I checked a small local fixture with one intentionally broken link:
docker compose run --rm linkchecker \
--no-status \
--output=text/utf-8 \
--file-output=html/utf-8/reports/fixture.html \
fixtures/index.html
That produced the expected result:
1 error found
exit_status=1
It also wrote:
reports/fixture.html
Finally, I checked https://example.com with recursion disabled:
docker compose run --rm linkchecker \
--no-status \
--recursion-level=0 \
--timeout=20 \
https://example.com
That returned:
0 errors found
exit_status=0
Is There a Web UI?
Sort of, but that is not the main deployment path.
The repo includes WSGI/web-interface examples. The upstream install notes warn that allowing a web script to trigger checks can consume CPU and memory, especially if many users can start large crawls.
For self-hosting, I would treat LinkChecker as a CLI and CI tool first. If you expose any web wrapper, restrict it to trusted users.
How It Compares
LinkChecker is good when you want a deterministic CLI, Docker runner, CI step, and report files.
Screaming Frog and similar desktop crawlers are more interactive and SEO-heavy, but not as simple to automate in a small self-hosted pipeline.
Playwright/Cypress tests are better for checking JavaScript-heavy flows, but they are not a general recursive link crawler by default.
Custom scripts are fine for a short URL list, but they usually miss anchors, sitemap parsing, robots behavior, output formats, recursion controls, and edge cases.
FAQ
Is LinkChecker self-hosted?
It is self-run rather than self-hosted as a web app.
You run the CLI locally, in Docker, or in CI. There is no database-backed dashboard to keep running.
Does LinkChecker need Docker?
No.
You can install it with pipx install linkchecker. Docker is useful when you want a repeatable runner without installing Python packages on the host.
How do I apply LinkChecker to my own website?
Start with one bounded scan and save the report:
mkdir -p reports
docker compose run --rm linkchecker \
--no-status \
--recursion-level=2 \
--timeout=30 \
--threads=5 \
--output=text/utf-8 \
--file-output=html/utf-8/reports/site.html \
https://example.com
Replace https://example.com with your site URL. Once the first report is readable, increase recursion, point it at your sitemap, or add --check-extern for external links.
For a static site, the stricter option is to check the generated output before deployment:
hugo --minify
linkchecker --output=text/utf-8 ./public/
That catches broken internal links and missing assets before they become public.
Should I check the live URL, sitemap, or local build?
Use all three for different jobs.
| Target | Best For |
|---|---|
./public/ |
Pre-deploy checks for generated static output. |
https://example.com/sitemap.xml |
Broad crawl of published pages without guessing entry points. |
https://example.com |
User-path crawl from the homepage and visible navigation. |
For Foss Engineer, the full-site pass used the sitemap because it naturally covers posts, docs, and app pages that may not all be linked from the homepage.
How do I understand the output?
A LinkChecker result is usually easiest to read from the HTML or CSV report.
The important fields are:
| Field | Meaning |
|---|---|
URL |
The link or asset LinkChecker tried to fetch. |
Parent URL |
The page where that link was found. This is usually where you fix the source. |
Name |
The link text or image alt text, when available. |
Result |
The outcome: valid, warning, redirect, missing, timeout, refused, and similar states. |
Line / Column |
Source position when LinkChecker can map the link back to HTML. |
Real URL |
The final URL after redirects. Useful when old paths still work but should be updated. |
Treat Parent URL as your starting point. The broken URL tells you what failed; the parent tells you which page produced it.
What do warnings and errors mean?
Errors are broken behavior: 404, DNS failures, refused connections, malformed URLs, missing local files, or anchor targets that do not exist.
Warnings are not always urgent. A 301 or 308 redirect can be acceptable, but many redirect warnings usually mean old URLs, old category paths, or links that should be updated to their final destination.
The exit code matters in automation:
| Exit Code | Meaning |
|---|---|
0 |
No errors or printed warnings. |
1 |
LinkChecker found errors or warnings. |
2 |
LinkChecker itself failed internally. |
When adopting it on an existing site, collect HTML/CSV reports first. After the noisy legacy redirects are cleaned up or ignored intentionally, make the CI job fail on new issues.
How do I keep the output useful in CI?
Write reports to files and keep the console quiet:
linkchecker \
--no-status \
--output=none \
--file-output=html/utf-8/reports/linkchecker.html \
--file-output=csv/utf-8/reports/linkchecker.csv \
./public/
Upload reports/linkchecker.html as a CI artifact. Use the CSV report when you want to sort failures by parent page, status, or URL in a spreadsheet.
Does LinkChecker check external links?
It can, but you need to choose that deliberately with --check-extern.
For first runs, keep scans limited. External checks can be slow, noisy, and sensitive to rate limits.
Can LinkChecker check local static-site output?
Yes.
That is one of the best use cases:
linkchecker ./public/
or through Docker with your site folder mounted into the container.
Can I use LinkChecker in GitHub Actions?
Yes.
Install it with pipx, your package manager, or run the container image after building the site. Then upload the HTML report as an artifact.
Does it execute JavaScript?
No, not like a browser automation tool.
Use it for document and website link validation. For JavaScript-rendered routes, pair it with a browser-based test runner or check the generated/static output that users actually receive.
Comments