acme-companion: automatic Let's Encrypt certificates for nginx-proxy containers
Automated ACME SSL certificate generation for nginx-proxy
At a glance
- What is it?
- acme-companion is a companion container that issues and renews ACME certificates for containers proxied by nginx-proxy. It removes the certificate chore from your deploy, but it ties you to both port 80 reachability and the nginx-proxy model.
- Who is it for?
- Adopt acme-companion if you already run nginx-proxy and want certificates issued from the same VIRTUAL_HOST and ACME_HOST labels you use for routing. Do not adopt it if you cannot expose port 80 to the internet and are unwilling to configure DNS-01, or if you do not want a second container reading the Docker socket.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Shell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The certificate chore acme-companion removes
Running nginx-proxy gets you routing by container label. It does not get you TLS. Someone still has to request a certificate, prove control of the domain, put the files where nginx can read them, and repeat the whole thing before expiry. acme-companion exists to delete that job for anyone already using nginx-proxy.
The README describes it as a lightweight companion container for nginx-proxy that handles automated creation, renewal and use of SSL certificates for proxied Docker containers through the ACME protocol. Certificate issuance itself is delegated to acme.sh, which the image installs at build time. The companion is the orchestration layer: it watches containers, decides which domains need certificates, and tells nginx-proxy to reload after the files change.
The intended audience is narrow and specific. You need Docker, you need nginx-proxy, and you need the ability to run a second container that mounts the host Docker socket. If you terminate TLS at a cloud load balancer, or you run a single hand-written nginx config that you edit by hand, this is not your tool. The project's own feature list puts it plainly: it works with all versions of docker, which is a statement about compatibility rather than about scope. The scope is nginx-proxy and nothing else.
How the companion, docker-gen and acme.sh fit together
The Dockerfile shows the shape of the image. It pulls docker-gen from the nginxproxy/docker-gen image, installs acme.sh through install_acme.sh, and copies the app directory, which holds the entrypoint and start scripts. The container runs on Alpine, its entrypoint is /app/entrypoint.sh, and its default command is /app/start.sh. The Dockerfile also sets DOCKER_HOST=unix:///var/run/docker.sock, which explains why the companion's socket bind target differs from the proxy's.
The data flow has three parts. First, docker-gen observes the Docker daemon and renders nginx configuration from container environment variables. Second, the companion reads the same kind of labels and environment variables to work out which domains need certificates, then calls acme.sh to obtain them. Third, once a certificate is written, nginx is reloaded so the new files are served. The README lists that reload as a feature in its own right: automated update and reload of nginx config on certificate creation or renewal.
Two environment variables drive the decision. VIRTUAL_HOST controls proxying by nginx-proxy, and ACME_HOST controls certificate creation and SSL enabling by acme-companion. The README is explicit that certificates are only issued for containers that have both variables set to domains that resolve to the host and where the host is publicly reachable. That coupling is the design. Routing and certificate issuance are declared in the same place, so you cannot accidentally serve a hostname over plain HTTP because you forgot a separate certificate config. The cost is that you now have two variables to keep in sync on every service, and a typo in either one produces a container that is proxied but not encrypted, or encrypted but not routed.
The README also lists multi-domain SAN certificates as a supported feature, which matters for services that answer on several names at once. A SAN certificate is requested for a set of domains rather than one, so the companion has to collect the names before it talks to the certificate authority. That is more state to track than a single-domain request, and it is one more reason the companion watches the Docker daemon continuously rather than reading a static config file at startup.
Installing acme-companion with docker run
The README's basic usage is three containers. Start nginx-proxy first with two extra volumes, certs for /etc/nginx/certs and html for /usr/share/nginx/html, plus the Docker socket bound to /tmp/docker.sock.
docker run --detach \
--name nginx-proxy \
--publish 80:80 \
--publish 443:443 \
--volume certs:/etc/nginx/certs \
--volume html:/usr/share/nginx/html \
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
nginxproxy/nginx-proxyThe socket path matters. nginx-proxy expects the host socket at /tmp/docker.sock inside its own container, which is why the bind target is not the usual /var/run/docker.sock. Get this wrong and the proxy starts but discovers no containers.
Next start the companion, reusing the proxy's volumes with --volumes-from and adding a third volume for acme.sh state.
docker run --detach \
--name nginx-proxy-acme \
--volumes-from nginx-proxy \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--volume acme:/etc/acme.sh \
--env "[email protected]" \
nginxproxy/acme-companionHere the socket is bound to /var/run/docker.sock instead, because that is the path the companion's own DOCKER_HOST expects. DEFAULT_EMAIL is optional, but the README recommends it so Let's Encrypt can warn about expiring certificates and so you can recover your account. The /etc/acme.sh volume is where acme.sh configuration and state live, and the README links a separate document on data persistence for it.
Finally, start the application you want proxied with both variables set to the same domain.
docker run --detach \
--name your-proxied-app \
--env "VIRTUAL_HOST=subdomain.yourdomain.tld" \
--env "ACME_HOST=subdomain.yourdomain.tld" \
nginxAfter the container starts, the companion should detect the new domain, complete an HTTP-01 challenge through the shared html volume, write the certificate into the shared certs volume, and trigger an nginx reload. The README does not print a specific expected log line, so watch the companion's logs rather than looking for a documented success string. The README also notes that the host docker socket has to be bound inside the companion container, and that binding it read-only is what its examples do, so a Compose translation has to reproduce both socket binds at their different target paths.
The HTTP-01 requirements that decide whether this works
The default challenge is HTTP-01, and its requirements are strict. The host must be publicly reachable on both port 80 and port 443. Firewall rules must not block port 80. You cannot use nginx-proxy's HTTPS_METHOD=nohttp, because that redirect would break the challenge. Every (sub)domain you request must correctly resolve to the host, and if those names have AAAA records, the host must be reachable over IPv6 on ports 80 and 443 as well.
There is also a DNS-side requirement that catches people out. The README says to ensure your DNS provider answers correctly to CAA record requests, and warns that if the provider answers with an error, Let's Encrypt will not issue a certificate. You do not need to set a CAA record; the provider just has to respond properly. This failure lives outside the container, so it will not show up in the companion's configuration at all, and the error you see will look like a certificate authority refusal rather than a DNS problem.
If any of this is impossible, the documented escape hatch is the DNS-01 challenge. That path also unlocks wildcard certificates, which the README lists as supported only with DNS-01. It is a real trade. DNS-01 removes the port 80 requirement but adds credentials for your DNS provider and a different failure surface, and the README defers the details to a dedicated section of its ACME documentation rather than repeating them in the basic usage. The README also mentions that the companion can talk to other ACME CAs besides Let's Encrypt, and the repository topics list Buypass and ZeroSSL alongside letsencrypt, so the challenge rules you have to satisfy depend partly on which authority you point it at.
Where acme-companion is the wrong choice
The strongest argument against it is architectural. acme-companion assumes nginx-proxy is doing the routing. If you use Traefik, Caddy or a Kubernetes ingress controller, the companion has nothing to attach to, and its label-driven model duplicates configuration those tools already handle.
A second limitation is the Docker socket. Both nginx-proxy and acme-companion need access to the host daemon, and the README's examples bind /var/run/docker.sock read-only. Read-only still means the container can enumerate and inspect every container on the host. In a shared or multi-tenant environment that is a meaningful widening of the blast radius, and it is not something the companion can avoid, because container discovery is how it works.
A third is the port 80 dependency. Any environment where inbound port 80 is closed by policy, or where the host sits behind a proxy that terminates HTTP before it reaches nginx, cannot use the default HTTP-01 flow. The README points to DNS-01 rather than offering a way around it.
Finally, the repository does not document rollback. If a renewal writes a certificate you did not expect, or an issuance fails midway, the basic usage section does not describe how to revert to a previous certificate. You are expected to have your own backup of the certs volume. The README also does not document a way to force a renewal on demand, so a certificate that is stuck in a bad state has no documented reset path beyond removing state and starting over.
acme-companion compared with a standalone ACME client
The obvious alternative is running an ACME client yourself, certbot being the most familiar, and wiring the resulting files into nginx with your own reload hook. The difference is not the protocol, since both speak ACME, but who decides what to request.
With a standalone client you maintain a list of domains, a renewal timer, and a deploy hook that copies files and reloads nginx. That is more moving parts, but it is also independent of your reverse proxy. You can switch proxies, or run certificates on a host with no Docker at all, without rewriting anything.
acme-companion inverts that. The domain list is derived from container environment variables at runtime, so adding a service adds its certificate automatically and removing the container removes the need. The cost is that certificate management now only exists inside the nginx-proxy and Docker model. The two approaches are not ranked; they suit different constraints. If your infrastructure is already a set of labeled containers behind nginx-proxy, the companion removes configuration. If it is not, a standalone client is the simpler dependency, and it will keep working when you change proxies.
There is a middle ground worth naming. The companion does not have to be the only certificate source. Nothing in the README prevents you from placing certificates issued elsewhere into the shared /etc/nginx/certs volume, since the proxy reads that directory. What you lose in that arrangement is the automatic reload on renewal, which is one of the features the README lists for the companion itself.
Maintenance, upgrades and the MIT licence
The repository is not archived, and the last push was on 2026-08-17. Recent releases are v2.8.0 on 2026-07-10, v2.8.1 on 2026-07-11 and v2.8.2 on 2026-07-17, so the project has seen a release within the last few months. That is a fact about release cadence, not a promise about the next one.
Upgrade cost is mostly the cost of restarting two containers. The Dockerfile pins ACMESH_VERSION to 3.1.4 and pulls docker-gen 0.17.2, so a new companion image can change the underlying ACME client without you touching your compose file. That is convenient, and it also means a pinned image tag is the only thing standing between you and an acme.sh behaviour change you did not read about. The persistent state lives in /etc/acme.sh, which is why the README links a separate document on data persistence. Lose that volume and you lose account state and have to re-register.
The project is MIT licensed. The practical implication for adopters is that you can use it commercially and modify it, subject to the usual attribution terms. That is a statement about the licence text, not legal advice; if your organisation has specific compliance requirements, read the LICENSE file in the repository. The Dockerfile copies that LICENSE file into the image at /app, so the terms travel with the container you deploy.
What to check before you rely on it in production
The README covers the happy path well and leaves operational recovery thin. There is no documented command for forcing a renewal, no documented rollback procedure, and no documented way to inspect which certificates the companion believes are current. Those gaps matter more than the install steps, because install failures are loud and renewal failures are quiet.
Before trusting it, confirm three things. That your DNS provider answers CAA queries without error, since that failure happens outside the container. That the certs and html volumes are genuinely shared between the two containers, since --volumes-from is doing the work and a typo there produces a proxy that serves no certificate. And that /etc/acme.sh is on a named volume rather than the container filesystem, so a container replacement does not force re-registration. If you need issuance to work without inbound port 80, plan the DNS-01 configuration before you deploy rather than after the first failed challenge.
Editorial conclusion
Adopt acme-companion if you already run nginx-proxy and want certificates issued from the same VIRTUAL_HOST and ACME_HOST labels you use for routing. Do not adopt it if you cannot expose port 80 to the internet and are unwilling to configure DNS-01, or if you do not want a second container reading the Docker socket. Before relying on it, verify that your DNS provider answers CAA queries correctly, that the /etc/nginx/certs and /usr/share/nginx/html volumes are shared with nginx-proxy, and that /etc/acme.sh is on persistent storage so account state survives a container replacement.
Frequently asked questions
What is an ACME protocol?
ACME is the protocol acme-companion uses to obtain certificates from a certificate authority such as Let's Encrypt. The README describes the companion as handling certificate creation and renewal through the ACME protocol, with acme.sh as the client doing the protocol work.
Is CertBot an ACME client?
Yes, certbot is an ACME client, which is why it is the natural comparison for acme-companion. acme-companion takes a different approach: it delegates the protocol work to acme.sh and drives it from Docker container environment variables rather than a domain list you maintain.
What is the ACME protocol used for in Let's Encrypt?
It is how a client proves control of a domain and obtains a certificate from Let's Encrypt. acme-companion uses it for automated creation and renewal of certificates, with domain validation through HTTP-01 by default or DNS-01 as an alternative.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nginx-proxy-acme-companion)