Elm Mirror Server

December 26, 2025 ยท View on GitHub

A Python-based read-only mirror of the Elm package server for Elm 0.19 packages. Use it to:

  • Work offline: Access Elm packages when package.elm-lang.org is unreachable
  • Speed up builds: Run a local mirror for faster CI/team package downloads
  • Archive packages: Preserve Elm packages independently of the official server

This is a single Python 3.10+ script with no other dependencies. Just take the elm_mirror.py file and run python elm_mirror.py straight out of the box with the appropriate flags.

Note that in order to download and compile packages from this mirror, you'll need Zokka, an alternative Elm compiler, to consume packages (the standard Elm compiler has a hardcoded package server URL).

Unlike the standard Elm package server which does not actually store packages, but only stores references to GitHub locations where the packages actually reside, this mirror downloads all packages locally. This means that if you choose to mirror all Elm packages, you will need to have a decent amount of storage available (as of December 2025, all public Elm 0.19 packages together is about 3.5 GB).

Quick Start

1. Sync packages from the official server

If you would like to sync from the official server please first run the download_preseeded_mirror.sh script that can be found in this repository. E.g. like so

# This will create a directory named `mirror` on your local machine that is
# pre-seeded with all Elm packages up to Dec 24th, 2025
# Note that for reasons associated with how the `tar.gz` were created, these
# will always be named `mirror`. The argument to the script only determines its
# parent directory.
./download_preseeded_mirror.sh .

This script will download all Elm packages as of Dec 24th, 2025 from pre-formed .tar.gz files in this project's GitHub releases. This reduces the load on the main Elm package server and only hits the Elm package server (and other GitHub links) for packages since that date. Note that the download may take a while since, as stated previously, it'll be about 3.5 GB to download. (N.B. if you're wondering why the tar files are gzipped if Elm package source code is already zipfiles, turns out there's actually a noticeable amount of compression that still happens!)

# Set up GitHub auth (increases rate limit from 60 to 5000 requests/hour)
# Not necessary if you're only planning to sync a small subset of packages
# Read on to see how to do that
export GITHUB_TOKEN=your_token_here

# Sync all packages to ./mirror
python elm_mirror.py sync --mirror-content ./mirror --incremental-sync

Note that as long as you have pre-seeded mirror, even if you didn't run --incremental-sync, the load wouldn't be awful on the Elm server. You would hit the all-packages endpoint which serves up a JSON object that is, as of Dec 2025, about 216 KB (and compressed so that it is 47 KB over the wire), instead of the since endpoint (which likely is less than ~10 KB). The biggest difference is making sure that mirror is pre-seeded. Otherwise every Elm package will require a trip to the Elm server to download its package-specific metadata.

2. Serve the mirror

If you don't care about keeping your mirror up to date with new Elm packages, you can run.

python elm_mirror.py serve \
    --mirror-content ./mirror \
    --port 8000 \
    --base-url http://localhost:8000 

Note that you should substitute http://localhost:8000 with whatever URL you expect packages to be available at, because this is the URL prefix that we will tell an Elm compiler where to fetch a package from. For example, if you are making this mirror publicly available at https://example.com behind a reverse proxy, even though you can hit it locally at localhost from the server, the --base-url should be set to https://example.com because that is where clients will expect to find packages.

If you would like to make sure that the mirror states up to date with the latest Elm packages, you can pass additional arguments to make sure it syncs with the main package server in the background. If you do so we would recommend you use --incremental-sync to reduce load on the main package repository.

Note that adding --sync-interval causes serve to subsume sync's function (the first thing serve will do is perform a sync).

python elm_mirror.py serve \
    --mirror-content ./mirror \
    --port 8000 \
    --base-url http://localhost:8000 \
    --sync-interval 86400 \ # Sync every day
    --incremental-sync

3. Configure Zokka to use the mirror

Zokka is a drop-in compatible variant of the vanilla Elm compiler that supports custom package repositories:

export ELM_HOME="elm_home"  # or ~/.elm for global config
mkdir -p "$ELM_HOME/0.19.1/zokka/"
cat > "$ELM_HOME/0.19.1/zokka/custom-package-repository-config.json" << EOF
{
    "repositories": [
        {
            "repository-type": "package-server-with-standard-elm-v0.19-package-server-api",
            "repository-url": "http://localhost:8000",
            "repository-local-name": "local-mirror"
        }
    ],
    "single-package-locations": []
}
EOF

# Use zokka exactly like you would use elm
npx zokka make src/Main.elm

Commands

CommandDescription
python elm_mirror.py syncDownload packages from package.elm-lang.org
python elm_mirror.py serveRun HTTP server to serve mirrored packages
python elm_mirror.py verifyCheck integrity of downloaded packages

Common Options

For sync:

  • --mirror-content DIR: Where to store packages (default: .)
  • --incremental-sync: Don't download the full JSON list of all Elm packages, but only the JSON list of packages since the last sync. Note that even without this flag, we won't re-download packages if they were previously successfully downloaded (to figure this out, the script looks at a file named registry.json in your mirror folder). This only changes which initial JSON endpoint we hit for the Elm package server
  • --package-list FILE: JSON file to sync specific packages only
  • --http-rate-limit N: Maximum HTTP requests per hour (default: 4000). Set to 0 to disable.

For serve:

  • --base-url URL: Public URL for the mirror (required)
  • --port PORT: Port to listen on (default: 8000)
  • --sync-interval SECS: Enable background sync at this interval
  • Other options for sync also work for serve if --sync-interval is set

Selective Sync

To sync only specific packages, create a JSON file:

["elm/core", "elm/html", "mdgriffith/elm-ui@2.0.0"]

If you leave off a version number then it will sync all versions of that package.

Then run:

python elm_mirror.py sync --package-list packages.json --mirror-content ./mirror

GitHub Token

Without a token, GitHub limits you to 60 requests/hour. With a token, you get 5000/hour. By default, we throttle to 4000 total HTTP requests per hour across all endpoints (configurable via --http-rate-limit). Hence if you don't use a GitHub token, you may quickly run into GitHub rate limiting issues.

Create a token at: https://github.com/settings/personal-access-tokens (make sure you're logged in)

Note that your token doesn't need any special permissions. It just needs to be able to read public repositories (which is the minimal set of permissions any token comes with). All GitHub cares about is that the token is associated with your account.

Once you have the token, make sure it is available as an environment variable under GITHUB_TOKEN and all the commands will automatically pick it up.