# Pebble is a throwaway ACME server for testing Let's Encrypt clients

> Pebble is a small Go implementation of an RFC 8555 ACME server, built so that ACME client developers can test against something self-contained instead of the production Boulder stack. It deletes its state on every run and randomises its keys, on purpose, and it enforces that with guardrails.

**letsencrypt/pebble** — A miniature version of Boulder, Pebble is a small RFC 8555 ACME test server not suited for a production certificate authority.

- Repository: https://github.com/letsencrypt/pebble
- Stars: 798 · Forks: 179
- Language: Go
- License: MPL-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/letsencrypt-pebble

## A miniature Boulder, not a certificate authority

Pebble is described as a miniature version of Boulder, and the README says it plainly: it is a small ACME test server, not suited for use as a production CA. The warning is repeated in bold capitals, that Pebble is NOT INTENDED FOR PRODUCTION USE and is for testing only.

What makes it different from a normal test double is that it is not a mock. Pebble implements RFC 8555, it issues real X509 certificates, and it runs the actual validation flows with the ACME challenge types. The certificates are real in the cryptographic sense and worthless in the trust sense.

Two design decisions define the whole project. By design Pebble drops all of its state between invocations, and it randomises the keys and certificates used for issuance. There is no database to seed, no account to create and no certificate that survives a restart.

The licence is MPL-2.0 and the project is written in Go, with the module path github.com/letsencrypt/pebble/v2 and a go.mod requiring Go 1.25.0.

## The problem Boulder is too heavy to solve in a test suite

The goals section explains why a second server exists at all. Pebble aims to address the need for ACME clients to have an easier to use, self-contained version of Boulder to test against while developing ACME v2 support.

The reason is stated directly: Boulder is multi-process, requires heavy dependencies such as MariaDB and gRPC, and is operationally complex to integrate with other projects. A CI job that needs a certificate authority cannot reasonably stand up that stack.

Beyond that, Pebble is a test bed for new and compatibility breaking ACME features, and it aims to encourage ACME client best practices. Where the ACME specification allows customisation or a choice of CA, Pebble deliberately makes choices that differ from Boulder. It changes the path structures for its resources and directory endpoints, with the stated goal of emphasising client specification compatibility and avoiding over-fitting on Boulder and the Let's Encrypt production service.

One example makes the intent legible. Pebble randomises the /directory endpoint URL, specifically to ensure that clients are not hardcoding URLs.

## Guardrails make production use inconvenient on purpose

One of the four top level goals is to aggressively build in guardrails against non-testing usage, and the README is candid that this is a feature rather than a limitation.

Pebble will not support non-volatile storage or persistence between executions, and it randomises the keys and certificates used for issuance. Where possible it makes decisions that force clients to implement ACME correctly, such as the randomised /directory URLs.

The effect is that a Pebble instance cannot be used as a real CA even by accident. Its certificates are not signed by a trusted root, its state evaporates, and a client that has hardcoded endpoint URLs will break between runs. That last point is the one to internalise as a developer: a client that passes against Pebble has to be reading the directory URL at runtime, which is exactly the behaviour a production CA test cannot check for you.

The same reasoning is why the ACME server is not configured to look like a real one. The differences from Boulder are deliberate, so that a client that only works against Pebble's shapes is caught early rather than in production.

## What Pebble deliberately does not implement

The limitations section is short and specific, and it is the part of the README to read before designing a test around it.

Pebble is missing some ACME features, with pull requests welcome. It does not presently support pre-authorization, and it does not support revoking a certificate issued by a different ACME account by proving authorization of all of the certificate's domains.

Pebble does not perform all of the same input validation as Boulder. Some domain names that would be rejected by Boulder and Let's Encrypt may work with Pebble, which means a client that passes against Pebble can still be sending invalid requests that a production CA would refuse.

And Pebble does not enforce any rate limits, so the README states plainly that it is not presently an appropriate tool for testing that your client handles Boulder and Let's Encrypt rate limits correctly.

Taken together, those three gaps describe a server for testing protocol mechanics, not a stand-in for the production service. A client tested only against Pebble knows that its ACME flow works, not that its error handling, its rate limit backoff or its cross-account revocation logic works.

## Running it as a binary with a config file

The install is a Go build. You set up Go, add ~/go/bin to your PATH or set GOBIN to a directory already on it, clone the repository, and then install the command:

```bash
git clone https://github.com/letsencrypt/pebble/
cd pebble
go install ./cmd/pebble
```

The binary is then started with a configuration file:

```bash
pebble -config ./test/config/pebble-config.json
```

and the ACME directory endpoint is then available at https://localhost:14000/dir. The README notes that if pebble is not easily accessible in your PATH you should replace the command with ~/go/bin/pebble or $GOBIN/pebble.

A detail worth understanding rather than copying: the default challenge validation ports are unprivileged high ports, with 5002 for HTTP-01 and 5001 for TLS-ALPN-01. The reason given is to make it easier to test ACME clients and run challenge response servers without root privileges. Those values can be changed by editing the httpPort and tlsPort values in the config file passed to -config.

## The compose file wires a challenge test server beside it

The Docker route exists because the challenge types need a server that can answer on demand, which is what pebble-challtestsrv provides for DNS resolution.

The bundled compose file starts both, and it shows the port layout the README describes later:

```yaml
  pebble:
    image: ghcr.io/letsencrypt/pebble:latest
    command: -config test/config/pebble-config.json -strict -dnsserver 10.30.50.3:8053
    ports:
      - 14000:14000 # HTTPS ACME API
      - 15000:15000 # HTTPS Management API
```

The two services sit on a bridge network with a fixed subnet, 10.30.50.0/24, and the challenge test server is given 10.30.50.3 while Pebble takes 10.30.50.2. The dnsserver flag points Pebble's DNS resolution at the challenge server, which is how the HTTP-01 and DNS-01 validations get answers for domains that do not really exist.

After docker-compose up, the ACME API is at https://localhost:14000/dir, the Pebble management interface at https://localhost:15000, and the challenge test server management interface at http://localhost:8055.

## Driving the challenge server by hand

Once the containers are up, the challenge test server is what lets you control the answers. The README suggests updating its DNS data with a new default IPv4 address used to respond to A queries from Pebble:

```bash
curl --request POST --data '{"ip":"172.20.0.1"}' http://localhost:8055/set-default-ipv4
```

The management interface at port 8055 is the control plane for the whole validation process, and the pebble-challtestsrv README has more on what it can do.

For one-off containers, the ports have to be mapped manually, and the README gives both commands:

```bash
docker run -p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest
docker run -p 5001:5001 -p 5002:5002 -p 5003:5003 -p 8053:8053 -p 8055:8055 -p 8443:8443 ghcr.io/letsencrypt/pebble-challtestsrv:latest
```

The first line only maps the ACME and management ports, so the default HTTP-01 port of 5002 and the TLS-ALPN-01 port of 5001 are not published in that case, and the second line is what makes the challenge flows reachable from the host.

## Strict mode exists because the two goals collide

The strict mode section states the tension in the project directly. Pebble's goal of aggressively supporting new protocol features and backwards compatibility breaking changes is slightly at odds with its goal of providing a simple, light-weight ACME test server for clients to use in integration tests.

On one hand the maintainers want to introduce breaking changes quickly and use Pebble as a test bed for that. On the other they want a server that does not surprise client developers in a routine integration test.

Strict mode is the compromise, and the compose file enables it with the -strict flag alongside the config and the dnsserver address. The README section on strict mode is cut off in the part of the documentation available here, so the exact behaviours it toggles are not documented in what can be read, and that is worth checking in the full documentation before you rely on it either way.

The release history is current: v2.10.1 on 2026-04-20, v2.10.0 on 2026-02-18 and v2.9.0 on 2025-12-18. The repository is not archived and the last push was on 2026-09-28, so the strict mode behaviour is likely to keep moving.

## Conclusion

Pebble suits anyone writing an ACME client who needs a deterministic certificate authority to point at in CI, since it is self-contained, starts in seconds and comes with a companion challenge test server. It is the wrong tool for testing rate limit handling, pre-authorization or revocation across accounts, because those are the features it deliberately leaves out. Before wiring a client to it, read the limitations section, since Pebble accepts some inputs that Boulder would reject, and start with the compose file rather than the binary so the challenge test server is already resolving DNS for you.

## FAQ

### What is Pebble and what is it used for?

Pebble is a small ACME test server, a miniature version of Boulder, that lets ACME client developers test against a self-contained RFC 8555 server. It is for testing only and is not suited for use as a production CA.

### How do I run Pebble with Docker Compose?

Run docker-compose up in the repository, which starts a pebble instance and a pebble-challtestsrv instance for DNS resolution with the ports mapped to the host. The ACME API is then at https://localhost:14000/dir, the Pebble management interface at https://localhost:15000 and the challenge test server interface at http://localhost:8055.

### What does Pebble not support?

It does not support pre-authorization, nor revoking a certificate issued by a different ACME account. It does not perform all of the same input validation as Boulder, so some domains Boulder would reject may work, and it enforces no rate limits, which makes it unsuitable for testing rate limit handling.

## Sources

- [Issues](https://github.com/letsencrypt/pebble/issues)
- [letsencrypt/pebble on GitHub](https://github.com/letsencrypt/pebble)
- [License: MPL-2.0](https://github.com/letsencrypt/pebble/blob/main/LICENSE)
- [README](https://github.com/letsencrypt/pebble/blob/main/README.md)
- [Releases](https://github.com/letsencrypt/pebble/releases)

---

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