Tutorials
Run the agent with Docker
Run the Kepeink agent as a Docker container in the same compose project as your app — no host install, no port forwarding, with exact env and networking.
Run the agent with Docker
Run the agent as a container registry.kepeink.hu/agent:2 in the same compose project as the service you want to expose. One service block, one env var, no host install.
Image
registry.kepeink.hu/agent:2
Anonymous pull (no docker login), multi-arch (amd64/arm64/arm), major-pinned (:2 always points to the latest release — docker compose pull is how you update). Exact immutable tags (2.5.44, and one per release) exist too.
Exact required runtime env:
| Variable | Value | Notes |
|---|---|---|
KEPEINK_AGENT_TOKEN | kpt_… from the tunnel you created | The only required input |
Baked defaults you do not need to set: KEPEINK_AGENT_NO_SUPERVISOR=1, KEPEINK_AGENT_AUTO_UPDATE=1 (the agent keeps itself patched, under a server-side staged-rollout policy that can halt or roll back the fleet; pulling a newer tag is still the durable path), KEPEINK_AGENT_DIR=/home/nonroot (the state dir, declared as a VOLUME — it holds the Let's Encrypt account key and certificate, so mount something there; see the end-to-end TLS note).
Do not override KEPEINK_AGENT_DIR. Pointing it somewhere your volume does not cover makes the ACME account key ephemeral, and a container that recreates often then spends a fresh Let's Encrypt issuance every time — against a per-registered-domain limit shared with every other tunnel on the same rproxy domain. Getting this wrong costs other people their certificates, not just yours. Mount a volume at /home/nonroot and leave the variable alone.
Verifying what the image bakes
If you want to confirm the values above rather than trust them, inspect the registry, not a local image — a local one may be a copy you pulled months ago:
docker buildx imagetools inspect registry.kepeink.hu/agent:2 \
--format '{{json .Image}}'
Look for KEPEINK_AGENT_DIR=/home/nonroot, "Volumes": {"/home/nonroot": {}}, and the org.opencontainers.image.version label naming the current release.
docker image inspect and docker inspect <container> both read local state and will happily describe an image you have already replaced. If you see KEPEINK_AGENT_DIR=/tmp and no volume, that is a pre-2.5.43 image — the ACME cache moved to /home/nonroot in 2.5.43. Re-pull rather than adapting your compose file to it:
docker compose pull && docker compose up -d
Minimal compose snippet
Same compose file (or docker-compose.override.yml) as your app — being in the same file puts the agent on the same network automatically:
services:
kepeink-agent:
image: registry.kepeink.hu/agent:2
restart: unless-stopped
environment:
KEPEINK_AGENT_TOKEN: ${KEPEINK_AGENT_TOKEN}
volumes:
- kepeink-agent-data:/home/nonroot
volumes:
kepeink-agent-data:
# .env next to compose file
KEPEINK_AGENT_TOKEN=kpt_xxxxxxxx
docker compose up -d kepeink-agent
docker compose logs -f kepeink-agent
One-liner without compose (uses host network's egress only — no --network host needed):
docker run -d --name kepeink-agent --restart unless-stopped \
-e KEPEINK_AGENT_TOKEN=kpt_… registry.kepeink.hu/agent:2
Networking: pick the target that the agent container can actually resolve
The container needs only egress to the internet. The tunnel target is how the agent reaches your app — choose the one the agent container can resolve:
| App location | Tunnel target (set in dashboard or POST /api/v1/tunnels) | Why |
|---|---|---|
| Service in same compose project | http://<service-name>:<port>, e.g. http://web:3000 or http://immich-server:2283 | Same Docker network → DNS name resolves inside the agent container |
| Service on the host (not in Docker) | http://host.docker.internal:<port> + add extra_hosts: ["host.docker.internal:host-gateway"] to the agent service | Bridge-to-host gateway |
| Service in a different compose project | Attach the agent to that project's network: networks: [other_default] with other_default: { external: true } | Otherwise the DNS name is not visible |
http://localhost:<port> inside the agent container means the agent container itself — it will 502. Join the right network and use the service name instead.
Verify
curl -fsS https://<your-sni>/ # the hostname the tunnel creation returned
docker compose logs kepeink-agent | grep -i session
Toggle the tunnel without redeploying the agent: POST /api/v1/tunnels/{id}/enabled {"enabled": false|true}.
Notes
docker compose pull && docker compose up -d kepeink-agentis the update path. The agent also self-updates in place between pulls, gated by a server-side rollout policy; a pull is still what makes the change survive a recreate.- For end-to-end TLS, mount a volume at
/home/nonroot. The Let's Encrypt account key and certificate are cached there, and losing the account key turns each recreate into a fresh issuance against a rate limit shared with every other tunnel on the samerproxydomain — not just a ~15 s delay for you. The image declares an anonymous volume so an image update keeps the cache, butdocker compose downand plaindocker rundo not; a named volume covers those. - More: Expose a local web app, Expose Immich with Docker, Turn on end-to-end TLS, API quickstart at
https://manage.kepeink.hu/llms.txt.