arrow_backAll Posts·5 Min Read·2025-10-7

Expose Local Apps with Traefik and Ngrok in Docker

Networking

When you build a homelab on a local VM, you may eventually want to share a project with teammates or friends. This guide combines Traefik and Ngrok to expose selected local services through a controlled public URL.

Traefik is a reverse proxy, load balancer, and API gateway. Think of it as a "gatekeeper": when a request arrives, Traefik reads the routing rules and forwards the request to the correct application.

For example, requests to /prome can be routed to Prometheus on port 9090. The client only needs to access the Traefik port, such as http://localhost:8000/prome.

The sections below explain both tools, including their strengths and trade-offs.

Traefik is a modern reverse proxy and load balancer designed for deploying microservices. It discovers configuration dynamically from providers such as Docker and Kubernetes by inspecting metadata such as Docker Compose labels in real time.

KeuntunganPenjelasan
Automatic configuration (auto-discovery)No need to restart Traefik whenever a container is added or removed; providers such as Docker labels expose the new service automatically.
Automatic SSL certificatesBuilt-in Let's Encrypt integration can obtain and renew SSL/TLS certificates for your domains.
Well suited to microservicesIts dynamic design integrates closely with Docker, Swarm, and Kubernetes.
API and web dashboardProvides a visual dashboard for routers, services, and middleware.
Flexible middlewareAdd rate limiting, basic authentication, header manipulation, and prefix stripping.
KekuranganPenjelasan
Initial learning curveRouters, services, middleware, and entrypoints can be unfamiliar to engineers used to static Nginx configuration.
Initial TLS complexityLet's Encrypt setup, especially with a DNS challenge, can require additional configuration.
Provider dependencyTraefik relies on provider metadata such as Docker labels; invalid metadata can prevent a service from working.

Ngrok provides reverse proxy and tunneling capabilities that make a local service reachable from the internet. On the free tier, one public URL can be used to expose a single service at a time.

Anyone other than the laptop owner reaches the local website through the configured public Ngrok URL. The owner can still access the service locally or through the public endpoint.

KeuntunganPenjelasan
Instant local exposureExpose a local web service without configuring router port forwarding.
Useful for testing and demosTest webhooks from services such as PayPal or Stripe, or demonstrate an application to a client.
Secure tunnelingNgrok provides a TLS-encrypted HTTPS tunnel even when the local service uses HTTP.
Traffic inspectionA local web UI makes incoming and outgoing HTTP requests easy to inspect during debugging.
KekuranganPenjelasan
Temporary free URLThe free tier assigns a public URL that changes when the tunnel restarts.
Free-tier connection and bandwidth limitsRate and session limits make the free tier unsuitable for sustained production use.
Not ideal for long-term productionEven with a custom domain, Ngrok is an external dependency and is rarely the primary reverse proxy for highly available production services.
Third-party dependencyIf Ngrok's cloud service is unavailable, the local service cannot be reached externally.

  • Minimum RAM: 9 GB (16 GB recommended)
  • CPU: 4 cores
  • Storage: 35 GB

This example runs Prometheus in Docker. Start by creating docker-compose.yml.

bash
nano docker-compose.yml
  • Add the following Prometheus service configuration.
    yaml
    services:
      prometheus:
        image: prom/prometheus:latest
        container_name: prometheus
        ports:
          - "9090:9090"
        command:
          - '--config.file=/etc/prometheus/prometheus.yml'
          - '--web.route-prefix=/'
        volumes:
          - ./prome/prometheus.yaml:/etc/prometheus/prometheus.yml:ro
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.prom.rule=PathPrefix(`/prome`)"
          - "traefik.http.routers.prom.entrypoints=web"
          - "traefik.http.middlewares.prom-strip.stripprefix.prefixes=/prome"
          - "traefik.http.routers.prom.middlewares=prom-strip"
          - "traefik.http.services.prom.loadbalancer.server.port=9090"
        networks:
          - monitoring
    

Configuration Notes

BagianBaris KonfigurasiPenjelasan SingkatDetail Teknis
command--config.file=/etc/prometheus/prometheus.ymlPoints Prometheus to a specific configuration file.Tells Prometheus where to find prometheus.yml inside the container.
--web.route-prefix=/Sets the URL prefix for the Prometheus web interface.Keeps the interface at the service root path (/); Traefik handles the external prefix through its labels.
volumes./prome/prometheus.yaml:/etc/prometheus/prometheus.yml:roMounts the local configuration file into the container.The local prometheus.yaml file is mounted read-only, so Prometheus cannot modify it.
labels"traefik.enable=true"Enables Traefik for the service.Traefik reads this and the remaining labels for the container.
traefik.http.routers.prom.rule=PathPrefix("/prome")Defines URL-based routing.Requests beginning with /prome are handled by this router.
"traefik.http.routers.prom.entrypoints=web"Selects the router entrypoint.The router listens on the web entrypoint, usually HTTP port 80.
"traefik.http.middlewares.prom-strip.stripprefix.prefixes=/prome"Defines a prefix-stripping middleware.The prom-strip middleware removes /prome before forwarding the request to Prometheus.
"traefik.http.routers.prom.middlewares=prom-strip"Attaches the middleware to the router.A request to /prome/graph is forwarded to /graph in Prometheus.
"traefik.http.services.prom.loadbalancer.server.port=9090"Defines the internal service port.Traefik forwards requests to Prometheus on port 9090 inside the container.
  • Create the Prometheus configuration at ./prome/prometheus.yaml.

    bash
    mkdir prome
    cd prome
    nano prometheus.yaml
    
  • Use the following scrape configuration. This example uses Prometheus as the only target and does not yet integrate additional exporters.

    yaml
    global:
      scrape_interval: 15s
    
    scrape_configs:
      - job_name: "prometheus"
        static_configs:
          - targets: ["localhost:9090"]
    
  • Open docker-compose.yml again.

    bash
    nano docker-compose.yml
    
  • Add the following Grafana service configuration.

    yaml
    services:
      grafana:
        image: grafana/grafana:latest
        container_name: grafana
        ports:
          - "3000:3000"
        environment:
          - GF_SERVER_ROOT_URL=http://172.1.1.12:3000/grafana/
          - GF_SERVER_SERVE_FROM_SUB_PATH=true
        volumes:
          - grafana-storage:/var/lib/grafana
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.grafana.rule=PathPrefix(`/grafana`)"
          - "traefik.http.services.grafana.loadbalancer.server.port=3000"
          - "traefik.http.routers.grafana.entrypoints=web"
        networks:
          - monitoring
    

Configuration Notes

BagianBaris KonfigurasiPenjelasan SingkatDetail Teknis
environmentGF_SERVER_ROOT_URL=http://172.1.1.12:3000/grafana/Sets Grafana's public base URL.Tells Grafana how to generate internal links for the /grafana/ subpath and port 3000.
GF_SERVER_SERVE_FROM_SUB_PATH=trueEnables subpath hosting.Required when Grafana runs behind a reverse proxy at a path such as /grafana; it keeps CSS and JavaScript assets working.
volumesgrafana-storage:/var/lib/grafanaPersists Grafana data.Stores the database, dashboards, and related data in the grafana-storage Docker volume.
labels"traefik.enable=true"Enables Traefik for Grafana.Traefik reads the remaining labels to configure routing.
"traefik.http.routers.grafana.rule=PathPrefix(\/grafana`)"`Defines URL-based routing.Requests beginning with /grafana are sent to the Grafana service.
"traefik.http.services.grafana.loadbalancer.server.port=3000"Defines the internal Grafana port.Traefik forwards traffic to port 3000 inside the container.
"traefik.http.routers.grafana.entrypoints=web"Selects the router entrypoint.The router listens on the web entrypoint, typically HTTP port 80 or 8080.
  • Open docker-compose.yml again.

    bash
    nano docker-compose.yml
    
  • Add the Traefik service together with the network and Grafana storage configuration.

    yaml
    services:
    traefik:
      image: traefik:v3.1
      container_name: traefik
      command:
        - "--entrypoints.web.address=:9000"
        - "--providers.docker=true"
        - "--api.dashboard=true"
        - "--log.level=info"
        - "--api.insecure=true"
      ports:
        - "9000:9000"        # main Prometheus and Grafana endpoint
      volumes:
        - /var/run/docker.sock:/var/run/docker.sock
      networks:
        - monitoring
    
    volumes:
      grafana-storage:
    
    networks:
      monitoring:
        driver: bridge
    
  • Start the three services.

    bash
    docker-compose up -d
    
  • Wait for the services to start. If a container exits, inspect its logs with:

    bash
    docker logs {SERVICE_NAME}
    
    # exp:
    docker logs prome
    docker logs traefik
    docker logs grafana
    

Docker Compose Configuration Notes

BagianBaris KonfigurasiPenjelasan SingkatDetail Teknis
command--entrypoints.web.address=:9000Defines the HTTP entrypoint listened to by Traefik.Creates a web entrypoint on port 9000, used by the Prometheus and Grafana routers.
--providers.docker=trueEnables Docker provider auto-discovery.Traefik watches the Docker socket and reads labels from running containers.
--api.dashboard=trueEnables the Traefik API and dashboard.Exposes the interactive dashboard for monitoring routers and services.
--log.level=infoSets the logging level.Records INFO, warning, and error messages for troubleshooting.
--api.insecure=trueAllows unauthenticated dashboard access.Use only for local development. Do not expose this setting on an untrusted network.
ports9000:9000Publishes the Traefik port on the host.Maps port 9000 in the container to port 9000 on the host.
volumes/var/run/docker.sock:/var/run/docker.sockGives Traefik access to the Docker socket.Required for Docker provider discovery and label inspection.
  • Create an Ngrok account if you do not already have one, then sign in.

  • Open the VM or homelab you want to expose publicly.

  • Add the Ngrok repository and install the client. The commands below target Ubuntu; adjust them for your distribution.

    bash
    curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
        | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null \
        && echo "deb https://ngrok-agent.s3.amazonaws.com bookworm main" \
        | sudo tee /etc/apt/sources.list.d/ngrok.list \
        && sudo apt update \
        && sudo apt install ngrok jq -y
    
  • Add the authentication token shown in the Ngrok dashboard.

    bash
    ngrok config add-authtoken {AUTH_TOKEN_NGROK}
    
  • Run Ngrok as a background service instead of starting ngrok http {PORT} manually.

    bash
    nano /etc/systemd/system/ngrok.service
    
  • Add the following service definition and replace {PORT_TRAEFIK} with the Traefik port. This example uses port 9000.

    bash
    [Unit]
    Description=Ngrok Tunnel
    After=network.target
    
    [Service]
    ExecStart=/usr/local/bin/ngrok http 9000 --log=stdout
    Restart=on-failure
    User=root
    StandardOutput=file:/var/log/ngrok.log
    StandardError=file:/var/log/ngrok_error.log
    
    [Install]
    WantedBy=multi-user.target
    
  • Reload systemd, then start and enable Ngrok.

    bash
    sudo systemctl daemon-reload
    sudo systemctl start ngrok
    sudo systemctl enable ngrok
    

From a laptop browser, validate Prometheus and Grafana at https://{NGROK_URL}/prome and https://{NGROK_URL}/grafana.

Prometheus

Grafana