FaviconAPI is a self-hosted favicon proxy with a browser-based UI that fetches website and service icons from multiple upstream sources (10+), caches results, and exposes them through simple HTTP routes.
Website: faviconapi.com
Browser tools: faviconapi.com/#tools
- Browser search - add
/search?q=%sas a custom search engine (Chrome, Edge, Firefox) - Custom URL - Build a shareable URL with your own preferred provider, fallbacks and minimum icon size.
- Bookmarklet - drag FaviconAPI Copy to your bookmarks bar to copy a site's favicon URL
- Github: R0GGER/favicon-api
- FaviconAPI - CHANGELOG
This README is the getting-started guide. The full documentation set lives in
src/docs-content and is also served at /docs on a running instance.
| Document | Contents |
|---|---|
| Getting started | Install, configure, and run FaviconAPI (this README) |
| API | Full endpoint reference, API v1 JSON, API keys, quotas, caching headers |
| Performance | Cache TTL tuning, scraper latency, worker sizing, preloading popular sites |
| Preload manager | Walkthrough of the /admin web interface |
| Browser tools | Custom search engine, custom URL builder, bookmarklet, offline search page |
| Reverse proxy | nginx, Caddy, and Traefik setups, HTTPS headers, canonical URLs |
- Why FaviconAPI?
- How it works
- Quick start (Docker)
- Configuration (.env)
- Preload manager (/admin)
- Browser tools
- Routes
- Custom profile URLs
- License
FaviconAPI started out of a very practical need. While building my own dashboards with Mafl+ (R0GGER/maflplus), I wanted a hassle-free way to fetch favicons and logos and link them to the services on my dashboard - without manually downloading and hosting an image for every single tile.
In practice that turned out to be surprisingly painful. To get decent coverage I always ended up combining multiple sources, and time and again I noticed that the "different" tools I was using were really just reaching for the same underlying providers behind the scenes - mostly Google and DuckDuckGo. When one of those came back with a blank, low-resolution, or generic placeholder icon, I had no fallback and was stuck.
What I was missing was a tool dedicated entirely to favicon lookup-one that aggregates independent sources, queries them together, and intelligently picks the best result instead of betting on a single upstream. Existing tools simply didn't offer that kind of integrated, multi-source solution.
So I built it. FaviconAPI brings 10+ favicon providers and 5 CDN-icon catalogs together behind one consistent API. It races providers in parallel, normalizes and caches the results, and returns the highest-quality icon it can find - with the others available as explicit fallbacks. It grew from a helper for my own dashboards into a self-hosted favicon proxy that anyone can run.
- Fetches favicons from multiple upstream sources (10+) or by scraping a site's HTML.
- Races providers in parallel on
/{domain}(website favicons) and/{app-name}(service icons when the path has no dot). - Caches responses in memory (LRU) and on disk to reduce upstream load and improve latency.
- Normalizes icons for the v1 JSON API into 128×128 PNG files served from a CDN route.
- Looks up service icons from the selfh.st icons, homarr dashboard-icons, LobeHub icons, SVGL, and theSVG catalogs by service name.
- Generates custom profile URLs that encode a preferred provider, fallbacks, and a minimum size directly in the path — no account or storage required.
Interactive API docs and a live playground are available at
/apion a running instance.
Clone the repository and start the stack. The bundled docker-compose.yml pulls the published image and reads its settings from .env.example.
docker compose up -dThe UI is at http://localhost:3100 (host 3100 → container 3000).
To build the image from this repository instead of pulling it, swap the two lines in docker-compose.yml: uncomment build: . and comment out image: ghcr.io/r0gger/favicon-api:latest.
docker pull ghcr.io/r0gger/favicon-api:latest.env.example is a documented example of every setting, tracked in git — the shipped docker-compose.yml reads it directly, so the stack starts with working defaults. Every variable is documented with comments in that file; the tables under Configuration (.env) cover the most-used ones.
For your own settings — and for anything secret, such as ADMIN_SESSION_SECRET, ADMIN_PASSWORD_HASH, LOGODEV_TOKEN, and BRANDFETCH_CLIENT_ID — copy it to .env (gitignored) and point env_file: at that copy:
cp .env.example .env env_file: .envenv_file is read when the container is created, so after editing it run docker compose up -d (which recreates the container). A plain docker compose restart keeps the old environment.
The besticon sidecar does not use env_file. Compose interpolates its BESTICON_* values from a project .env or from the defaults in docker-compose.yml.
services:
favicon-api:
#build: .
image: ghcr.io/r0gger/favicon-api:latest
container_name: favicon-api
restart: unless-stopped
ports:
- "3100:3000"
volumes:
- favicon-cache:/cache
env_file: .env.example
depends_on:
besticon:
condition: service_healthy
networks:
- besticon
besticon:
image: matthiasluedtke/iconserver:latest
container_name: besticon
restart: unless-stopped
environment:
TZ: ${BESTICON_TZ:-Europe/Amsterdam}
ADDRESS: ${BESTICON_ADDRESS:-}
CACHE_SIZE_MB: ${BESTICON_CACHE_SIZE_MB:-1024}
HOST_ONLY_DOMAINS: ${BESTICON_HOST_ONLY_DOMAINS:-*}
HTTP_CLIENT_TIMEOUT: ${BESTICON_HTTP_CLIENT_TIMEOUT:-5s}
HTTP_MAX_AGE_DURATION: ${BESTICON_HTTP_MAX_AGE_DURATION:-720h}
HTTP_USER_AGENT: ${BESTICON_HTTP_USER_AGENT:-}
PORT: ${BESTICON_PORT:-8080}
SERVER_MODE: ${BESTICON_SERVER_MODE:-redirect}
healthcheck:
test:
- CMD
- wget
- --quiet
- --tries=1
- --spider
- http://localhost:8080/up
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- besticon
networks:
besticon:
name: besticon
driver: bridge
volumes:
favicon-cache:Notes
- besticon has no
ports:mapping — only thefavicon-apiservice can reach it onhttp://besticon:8080. SetBESTICON_URL=http://besticon:8080in your env file. - Without besticon: remove the
besticonservice,depends_on,networks, andBESTICON_URL. The built-in HTML scraper is used instead. - Host cache path: use
- /path/to/cache:/cacheinstead of the named volume; runchown 100:101 /path/to/cacheandchmod 755 /path/to/cacheso the container user can write. - Behind a reverse proxy: point nginx, Caddy, or Traefik at host port 3100 — see Reverse proxy.
All settings are documented in .env.example. Copy that file to .env and edit it (or set environment: entries in Compose).
The tables below cover the most-used variables. For the complete list — including UI_CARD_URL, UI_INCLUDE_APP_ICONS, SCRAPER_FALLBACK, and tuning notes — see .env.example and Performance.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
TCP port the HTTP server listens on. |
CACHE_DIR |
./cache (/cache in Docker) |
Base directory for on-disk favicon cache files. |
MEMORY_CACHE_MAX |
2000 |
Max favicons in the per-worker in-memory LRU cache. |
MEMORY_CACHE_TTL |
86400 |
In-memory cache entry lifetime (seconds). .env.example ships 1 day; the code fallback if unset is 3600. |
DISK_CACHE_TTL |
7 |
On-disk cache entry lifetime (days). |
CACHE_STALE_RETENTION |
30 |
Days past DISK_CACHE_TTL an icon may still be served while it is refreshed in the background. 0 = hard expiry. |
CACHE_SIZE_MB |
1024 |
Max total disk cache size (MB). Oldest entries are evicted when exceeded. Code fallback 0 = no size cap (TTL eviction only). |
UPSTREAM_TIMEOUT |
5000 |
Upstream HTTP timeout (ms) for providers, besticon, and scrape targets. |
UV_THREADPOOL_SIZE |
16 |
Node libuv thread pool size for disk I/O, DNS, etc. Must be set before process start. |
WORKERS |
CPU core count | Number of cluster workers. Set explicitly in Docker when CPU is limited. 1 disables clustering. |
| Variable | Default | Description |
|---|---|---|
DEFAULT_PROVIDER |
scraper |
Preferred provider for /{domain}. When set, it runs exclusively first; fallbacks only race after it fails. Values: scraper, google, googlev2, duckduckgo, yandex, faviconso, vemetric, favicondev, faviconkit, faviconrun, twentyicons, ryanjc, logodev, brandfetch, selfhst, dashboardicons, lobehub, svgl, thesvg. logodev requires LOGODEV_TOKEN; brandfetch requires BRANDFETCH_CLIENT_ID. |
PICK_HEAD_START_MS |
150 |
Head-start (ms) for the built-in first provider on /{domain} when DEFAULT_PROVIDER is unset. |
LOGODEV_TOKEN |
(unset) | logo.dev publishable key. Enables /logodev/{size}/{domain}; without it the route returns 503. |
BRANDFETCH_CLIENT_ID |
(unset) | Brandfetch Logo API client ID. Enables /brandfetch/{size}/{ext}/{domain}; without it the route returns 503. |
BESTICON_URL |
(unset) | Base URL of a sidecar besticon instance (e.g. http://besticon:8080). /scraper/{domain} asks besticon first, then falls back to the built-in scraper. |
SCRAPER_PROBE_BATCH_SIZE |
4 |
HTML scraper icon candidates probed in parallel per batch (/scraper/{domain} and /{domain}). |
SCRAPER_ICONS_CACHE_TTL |
604800 |
TTL (seconds) for the in-memory cache of enriched scraper icon lists (/{domain}/json). Also used for scraper discovery disk cache entries when SCRAPER_DISK_CACHE is enabled. Unset → same as DISK_CACHE_TTL. |
SCRAPER_ICONS_CACHE_MAX |
500 |
Max domains in that scraper-icons LRU cache. |
SCRAPER_DISK_CACHE |
true |
When true, persist scraper discovery (HTML, icon lists, besticon JSON, manifests, probes) under {CACHE_DIR}/scraper-discovery. Survives restarts; shared across workers. |
SCRAPER_DISK_CACHE_DIR |
{CACHE_DIR}/scraper-discovery |
Directory for that discovery cache. Only used when SCRAPER_DISK_CACHE=true. |
MANIFEST_PROBE_MAX |
12 |
Max manifest URLs to probe per domain when HTML does not link one directly. |
SCRAPER_MAX_ICON_SIZE |
0 |
Max output dimension for /scraper/{domain}. Larger sources are downscaled; when set, output is also PNG-recompressed (truecolor vs. palette — whichever is smaller, see SCRAPER_PNG_*). 0 = native resolution. |
SCRAPER_PNG_PALETTE |
true |
Enable the near-lossless indexed/palette PNG pass for capped scraper output (typically 25-55% smaller). false forces strict truecolor PNG. |
SCRAPER_PNG_MIN_PSNR |
40 |
Minimum perceptual PSNR (dB, alpha-premultiplied) the palette PNG must reach to be used over truecolor. Higher = closer to lossless; 0 = always take the smaller file. Ignored when SCRAPER_PNG_PALETTE=false. |
| Variable | Default | Description |
|---|---|---|
API_KEYS_DB |
/cache/api-keys.sqlite |
SQLite file for hashed API keys and monthly usage counters. Keep on the same volume as CACHE_DIR. |
API_CACHE_DIR |
/cache/api |
Directory for normalized 128×128 PNGs from /api/v1/favicon. Served via /cdn/favicons/{domain}.png. |
API_CACHE_TTL |
7 |
How long a generated PNG counts as cached (days). Converted to seconds for Cache-Control max-age on the CDN route. |
API_REQUIRE_KEY |
false |
.env.example makes /api/v1/favicon public. Code fallback if unset is true (key required, quotas enforced). A provided key is silently ignored when this is false. |
PLAN_FREE_LIMIT |
25 |
Monthly call quota for free plan keys. 0 = unlimited. |
PLAN_PRO_LIMIT |
2500 |
Monthly call quota for pro plan keys. 0 = unlimited. |
PLAN_ENTERPRISE_LIMIT |
0 |
Monthly call quota for enterprise plan keys. 0 = unlimited. |
Endpoint reference, authentication, error codes, and the API-key CLI: API.
| Variable | Default | Description |
|---|---|---|
PRELOAD_DB |
/cache/db/preload.sqlite |
SQLite file with the preload domain list, popularity counters and icon overrides. Keep it in a subdirectory: loose files in CACHE_DIR can be evicted by the disk-cache size cap. |
PRELOAD_TRACK_HITS |
true |
Count successful domain lookups so rank reflects real usage. Preload-script requests are never counted. false disables all counting. |
PRELOAD_HIT_FLUSH_MS |
15000 |
How long each worker buffers hits before writing them. Counting never writes per request. |
PRELOAD_HIT_DEDUPE_MS |
60000 |
Window in which the same visitor asking for the same domain counts once. Keeps one web-UI search from outweighing real usage. 0 counts every request. |
PRELOAD_MIN_RANK |
3 |
Minimum usage rank (hit count) for traffic-only domains in preload-top-sites.js --source db. List-imported domains still fill remaining slots, after real traffic. |
PRELOAD_AUTO_DISABLE_AFTER |
5 |
Consecutive failed preload runs after which an automatically managed domain is switched off. |
PRELOAD_RANK_MONTHS |
3 |
Rolling window (months) used by manage-preload.js recalc to recompute usage rank from monthly hit buckets. |
PRELOAD_OVERRIDE_RELOAD_MS |
30000 |
How often a worker re-checks the database for changed icon overrides and blocklist patterns. |
Manage the list with scripts/manage-preload.js (npm run preload:list,
preload:add, preload:disable, preload:import, preload:export,
preload:recalc), or from the browser with the preload manager.
See Performance §10 for the full workflow.
| Variable | Default | Description |
|---|---|---|
ADMIN_SESSION_SECRET |
(unset) | Signing key for the /admin session cookie. Unset or shorter than 32 bytes keeps the page at 404. Legacy name ADMIN_JWT_SECRET still works. |
ADMIN_PASSWORD_HASH |
(unset) | scrypt hash of the login password. Unset or malformed keeps the page at 404. |
ADMIN_USER |
admin |
Login name. |
ADMIN_SESSION_TTL |
3600 |
Idle timeout (seconds) of a normal session; every action extends it. |
ADMIN_REMEMBER_TTL |
30d |
Idle timeout when "Keep me signed in" was ticked. |
ADMIN_LOGIN_MAX_ATTEMPTS |
10 |
Failed sign-ins per IP per 15 minutes, counted per worker. |
The first request for a domain is slow: FaviconAPI still has to discover icons and fetch them from upstream. Preload warms that cache in advance — for well-known sites, for domains your users actually look up, or for a list you curate — so later requests hit disk instead of the network.
The preload manager is the password-protected web UI for that workflow, at /admin. It talks to the same SQLite database as the CLI (scripts/manage-preload.js): which domains are on the list, how often they are requested, which image to force for a given site, which hosts to block, and when to run a preload pass. From the browser you can import or export CSV, start a run and watch the log, or generate a crontab line — without a shell on the server.
The page is off until you configure both a signing secret and a password. Without ADMIN_SESSION_SECRET and ADMIN_PASSWORD_HASH, /admin returns 404 and management stays CLI-only.
# 1. Generate the 512-bit signing key for the session cookie
docker compose exec favicon-api npm run admin:secret
# 2. Set the login password (asks twice, prints ADMIN_USER + ADMIN_PASSWORD_HASH)
docker compose exec favicon-api npm run admin:password -- --user yourname
# 3. Put both lines in .env, recreate the container, then sign in at /admin
docker compose up -dThe key is always 512 bits and signs HS512. Only the scrypt hash of the password lands in .env. Sign-in issues an HttpOnly, SameSite=Strict session cookie whose TTL is an idle timeout — every action extends it — and Keep me signed in switches that window to ADMIN_REMEMBER_TTL. Rotating ADMIN_SESSION_SECRET signs everyone out at once.
Full walkthrough: Preload manager.
The Tools button on the homepage (and on /api and /docs) opens a side panel of browser helpers. You set a profile once — preferred provider, optional fallbacks, and a minimum icon size — and that profile powers every tool in the panel. The settings are encoded in the URL: no account, nothing stored on the server.
From the panel you can:
- Add FaviconAPI as a custom search engine (
/search?q=%s) so typing a domain or app name in the address bar opens the homepage with results already loaded. - Build a custom URL that pins that provider chain for dashboards, password managers, and
<img>tags (/{id}/{domain}). - Drag the FaviconAPI Copy bookmarklet to your bookmarks bar and copy a site's favicon URL from any page you visit.
- Download an HTML file that searches using the same profile, from your own computer.
Open it via Tools in the top navigation, or https://your-host/#tools.
Full walkthrough: Browser tools.
Domain providers use /{provider}/{size}/{ext}/{domain} (e.g. /google/128/png/github.com). Catalog providers use /{provider}/{size}/{format}/{service} — SVG with size 0 (e.g. /svgl/0/svg/github). Legacy three-segment routes and short aliases (/g/, /d/, /sh/, …) remain valid.
/{provider}/{size}/{domain} # legacy; PNG assumed
/{provider}/{size}/{ext}/{domain} # canonical for domain providers
Providers without a native upstream size accept the size segment and are resized server-side.
Quick examples
https://your-host/github.com
https://your-host/scraper/github.com
https://your-host/google/64/png/github.com
https://your-host/selfhst/128/png/jellyfin
https://your-host/svgl/0/svg/github
https://your-host/thesvg/0/svg/github
Full endpoint list, JSON discovery, and caching headers: API.
All providers run in parallel on /{domain}; each also has its own route.
| Provider | Route | Alias | Notes |
|---|---|---|---|
| HTML scraper | /scraper/{size}/{domain} |
/s/ |
/scraper/{domain} serves the largest available icon; parses <link rel="icon">, og:image / twitter:image meta (near-square only), manifest, and fallbacks; optional besticon sidecar via BESTICON_URL |
/google/{size}/{domain} |
/g/ |
Sizes 16, 32, 64, 128 | |
| Google v2 | /googlev2/{size}/{domain} |
/g2/ |
faviconV2; sizes 16, 32, 64, 128, 180, 256 |
| DuckDuckGo | /duckduckgo/{size}/{domain} |
/d/ |
Resized server-side |
| Yandex | /yandex/{size}/{domain} |
/y/ |
Resized server-side |
| Favicon.so | /faviconso/{size}/{domain} |
/f/ |
Resized server-side |
| Vemetric | /vemetric/{size}/{domain} |
/v/ |
?format=webp, png, or jpg; or /{size}/{ext}/ in path |
| Favicon Extractor | /favicondev/{size}/{domain} |
/p/ |
Resized server-side |
| Faviconkit | /faviconkit/{size}/{domain} |
/k/ |
Sizes 16, 32, 64, 128, 256 |
| Favicon.run | /faviconrun/{size}/{domain} |
/fr/ |
Sizes 16, 32, 64, 128, 256 |
| twenty-icons.com | /twentyicons/{size}/{domain} |
/ti/ |
Sizes 16, 32, 64, 128, 180, 192 |
| favicon.ryanjc.com | /ryanjc/{size}/{domain} |
/rj/ |
Resized server-side |
| logo.dev | /logodev/{size}/{domain} |
/l/ |
Requires LOGODEV_TOKEN; resized server-side |
| Brandfetch | /brandfetch/{size}/{ext}/{domain} |
/bf/ |
Requires BRANDFETCH_CLIENT_ID; canonical SVG route uses size 0 (e.g. /brandfetch/0/svg/github.com); raster sizes 16–512 for png/webp/jpg in the path; auto-fallback svg → png → webp when format is not pinned in the path; ?type=icon|symbol|logo&theme=light|dark; legacy /brandfetch/{size}/{domain} still works |
Look up an icon by app/service name (e.g. jellyfin). All support ?variant=color\|light\|dark where applicable.
| Catalog | Route | Alias |
|---|---|---|
| selfhst icons | /selfhst/{size}/{service} |
/sh/ |
| Dashboard Icons | /dashboardicons/{size}/{service} |
/di/ |
| LobeHub icons | /lobehub/{size}/{service} |
/lb/ |
| SVGL | /svgl/{size}/{service} |
/sv/ |
| theSVG | /thesvg/{size}/{service} |
/ts/ |
- 128×128 is the site default — the Web UI, service-icon catalogs, LobeHub, SVGL, theSVG, and the API v1 CDN all standardize on 128 when no size is specified. It sits in the middle of the supported range: large enough to stay sharp on dashboards, bookmark tiles, and password-manager entries (including on retina displays when shown smaller), yet small enough to keep responses fast and cache-friendly. 128 is also a safe minimum icon size when you need a guaranteed baseline that most providers can satisfy without upscaling a tiny source into a blurry icon.
- Resized server-side providers and catalogs accept sizes 16, 32, 64, 128, 256.
- Brandfetch SVG routes use size 0 in the path; raster routes use native upstream sizes 16, 32, 64, 128, 256, 512 (via Brandfetch's
/h/{size}/w/{size}/icon.pngpath). - LobeHub, SVGL, and theSVG use sizes 64, 128, 256 (default 128).
- A few resize-only domain providers (DuckDuckGo, Yandex, Favicon.so, Favicon Extractor) default their sizeless proxy URLs to 64 instead — their upstream icons are often small, and 64 avoids serving an upscaled, soft image when you omit the size segment.
- Legacy short aliases also accept the original sizeless form (e.g.
/sh/{service},/d/{domain}).
| Endpoint | Description |
|---|---|
/{domain} |
Best favicon (parallel provider race) |
/{id}/{domain-or-appname} |
Custom profile favicon |
/{domain}/json |
JSON list of all endpoint URLs for a domain |
/api/v1/favicon?url= |
FaviconAPI-compatible JSON API — see API v1 |
/cdn/favicons/{domain}.png |
Public CDN route for cached API v1 PNGs |
/providers |
JSON: which optional providers are enabled |
/services/resolve/{service} |
JSON: per-catalog slug matches for a service name |
/search?q= |
Custom search engine redirect to the homepage |
https://your-host/scraper/{domain}?refresh=1
Forces a fresh scrape by clearing the cached scraper entry (memory and disk) before fetching again. Use when a site changed its favicon, after scraper fixes, or when debugging stale results. ?nocache=1 is an alias for ?refresh=1.
Build a shareable URL that pins your own preferred provider, an ordered list of up to four fallbacks, and a minimum icon size — without an account or any server-side storage:
https://your-host/{id}/{domain-or-appname}
The {id} is a URL-safe (base64url) string that encodes the whole configuration; there is no database. Generate one from Tools → Build custom URL on the homepage, then append any domain (github.com) or app name (immich). See Browser tools.
How the icon is resolved
The chain [preferred, ...fallbacks] is tried in order and the first usable icon wins:
- A provider that returns an SVG satisfies any minimum (vector) and is served as-is (
image/svg+xml). - A provider that returns a raster icon must have a source whose smaller side is ≥ the minimum size; it is then served as PNG at exactly that size.
- If a provider returns nothing usable or a raster below the minimum, the next fallback is tried. If the whole chain fails, a transparent placeholder is returned with
404.
Encoding
The id is the base64url of a compact JSON array — keep this contract identical on both ends:
// [version, preferredProvider, [fallbacks...], minSize]
[1, "scraper", ["googlev2", "duckduckgo"], 128]Providers are any from the favicon providers / catalogs tables; minimum sizes are 16, 32, 64, 128. logodev/brandfetch only resolve when their credentials are configured (otherwise that step is skipped). Domain-only providers (scraper, raster providers, brandfetch) are skipped for app-name targets.
API ·
Performance ·
Preload manager ·
Browser tools ·
Reverse proxy —
all in src/docs-content.
FaviconAPI is released under the MIT License.
Name and branding. The MIT License covers the source code only. The name FaviconAPI, the logo, and the faviconapi.com domain are not part of the license — if you publish a fork or run a modified public instance, please give it its own name so users can tell the two apart.
Third-party icons. FaviconAPI fetches icons from upstream providers and catalogs at runtime; it does not ship them. Those icons, logos, and brand marks remain the property of their respective owners and are covered by the terms of the source they come from — selfh.st icons, homarr dashboard-icons, LobeHub icons, SVGL, theSVG, and the favicon providers. Check those terms before using the results commercially.