Sarah Ting

Setting up self-hosted Expose (ngrok)

This is a part two of Django dev docker compose — in the prior entry, I ran into an issue where Django apparently has no equivalent of Laravel’s sail share.

Since today’s Christmas, I rabbit holed a little into checking out how sail share works and whether I could spin something similar up myself.

sail share runs off of Beyond Code’s Expose. While it is written in PHP/Laravel (Zero), Expose itself is agnostic to the content it’s serving, so it’s 100% possible to set up Expose to point towards a Django project. This would be pretty similar to just setting up ngrok, aside from some difference in pricing (Expose vs ngrok).

While looking this over I decided I wanted custom subdomains, but I don’t want to pay for the pro plans of Expose/ngrok. Since Expose offers an open source option for self-hosting, I had a go at setting up my own Expose server.

Now I can use my nice personal domains for localhost forwarding, as well as not having to deal with my ngrok URL changing every time I restart 🎉 Plus I can share my server with my friends, so we have our own private ngrok server without having to pay for everyone's memberships ☺️

Set up the server

  1. For the server, I wanted to try out Vultr so I set this up on a 0.5GB RAM $2.50/mo cutest tiniest babiest server in the world, but I’ll probably move this over to one of my spare bare metals shortly.
  2. First, set up Cloudflare to point the desired domain towards the server; it will very helpfully take care of HTTPS so we don’t have to worry about it.
  3. Set up files on the server —
# docker-compose.yml
version: '3'
services:
  nginx:
    image: nginx:1.15-alpine
    ports:
      - "80:80"
    restart: always
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
  expose:
    image: beyondcodegmbh/expose-server:latest
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      port: 8080
      domain: mydomain.app
      username: ${ADMIN_USERNAME}
      password: ${ADMIN_PASSWORD}
    restart: always
    volumes:
      - ./database/expose.db:/root/.expose
# nginx/conf/expose.mydomain.app.conf
# taken from https://expose.dev/docs/server/ssl

server {
    listen        80;
    server_name   mydomain.app *.mydomain.app;

    location / {
        proxy_pass http://expose:8080;
        proxy_read_timeout     300;
        proxy_connect_timeout  300;
        proxy_redirect         off;

        # Allow the use of websockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }    
}

This sets up nginx to receive requests at port 80 and forward them to expose’s 8080. I also have additional static assets & files to replace the home page.

  1. Run the container —
ADMIN_USERNAME=admin ADMIN_PASSWORD=password docker compose up -d

Done! The server is now ready for connections, and there should have a nice admin interface at https://expose.mydomain.app/ protected by the selected admin username/password.

This is also where you can add users and generate auth tokens (read more at Expose).

Configure the client

Added the following to my codebase —

# docker-compose.yml
services:
	web:
		...
		networks:
      - expose
  ...
  expose:
    build: ./docker/expose
    extra_hosts:
      - 'host.docker.internal:host-gateway'
    profiles:
      - donotstart
    command: share web:8000 --server-host=${EXPOSE_SERVER_HOST:-sharedwithexpose.com} --server-port=${EXPOSE_SERVER_PORT:-443} --subdomain=${EXPOSE_SUBDOMAIN:-mysubdomain} --auth=${EXPOSE_TOKEN}
    networks:
      - expose
networks:
  expose:
    driver: bridge
# docker/expose/Dockerfile
FROM beyondcodegmbh/expose-server:latest
RUN sed -i "s/'dns' => '127.0.0.1'/'dns' => true/g" config/expose.php

Unfortunately there doesn’t appear to be a way to pass this configuration value in, so instead we have a sad little Dockerfile. This is required to make Expose work nicely with Docker, otherwise it will not be able to resolve the container address

# .env
EXPOSE_SERVER_HOST=mydomain.app
EXPOSE_SERVER_PORT=443
EXPOSE_SUBDOMAIN=subdomain
EXPOSE_TOKEN=<EXPOSE_TOKEN>

In this example, when you run docker compose run expose it’ll pop a subdomain up for you at https://subdomain.mydomain.app tunneled to web:8000 🚀 Cool!

I added an extra helper to my ./d helper script to shorten this to ./d expose

Maybe I can stop getting side-tracked now and get back to work on the application code!