Guide

# Shrinking icons in Docker

Ship only the icons you render — delete the thousands you synced but never use.

## The problem

A full sync is thousands of files; you use a handful.

`rails g rails_icons:sync` copies a library's **entire** icon set into `app/assets/svg/icons/<library>/<variant>/`. Lucide alone is ~1,745 SVGs (~6.8 MB) — all git-tracked and baked into your image, even though a typical app renders a few dozen.

`glyphs:prune_icons` scans your source for the icons you actually reference and deletes the rest. Run it in the Docker build so the **image** ships only what it renders, while your **repo** keeps the full set for development. Icons resolve from disk at request time (not the Propshaft pipeline), so pruning is a plain file delete — nothing else to rebuild.

The scanner reads `app/**/*.rb` and `lib/**/*.rb` with a real parser, plus `.erb/.haml/.slim` as text, and recognizes every reference form:

```ruby
LucideIcon(:house)                      # component call
HeroIcon(:check, variant: :solid)       # explicit variant
_lucide(:triangle_alert)                # legacy helper
Icon(:house, library: :lucide)          # generic component
span(class: "iconify lucide--menu")     # raw iconify class
```

## Keeping dynamic icons

Names a static scan can't see.

Icon names built at runtime — from a database, config, or a gem's chrome — are invisible to a static scan. List them in `keep_icons` so the prune keeps them. It accepts a flat list or a per-library hash, and each entry may be an exact name or an `fnmatch` glob.

```ruby
Glyphs.configure do |config|
  config.keep_icons = %w[menu palette search circle-*]
  # or, scoped per library:
  # config.keep_icons = { lucide: %w[menu palette], phosphor: %w[lock] }
end
```

> **Tip:** Your configured `fallback_icons` are **always** kept automatically — a pruned fallback would 500 the next time any icon is missing.

## Running it

Dry-run by default; deletion is an explicit opt-in.

With no flags the task reports what it *would* delete and touches nothing:

```ruby
bin/rails glyphs:prune_icons
# [dry-run] Would prune 1735 icons, kept 10, freed 6.75 MB
#   lucide/outline: 1735 deleted, 10 kept
#   Re-run with PRUNE=1 GLYPHS_PRUNE_ICONS=1 to delete.
```

Deleting requires **both** `PRUNE=1` and `GLYPHS_PRUNE_ICONS=1` — a deliberate double opt-in so the task never wipes a developer's synced icons by accident:

```ruby
PRUNE=1 GLYPHS_PRUNE_ICONS=1 bin/rails glyphs:prune_icons
```

## In the Docker build

After precompile, before the final copy.

Run the prune in the build stage, **after** `assets:precompile` and before the final stage copies the app. The deletions land in the shipped image; your checkout keeps every icon.

```ruby
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
RUN SECRET_KEY_BASE_DUMMY=1 PRUNE=1 GLYPHS_PRUNE_ICONS=1 \
    ./bin/rails glyphs:prune_icons
```

> **Note:** Don't hook it onto `assets:precompile` with `enhance` — that would fire on a local precompile too and delete a developer's icons. A dedicated `RUN` keeps it to the image build.

## Safety net

A bad prune fails the build, never a request.

After deleting, the task **verifies** every kept icon — static references, `keep_icons`, and `fallback_icons` — still resolves on disk. If any is missing (say, a dynamic name you forgot to allowlist), it exits non-zero and **fails the build** instead of shipping an image that renders broken glyphs or 500s in production.

Two libraries are always left alone: the `animated` set bundled inside the `icons` gem, and any `custom_path` library outside the icons tree. And the prune refuses to empty a library whose keep-set is empty — a mis-scan can't silently wipe an entire library.