Local Development Gateway
One shared loopback-only gateway for browser-facing Docker development services.
It lets several local projects use stable hostnames instead of competing for browser ports. It is intended to run once at the beginning of a development day and remain running while projects start and stop.
What problem this solves
Without a gateway, two projects commonly both try to publish a web service on port 8080 and a backend service on port 3000. Only one can start.
With this gateway running, a project routes its internal web service through a checkout-derived hostname instead:
http://traditional-knowledge.localhost
|
v
Local Development Gateway on 127.0.0.1:80
|
v
Traditional Knowledge web container on internal port 8080
The checkout folder identifies the project. A separately named worktree such as
issue-123 uses http://issue-123.traditional-knowledge.localhost; the browser
reaches the web service through the gateway on port 80, not a published
application port.
These local *.localhost hostnames resolve to loopback and do not require a
public domain, external DNS provider, or internet-facing listener.
Start once per day
Use the small repository wrapper for the common lifecycle commands:
bin/dev up
Check the gateway:
bin/dev status
Follow its logs:
bin/dev logs
Stop it only when no participating local project needs it:
bin/dev down
The wrapper also passes unrecognized arguments directly to docker compose, so
bin/dev config and bin/dev up --force-recreate remain available.
Port 80 on 127.0.0.1 must be free before starting. The gateway never binds to a non-loopback address.
Install as a gem
Consuming projects do not need a gateway checkout. Add the released gem to their bundle and pin the compatible minor version:
gem "local-development-gateway", "~> 0.1"
Published gem: local-development-gateway
The gem packages the gateway Compose file and pinned Traefik configuration. Its executable provides the same lifecycle commands:
bundle exec local-development-gateway up
bundle exec local-development-gateway status
bundle exec local-development-gateway logs
bundle exec local-development-gateway down
local-development-gateway uses the installed asset path, the
local-gateway Compose project and network labels, and the pinned minimum Ruby
version declared by the gem. The ~> 0.1 constraint follows the consuming
project's published Gemfile.
The repository's bin/dev is only a thin wrapper around this executable API.
Ruby API lifecycle
Wrap a command that needs the gateway with the block-oriented helper:
require "local_development_gateway"
result = LocalDevelopmentGateway.with_running do
run_development_command
end
The helper starts or reuses the gateway before yielding, returns the block's
result, and conditionally stops an unused gateway afterward. Cleanup also runs
when the block raises without replacing the original exception. For a shutdown
command that must not start a missing gateway, pass
ensure_running: false; the block still runs and unused-gateway cleanup is
attempted.
Development checks
Install the locked development tools with bundle install. Run the Ruby
formatter check and tests with:
bundle exec ruby "$(bundle show syntax_tree)/exe/stree" check \
Gemfile \
Rakefile \
lib/local_development_gateway.rb \
lib/local_development_gateway/version.rb \
bin/dev \
bin/local-development-gateway \
test/local_development_gateway_test.rb
rake test
Use case: Traditional Knowledge beside WRAP
- Use the published gateway gem; it starts or reuses the shared gateway when Traditional Knowledge starts.
- Run
bundle installin Traditional Knowledge so the published gateway gem is available. - Run
dev up --watchin Traditional Knowledge. The gateway is the browser entrypoint; the base checkout useshttp://traditional-knowledge.localhost, with the API athttp://api.traditional-knowledge.localhostand MailDev athttp://mail.traditional-knowledge.localhost. - A separately named checkout or worktree derives its own hostname, such as
http://issue-123.traditional-knowledge.localhost. The API and MailDev hosts use the correspondingapi.andmail.prefixes. - Run
dev downto stop the application services. It stops the shared gateway only when no other project is attached. - When WRAP adopts the same contract, it can use its own checkout-derived hostnames through this shared gateway.
The gateway owns the single browser port on 127.0.0.1:80. Participating projects keep service ports internal to Docker and route browser traffic with explicit Traefik labels.
Contract for participating projects
The gateway:
- listens only on
127.0.0.1:80; - creates the external Docker network
local-gateway; - discovers only services explicitly labelled for routing;
- has the Docker label
local-gateway=trueso a project can detect it.
A participating project joins routed services to local-gateway and supplies a checkout-derived hostname plus explicit service labels:
services:
web:
networks:
- default
- local-gateway
labels:
- "traefik.enable=true"
- "traefik.docker.network=local-gateway"
- "traefik.http.routers.<compose-project>-web.rule=Host(`<web-hostname>`)"
- "traefik.http.routers.<compose-project>-web.entrypoints=web"
- "traefik.http.services.<compose-project>-web.loadbalancer.server.port=8080"
networks:
local-gateway:
external: true
name: local-gateway
Use a project- and service-prefixed router/service identifier. Never derive a destination port from the hostname; each routed service must name one explicit internal destination port.
Without the gateway, a participating project may use its own loopback port-publishing fallback. That fallback is project-specific and is not part of the shared gateway contract.
Authentication callbacks
A wildcard hostname lets an identity provider allow one local callback pattern instead of every generated port. For Traditional Knowledge, configure:
Allowed Callback URLs: http://*.traditional-knowledge.localhost/callback
Allowed Logout URLs: http://*.traditional-knowledge.localhost
Allowed Web Origins: http://*.traditional-knowledge.localhost
Keep existing production and staging entries. Verify that the identity provider accepts wildcard hostnames in each field before relying on this pattern.
Security boundary
The gateway publishes port 80 only to the local machine. It does not create a public endpoint.
It mounts the Docker socket so it can discover labelled containers. A read-only socket mount still exposes broad Docker metadata and control-plane access. Treat anyone who can modify this repository or its running gateway container as having significant access to local Docker. Do not run untrusted gateway images or route untrusted containers through it.
Troubleshooting
| Symptom | Check |
|---|---|
| Gateway will not start | Confirm 127.0.0.1:80 is free. |
Generated hostname returns 404 |
Confirm the web container has the route labels and is attached to local-gateway. |
| A project uses normal ports unexpectedly | Confirm the gateway container is running and has local-gateway=true. |
| Authentication rejects the callback | Confirm the generated hostname matches the allowed wildcard callback pattern. |