In the world of modern web development, sharing your local work with clients or testing webhooks can be challenging. While tools like ngrok have been the go-to solution for many developers, Expose.dev offers a compelling alternative – especially for PHP developers.
Expose.dev is an elegant open-source tunneling service created by BeyondCode that allows you to generate public URLs for your local applications. What makes it particularly attractive is that you can host the entire infrastructure yourself, giving you complete control over your tunneling service.
In this comprehensive guide, I’ll walk you through setting up your own Expose server using Docker Compose and configuring clients to connect to it.
Why Host Your Own Expose Server?
Before diving into the technical setup, let’s consider why you might want to self-host Expose:
- Complete Privacy: Your traffic stays within your infrastructure
- Custom Domains: Use your own domain for all tunnels
- No Connection Limits: Set your own rules for connection duration and bandwidths
While BeyondCode offers a managed Expose Pro service, self-hosting gives you maximum flexibility and control.
Server Setup with Docker Compose
Setting up your own Expose server is surprisingly straightforward with Docker Compose. Here’s everything you need to get started.
Prerequisites
- A server with Docker and Docker Compose installed
- A domain name pointing to your server (for proper SSL)
- Basic knowledge of Docker and networking
Step 1: Create Your Project Structure
First, let’s create a dedicated directory for our Expose server:
1 2 3 4 |
mkdir expose-server cd expose-server |
Step 2: Set Up Your Docker Compose Configuration
Now, let’s create the Docker Compose configuration file that will define our Expose server. Create a docker-compose.yml
file with the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
version: "3.7" services: expose: image: beyondcodegmbh/expose-server:latest extra_hosts: - "host.docker.internal:host-gateway" ports: - 8080:${PORT} environment: port: ${PORT} domain: ${DOMAIN} username: ${ADMIN_USERNAME} password: ${ADMIN_PASSWORD} restart: always volumes: - ./database/expose.db:/root/.expose |
This configuration:
- Uses the official Expose server image
- Maps the container’s port to your host’s port 8080
- Sets up environment variables for configuration
- Persists the database to keep your settings and authentication tokens
- Automatically restarts the container if it crashes or if your server reboots
Step 3: Configure Your Environment Variables
Expose needs a few key configurations to run properly. Create a .env
file with your specific settings:
1 2 3 4 5 6 |
PORT=8080 DOMAIN=yourdomain.com ADMIN_USERNAME=yourusername ADMIN_PASSWORD=yourpassword |
Make sure to replace:
yourdomain.com
with your actual domain nameyourusername
andyourpassword
with secure credentials for accessing the admin interface
These environment variables are crucial:
PORT
: The internal port the Expose server will run onDOMAIN
: Your domain name, which will be used for all tunneled URLsADMIN_USERNAME
andADMIN_PASSWORD
: Credentials to access the admin dashboard
Step 4: Create the Database Directory
Expose stores its data in an SQLite database. Let’s create a directory for this:
1 2 3 |
mkdir -p database |
This directory will store user tokens, connection data, and other persistent information.
Step 5: Launch Your Expose Server
Now that everything is set up, it’s time to start your Expose server:
1 2 3 |
docker-compose up -d |
This command starts the Expose server in detached mode, meaning it will run in the background. You can check its status with:
1 2 3 |
docker-compose ps |
If everything is working correctly, you should see your Expose container running. The default admin can be found under – expose.yourdomain.tld.
Step 6: Setting Up a Reverse Proxy (Recommended)
For maximum security and accessibility, I strongly recommend setting up a reverse proxy with SSL in front of your Expose server. This allows:
- Secure HTTPS connections
- Running on standard port 443, which helps bypass most firewalls
- Proper WebSocket support for real-time updates
Here’s a sample Nginx configuration to get you started:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
server { listen 443 ssl; server_name yourdomain.com; # SSL configuration ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # Modern SSL settings ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_session_cache shared:SSL:10m; location / { proxy_pass http://localhost:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; 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 $scheme; proxy_cache_bypass $http_upgrade; } } |
If you’re using Let’s Encrypt for SSL certificates, you can automate certificate renewal with Certbot. For even more streamlined setup, consider using Traefik as your reverse proxy, which can automatically generate and renew certificates.
Client Configuration: Connecting to Your Custom Server
Once your server is up and running, the next step is configuring your development machines to connect to it. Let’s walk through this process step-by-step.
Step 1: Installing the Expose Client
First, you’ll need to install the Expose client on your development machine. There are two primary installation methods to choose from:
Option 1: Via PHAR file (Recommended for most users)
This method works on any machine with PHP installed and doesn’t require Composer:
1 2 3 4 5 6 7 8 9 10 |
# Download the Expose executable curl https://github.com/exposedev/expose/raw/master/builds/expose -L --output expose # Make it executable chmod +x expose # Move it to a directory in your PATH for easy access sudo mv expose /usr/local/bin/expose |
Option 2: Via Composer
If you’re a PHP developer and already use Composer, this method integrates well with your existing workflow:
1 2 3 |
composer global require exposedev/expose |
Make sure your global Composer bin directory is in your PATH to access the expose command from anywhere.
If you’re using Laravel Herd, Expose is already included and ready to use.
Step 2: Creating and Publishing Your Configuration File
Expose uses a PHP configuration file to store your settings and server information. To generate this file:
1 2 3 |
expose publish |
This command creates a default configuration file at ~/.expose/config.php
. This file will be the foundation for connecting to your custom server.
You should see a success message showing the path where your configuration file was created. If you’re using a different shell or operating system, the location might vary slightly.
Step 3: Configuring Your Client to Use Your Custom Server
Now comes the crucial part – configuring your client to connect to your self-hosted Expose server instead of the default one.
Open the configuration file in your preferred text editor:
1 2 3 |
nano ~/.expose/config.php |
The configuration file contains a ‘servers’ array where you can define multiple server configurations. You’ll need to modify this array to include your custom server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
return [ // ... other configuration options 'servers' => [ // Keep the default server as a fallback option 'main' => [ 'host' => 'sharedwithexpose.com', 'port' => 443, ], // Add your custom server 'custom-server' => [ 'host' => 'yourdomain.com', 'port' => 8080, // Or 443 if using a reverse proxy with SSL ], ], // Set your custom server as the default (optional but recommended) 'default_server' => 'custom-server', // ... other configuration options ]; |
This configuration does two important things:
- It defines your custom server with its domain and port
- It sets your custom server as the default, so you don’t have to specify it every time
Step 4: Setting Up Authentication (Recommended)
For added security, I recommend setting up authentication for your Expose server. This prevents unauthorized users from using your tunneling infrastructure.
Here’s how to implement token-based authentication:
- Enable Authentication on the Server:
- Access the admin interface at
https://yourdomain.com
using the credentials from your .env file - Navigate to the Settings section
- Find and enable the “Validate Auth Tokens” option
- Create a new user token in the admin interface (note it down for client configuration)
- Access the admin interface at
- Configure Your Client with the Token:
The easiest way to set your authentication token is using the built-in command:
1 2 3 |
expose token YOUR-AUTH-TOKEN |
This automatically updates your configuration file.
Alternatively, you can manually edit the config file to add the token:
1 2 3 |
'auth_token' => 'YOUR-AUTH-TOKEN', |
With authentication enabled, only clients with valid tokens can create tunnels through your server, giving you complete control over who can use your infrastructure.
Using Your Self-Hosted Expose Server
Now that you’ve set up both the server and client sides, it’s time to put your custom Expose server to work. Let’s explore the various ways to share your local applications.
Basic Sharing Commands
Here are some common patterns for sharing your sites:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Share a local site running on port 8000 (uses your default server) expose share http://localhost:8000 # Share with a specific subdomain (useful for consistent links) expose share http://localhost:8000 --subdomain=myapp # Share a specific local domain (works with Valet or custom hosts) expose share myapp.test # Share using a specific server from your config (if you have multiple) expose share http://localhost:8000 --server=custom-server # Combine options for maximum flexibility expose share myapp.test --server=custom-server --subdomain=myapp |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
expose share http://localhost --subdomain=localhost --basicAuth="***:***" -v > Expose Expose version v3.0.3 Using auth token: ****** Using basic auth: ***:***** Trying to use custom subdomain localhost Using SQLite database: /private/var/folders/* Thank you for using expose. Shared site localhost Dashboard http://127.0.0.1:4040 Public URL https://localhost.yourdomain.tld |
Working with Multiple Server Configurations
One of the powerful features of a self-hosted Expose setup is the ability to work with different server configurations. Here’s how to make the most of this capability:
1. Using Your Default Server
If you’ve set the default_server
option in your config file, you can simply run Expose without specifying a server:
1 2 3 |
expose share myapp.test |
This uses the server defined in the ‘default_server’ setting, making the command simpler for your most common use case.
2. Specifying a Server Explicitly
When you need to use a different server than your default:
1 2 3 |
expose share myapp.test --server=custom-server |
The server name (custom-server
in this example) must match a key in the ‘servers’ array in your config file.
3. Project-Specific Configurations
For teams working on different projects that might need different Expose configurations:
1 2 3 |
EXPOSE_CONFIG_FILE="/path/to/project-specific-config.php" expose share myapp.test |
This allows you to maintain separate configurations for different projects or clients.
Advanced Configuration Techniques
As you become more comfortable with your self-hosted Expose setup, you may want to explore more advanced configuration options. Here’s how to fine-tune both the server and client sides.
Enhanced Server Security
Implementing Token-Based Authentication
For team environments, token-based authentication is essential:
- Access the admin interface at your domain
- Navigate to Settings
- Enable “Validate Authentication Tokens”
- Create distinct tokens for different team members or projects
This lets you track which team members are using the tunneling service and revoke access when needed.
Custom Subdomain Generation
You can implement custom subdomain patterns by modifying the server’s configuration:
- Edit the
config/expose.php
file inside the Docker container or mount a custom config file - Change the
subdomain_generator
configuration to implement your own logic - This allows for branded subdomains or meaningful naming patterns
Data Management
Customizing Database Storage
The default SQLite database configuration works well for most deployments, but you can customize it:
1 2 3 4 |
volumes: - ./path/to/custom/database:/root/.expose |
This lets you store the database in a different location, which might be useful for backup strategies or when using networked storage.
Request Logging Performance Tuning
For high-traffic scenarios, tune the request logging settings to prevent memory issues:
1 2 3 4 5 6 7 8 9 10 11 |
// In your client config file 'max_logged_requests' => 50, // Reduce from default 100 'skip_body_log' => [ 'status' => ["4*", "5*"], // Skip error responses 'content_type' => ["text/css", "application/javascript"], 'extension' => ['.js.map', '.css.map', '.min.js', '.min.css'], 'size' => '500KB', // Lower threshold for large responses ], |
Advanced Client Options
The client configuration file offers numerous options to customize your experience:
1 2 3 4 5 6 7 8 9 10 11 |
// Fine-tune memory allocation 'memory_limit' => '256M', // Increase for complex applications // Set default protocols and domains 'default_tld' => 'local', // Use .local instead of .test 'default_https' => true, // Always use HTTPS locally // Configure DNS resolution 'dns' => '1.1.1.1', // Use Cloudflare DNS instead of local |
Troubleshooting Common Issues
Even with the most careful setup, you might encounter some issues with your self-hosted Expose environment. Here are solutions to the most common problems developers face.
Connection Problems
Cannot Connect to Server
If your client can’t connect to your Expose server:
- Domain Resolution: Verify your domain is properly pointing to your server’s IP address
- Firewall Settings: Ensure your server’s firewall allows connections on the Expose port
- Client Configuration: Double-check the host and port in your client config match your server
- Network Rules: Some corporate networks block WebSocket connections; try using port 443 with SSL
Try testing connectivity with a simple curl command:
1 2 3 |
curl -vI https://yourdomain.com |
Subdomain Already Taken
If you get an error about your subdomain being already in use:
- Someone else is using that subdomain on your server
- A previous connection might not have been properly terminated
- Check the admin interface to see active connections and potentially terminate stale ones
SSL Configuration Issues
Certificate Errors
When facing SSL certificate problems:
- Ensure your SSL certificates are valid and not expired
- The certificate must match the domain exactly
- If using Let’s Encrypt, verify the renewal process is working
WebSocket Connection Failures
For WebSocket connection issues:
- Verify your reverse proxy is properly configured for WebSocket upgrading
- Check the
Upgrade
andConnection
headers are being properly forwarded - Test with a WebSocket client like wscat to isolate the issue
Authentication Problems
Token Not Recognized
If your authentication token isn’t working:
- Verify the token is correctly set in your client configuration
- Check that the token exists in the server’s database
- The server might need to be restarted after adding new tokens
- Ensure “Validate Authentication Tokens” is enabled in the server settings
A simple test is to use the token command and then check your config file:
1 2 3 4 |
expose token YOUR-AUTH-TOKEN cat ~/.expose/config.php | grep auth_token |
Additional Resources
For further exploration and assistance:
- Official Expose Documentation – Complete reference for all Expose features
- Expose GitHub Repository – Source code and issue tracking
- BeyondCode Blog – Occasional articles about Expose
- Docker Documentation – For deeper Docker configuration
- Nginx WebSocket Proxying – Official guide on WebSocket configuration
- Let’s Encrypt Documentation – Free SSL certificate setup
Thoughts
Setting up your own Expose server using Docker Compose provides a powerful, customizable alternative to services like ngrok. With complete control over your tunneling infrastructure, you can ensure privacy, customize domains, and manage team access as needed.
While the initial setup requires some technical knowledge, the benefits of self-hosting – especially for teams – make it well worth the effort. As your development workflows evolve, you can continue to refine your Expose configuration to match your specific needs.
For developers who prefer a managed solution with many of the same benefits, consider Expose Pro, which offers a globally distributed network without the maintenance overhead.
Whether you choose the self-hosted route or the managed service, Expose represents a PHP-native, developer-friendly approach to solving the local tunnel challenge.