exposedev/expose: a PHP tunneling server you can self-host
A beautiful, fully open-source, tunneling service - written in pure PHP
At a glance
- What is it?
- Expose is an MIT-licensed ngrok alternative written in pure PHP, shipped as a Composer package and a Docker image. It is for developers who want their own tunnel server rather than a hosted plan, and who accept that running the server is their job.
- Who is it for?
- Adopt Expose if you want a tunnel server you control and are comfortable running PHP, Docker and a SQLite file yourself; skip it if you need a zero-ops hosted tunnel or a documented rollback path, because the README points to the docs site for deployment details rather than covering them.
- 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 13 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Expose solves, and who ends up running it
A tunnel service gives a machine behind NAT a public URL. Expose does that, but the README positions it as an open-source ngrok alternative written in PHP, which changes who owns the moving parts. With a hosted tunnel, someone else runs the relay and you get a URL. With Expose, the relay is a PHP application you install, and the README's Dockerfile shows what that means: php:8.1-cli, the zip extension, Composer, and a bundled expose binary that is made executable during the build.
The audience is narrow and specific. It suits teams already running PHP infrastructure, because the server is a Laravel-style application with app/, bootstrap/, config/, database/ and public/ directories at the repository root. It also suits anyone who wants the tunnel endpoint inside their own network boundary, or who needs to demonstrate that traffic does not leave infrastructure they control. It does not suit someone who wants a URL in thirty seconds and no server to babysit.
How the server is put together
The repository layout tells most of the story before you read any documentation. There is a config/ directory holding expose.php, which the Dockerfile exposes through the environment variable exposeConfigPath=/src/config/expose.php. There is a database/ directory, and the compose file mounts ./database/expose.db into the container at /root/.expose. So the server's state, at least the part that matters for persistence, is a single SQLite file rather than an external database service.
docker-compose.yml wires the rest. The service uses the image beyondcodegmbh/expose-server:latest and maps host port 8080 to the container's ${PORT}. It passes port, domain, username and password as environment variables read from a .env file, since .env-example sits at the repository root. The extra_hosts entry adds host.docker.internal:host-gateway, which is how the container reaches services running on the Docker host. The restart: always policy means the container comes back after a reboot without supervision.
The Dockerfile is a plain PHP CLI image, not a web server image. It installs git, libzip-dev and zip, installs Composer, copies the source to /src, and runs composer install -o --prefer-dist. Defaults are baked in as environment variables: port=8080, domain=localhost, username=username, password=password. Those defaults are the first thing to change, and the entrypoint script at docker-entrypoint.sh is where the container's startup behaviour lives.
Installing the server with Docker Compose
The compose file is the shortest path to a running server. It expects a .env alongside it, because every value is referenced as ${PORT}, ${DOMAIN}, ${ADMIN_USERNAME} and ${ADMIN_PASSWORD}. Copy the example file and fill it in before starting anything.
cp .env-example .envThen set the four values the compose file reads. The keys must match exactly, since Compose substitutes them literally.
PORT=8080
DOMAIN=localhost
ADMIN_USERNAME=username
ADMIN_PASSWORD=passwordWith those in place, bring the stack up. The database directory is created on the host and mounted into the container, so the SQLite file survives a container restart.
docker compose up -dWhat you should see is one container running and listening on the host port you chose, with ./database/expose.db appearing on the host side. If the container exits immediately, check the port and domain values first, because the entrypoint reads them from the environment.
Building the image yourself instead of pulling it
Pulling beyondcodegmbh/expose-server:latest is convenient, but the tag moves. If you want a build you control, the Dockerfile in the repository is the recipe, and it is short enough to audit in a couple of minutes. It pins the base image to php:8.1-cli and installs dependencies with composer install -o --prefer-dist, then marks the expose binary executable.
docker build -t expose-server .
docker run -p 8080:8080 \
-e port=8080 \
-e domain=localhost \
-e username=username \
-e password=password \
-v ./database/expose.db:/root/.expose \
expose-serverNote the casing difference: the Dockerfile defines lowercase environment names (port, domain, username, password), while the compose file passes lowercase names too but reads uppercase variables from .env. Getting that wrong is a silent failure, because the container will start with the baked-in defaults and you will not notice until you try to authenticate. The compose file is the safer of the two paths for that reason.
The limitation that matters: the README is not the manual
The README is mostly a pointer. It says that installation instructions for your own server, in-depth usage and deployment details live at the official documentation, and that is the whole of the self-hosting guidance. There is no rollback procedure in the README, no upgrade path between releases, and no description of what changes in the SQLite schema when the server version moves. The CHANGELOG.md at the repository root is where release notes live, and the recent releases listed are 3.2.4, 3.2.3 and 3.2.2.
The second limitation is the default credential set. username=username and password=password are compiled into the image as environment defaults. Anyone who starts the container without overriding them has an open tunnel server. The compose file reads from .env, which mitigates this only if you actually edit the file.
The third is the single-file database. Mounting expose.db into the container is straightforward, but it also means the server's state is one file on one host. Nothing in the repository material describes replication or a shared database backend, so scaling the relay horizontally is not something the documentation supports.
Expose is the wrong tool if you need a managed service with an SLA, or if your team has no PHP or Docker experience. It is also the wrong choice if you cannot expose a port from your relay host to the public internet, since the whole point is that the relay is reachable.
Managed Expose, ngrok, and the difference in approach
The README itself names the alternative: ngrok. The practical difference is where the relay runs and who patches it. ngrok gives you a hosted endpoint and an account; Expose gives you a PHP application you install, with a Compose file and a SQLite volume. If your constraint is that tunnel traffic must terminate on hardware you control, ngrok is not a substitute, and Expose is.
Expose's own project also offers a managed option. The README describes a managed version with a proprietary platform and a free EU test server at expose.dev, plus Expose Pro for a global server network, custom domains and higher-speed tunnels. That is a genuine fork in the road: you can run the open-source server yourself, or use the vendor's hosted network. The self-hosted path is MIT-licensed and the managed path is proprietary, and the README is explicit about that split.
The trade-off is maintenance. A hosted tunnel is someone else's upgrade problem. A self-hosted Expose server is a container you rebuild when a new release lands, a SQLite file you back up, and a credential set you rotate. That is not a large amount of work, but it is recurring work, and it is the reason most teams pick a hosted service.
Licence, upgrades, and what maintenance actually costs
Expose is MIT-licensed, and the README points to LICENSE.md for the full text. MIT is permissive: you can run it commercially, modify it, and ship it inside your own product, provided you keep the copyright notice and licence text. The managed platform and Expose Pro described in the README are separate and proprietary, so the open-source licence does not extend to the vendor's hosted network. Nothing here is legal advice, and if you plan to redistribute the server inside a commercial product, read LICENSE.md rather than a summary.
The upgrade picture is the weak point. The repository carries a CHANGELOG.md, and the recent releases are 3.2.4 (2026-09-17), 3.2.3 (2026-09-11) and 3.2.2 (2026-03-27). The gap between 3.2.2 in March and 3.2.3 in September is roughly six months, so releases arrive in bursts rather than on a schedule. The last push to the repository was on 2026-09-17, which is recent, and the repository is not archived.
Because the state lives in ./database/expose.db, an upgrade means backing up that file before you pull a new image. The README does not document a migration step, so the safe assumption is that you own the backup. If you cannot tolerate that, the managed option exists for exactly this reason.
Editorial conclusion
Adopt Expose if you want a tunnel server you control and are comfortable running PHP, Docker and a SQLite file yourself; skip it if you need a zero-ops hosted tunnel or a documented rollback path, because the README points to the docs site for deployment details rather than covering them. Before committing, read https://expose.dev/docs, check the entrypoint script's default credentials, and confirm the database volume mapping in docker-compose.yml matches where you intend to keep expose.db.
Frequently asked questions
What is Expose?
Expose is an open-source tunneling service written in PHP, described in its README as an ngrok alternative. You can run the server yourself with Docker or Composer, or use the vendor's managed platform at expose.dev.
How do I use Expose in a Dockerfile?
The repository's own Dockerfile builds from php:8.1-cli, installs git, libzip-dev, zip and Composer, copies the source to /src, and runs composer install -o --prefer-dist before marking the expose binary executable. It sets port, domain, username, password and exposeConfigPath as environment variables.
Does Expose work on macOS?
The repository material does not describe a macOS-specific installation path. The documented routes are the Docker image and the Composer package, both of which run wherever Docker or PHP 8.1 is available, and the README directs you to expose.dev/docs for installation instructions.
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/exposedev-expose)