# stackage: the curated package sets that make Haskell builds reproducible

> Stackage is a set of curated Haskell package snapshots from Hackage, maintained as build constraints and turned into tested snapshots by a curator team, and this repository is where package authors get their libraries into them. It suits a Haskell library author who wants their package to be installable as part of a known-good set, and it is metadata and CI rather than a library.

**commercialhaskell/stackage** — Stable Haskell package sets: vetted consistent packages from Hackage

- Repository: https://github.com/commercialhaskell/stackage
- Website: https://www.stackage.org/
- Stars: 550 · Forks: 827
- Language: Dockerfile
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/commercialhaskell-stackage

## The repository is for package authors, not for users

The stackage repository is for package authors and maintainers to get their packages into Stackage. That sentence, first thing in the readme, settles what kind of project this is and who it is for.

If you simply want to use Stackage as an end user, the readme redirects you to the stack documentation or to stackage.org. The project itself is described as stable sets of Haskell packages from Hackage, and the primary language recorded for the repository is Dockerfile, which is a hint about where the work actually happens.

The readme also recommends the stack tool for doing builds, and notes that stack includes built-in Stackage support. So the division is: stack is what you run, stackage.org is what you consume, and this repository is where the curated inputs live.

The project has a licence of MIT, the default branch is master, and the last push was on 2026-10-20. There are no GitHub releases, which is right, because what is published from this repository is not a versioned artefact but a set of constraints and a nightly build plan that changes every day.

The repository layout makes the shape visible. There is build-constraints.yaml at the top level, which is the Nightly constraint file, and there are directories named automated/, check, check-lts, commenter, docker/, etc/ and verify-package, several of which are scripts rather than directories. There is also DATA-FLOW.md, which the readme flags as slightly outdated, CURATORS.md, MAINTAINERS.md, become-a-curator.md and a Dockerfile.

## Adding a package, and the four rules that will reject it

There are two different pull requests depending on which set you want to be in, and the readme distinguishes them precisely.

To add your package in Stackage Nightly, you edit the build-constraints file and open a PR. To add your package to Stackage LTS, you need to open a pull request for changing the lts-haskell build-constraints, which lives in a separate repository.

That split is the difference between a fast-moving stream and a frozen one. Nightly is rebuilt frequently against the newest versions on Hackage, and LTS is a stable series where the versions are locked down.

The conditions for acceptance are four, and the readme says all packages are welcome provided them.

The package from Hackage must build in current Stackage snapshots. The first sub-point is the one that surprises contributors: Stackage only uses pristine sources from Hackage, with no patches of packages allowed, including bounds, other than Hackage revisions. The second is that the package must be compatible with the versions of core libraries that ship with GHC, with a link to the FP Complete post on lenient lower bounds.

The package's tests and benchmark code should also build, and if possible the test suite should pass. The readme recommends setting up CI, which ensures a package is not accidentally incomplete.

The package should stay compatible with the newest versions of its dependencies.

And the package author or maintainer aims to follow MAINTAINERS.md.

That last one is not a technical requirement but it is the mechanism by which the set stays usable. A snapshot that includes a package whose maintainer has abandoned it is a package that will block the next dependency bump, and the aim to follow the maintainers document is the reviewer's tool for noticing that.

One practical detail is called out in a note. There is an approximate 30 minute delay between a package uploading to Hackage and being available to the GitHub workflow action to check upper bounds, so a pull request marked as failed due to using an older version should be closed and reopened to retrigger a CI build.

## The seven steps that turn constraints into a snapshot

The processing section is the part that explains how the curated set actually becomes a tested build, and it is worth reading in order.

Nightlies work like this. Get the list of core packages. Get build constraints from the list of maintained packages. Load up the package index. Calculate the build plan using newest versions of packages. Write out a YAML file with the complete build plan. Verify that the build plan can be compiled. Perform the build.

Two of those steps are the substance. Calculating the build plan is the solver: given the constraints and the package index, it picks a version of every package that is mutually satisfiable. Verifying that the plan can be compiled is the step that makes the set trustworthy, because it catches an unsatisfiable combination before anyone depends on it.

LTS is much shorter, and the reason is instructive. Load up the most recent build plan, convert the build plan into constraints for the next build, and then continue from step 3 of the nightly process, which is loading the package index.

So an LTS series begins life as a nightly build plan and is then frozen into constraints. That single mechanism explains most of the behaviour users notice, because constraints do not move on their own. The LTS 6 and LTS 7 lines running in parallel is the visible consequence of the same design.

There is one policy note attached to the nightly process. Packages in Nightly that are holding newer versions of other packages back for an extended period of time will eventually get disabled, depending on the impact but at latest usually right after the branching of the next major LTS version.

That is a deadline rather than a courtesy. A package that keeps the resolver pinned cannot be carried into the next LTS branch, so the set has a natural expiry for stale members.

## Why your package version looks older than Hackage

The FAQ is where the project answers the questions that generate the most confusion, and the answer to why a Stackage snapshot has an older version than Hackage has four distinct causes.

The first is the age of the snapshot. Once a snapshot is created, it is frozen for all time, so a nightly from 2016 will look dated by 2018 no matter what has happened on Hackage since. That is the simplest reason and the one most people hit by accident.

The second is LTS version locking. Major versions are locked down when an LTS run is first created, so subsequent minor versions do not get new versions. The readme's example is precise: if LTS 6.0 has foo at version 1.2.3, and the author immediately releases 1.3.0 and never releases another 1.2.x version, you will never get another update in the LTS 6 line.

The third is upper bounds placed because other packages have problems with newer dependency versions. The readme's advice is to open the build-constraints file and search for Stackage upper bounds, which is a direct pointer into the file that generates the behaviour.

The fourth is wired-in packages. Those ship with GHC and cannot be upgraded, and packages depending on them are fixed to GHC versions. The common examples named are containers and transformers.

There is also a separate FAQ on why LTS is still on an older GHC. Typically it takes some months from a new major GHC release before the ecosystem supports it enough to push it to a new stable Stackage major version release, and there can be GHC regressions that hold up an LTS major release. For minor GHC releases the lag should be less, but it still requires extra work, and the delay also allows for some community testing before updating LTS.

That last clause is the one to keep in mind. The lag is not only a technical constraint, it is a deliberate staging step, and the projects that are waiting for a new GHC are waiting for a tested one.

## The build server image, the curator role and the other repositories

Building the whole package set is not a normal user task, and the readme says so.

Generally only the stackage build server run by the stackage curator team, and people interested in incorporating Stackage snapshots into an OS distribution, need to build the entire package set. If you are interested in trying it yourself, the readme points at the curator guide, with a warning: it is not a recommended practice and there will likely be problems you will need to debug yourself.

The Dockerfile at the root shows what that server is. It starts from ubuntu:24.04 and sets HOME to /home/stackage and LANG to en_US.UTF-8. It then runs four numbered setup scripts in sequence, each copied into the image and executed and then removed: 01-build-server.sh, 02-apt-get-install.sh, 03-custom-install.sh and 04-cleanup.sh.

```dockerfile
FROM ubuntu:24.04

ENV HOME=/home/stackage
ENV LANG=en_US.UTF-8

COPY docker/01-build-server.sh /tmp/01-build-server.sh
RUN /tmp/01-build-server.sh && rm /tmp/01-build-server.sh

COPY docker/02-apt-get-install.sh /tmp/02-apt-get-install.sh
RUN /tmp/02-apt-get-install.sh && rm /tmp/02-apt-get-install.sh
```

The environment settings that follow are the interesting ones. CPATH is set to include the Java 8 OpenJDK include directories and the LLVM 3.7 include directory, which tells you some Haskell packages in the set need to compile against those toolchains. And LIBTORCH_HOME points at a libtorch directory inside a mounted work volume, with a comment explaining the reason: the artifact is shared on rebuilds, so a very large download is not repeated every time the image is rebuilt.

That is a good illustration of what the set contains. Stackage is not only the popular pure Haskell libraries; it includes packages that pull in Java and machine learning toolchains, which is why the build image needs a C include path and a torch installation.

The Stackage project consists of multiple repositories, and the readme lists the others: stackage-server, curator, lts-haskell and stackage-snapshots. This repository holds the metadata on packages to be included in future builds plus some project information, and DATA-FLOW.md describes how it all fits together, though the readme marks that document as slightly outdated.

There is also a become-a-curator.md file, and the curator repository carries its own runtime image build badge, which is the other half of the story: curation is a role with tooling, not just a set of YAML files.

## Conclusion

stackage fits a Haskell package author who wants their library to build inside a known-good snapshot, and it does not fit someone looking for a package index or a build tool, since the user-facing side is the stack tool and the stackage.org site. Before opening a pull request, read MAINTAINERS.md, confirm your package builds against current snapshots with pristine Hackage sources, and check the CI timing note, because the upper bounds check runs against the Hackage index with an approximate 30 minute delay after upload.

## FAQ

### How do I get my Haskell package into Stackage?

For Stackage Nightly, edit the build-constraints.yaml file in the stackage repository and open a pull request. For Stackage LTS, open a pull request against the build-constraints in the separate lts-haskell repository.

### Why does Stackage use an older version of my package than Hackage?

There are four causes. A snapshot is frozen once created, LTS lines lock major versions at creation so later 1.x releases never reach that line, some packages carry upper bounds because of dependency problems, and wired-in packages that ship with GHC such as containers and transformers are fixed to the GHC version.

### How long is a Stackage LTS release maintained?

The readme says only a single LTS major version is guaranteed to be maintained at a time, and it will be maintained for at least three months, which is the originally proposed support window and has not changed since. The capability to keep multiple LTS runs operational in parallel is maintained, and LTS 6 and 7 in fact did so.

### Why does my pull request to Stackage fail with an older version?

There is an approximate 30 minute delay between a package uploading to Hackage and being available to the GitHub workflow action that checks upper bounds. If a pull request is marked as failed because it used an older version, close and reopen it to retrigger a CI build.

## Sources

- [commercialhaskell/stackage on GitHub](https://github.com/commercialhaskell/stackage)
- [Issues](https://github.com/commercialhaskell/stackage/issues)
- [License: MIT](https://github.com/commercialhaskell/stackage/blob/master/LICENSE)
- [Project website](https://www.stackage.org/)
- [README](https://github.com/commercialhaskell/stackage/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/commercialhaskell-stackage
