Self-hosted reverse proxy tunneling solution
Octoporty is a self-hosted alternative to ngrok that lets you expose internal services through a public endpoint. Deploy the Gateway on a cloud server with a public IP, run the Agent inside your private network, and securely tunnel traffic to your internal services.
- Features
- Architecture
- Quick Start
- Installation
- Updating
- Configuration
- Web Interface
- Development
- API Reference
- Security
- Troubleshooting
- Contributing
- License
- Self-Hosted - Full control over your infrastructure and data
- WebSocket Tunnel - Efficient binary protocol with MessagePack serialization and Lz4 compression
- WebSocket Proxy - End-to-end WebSocket forwarding for real-time applications (chat, live dashboards, hot reload)
- Automatic HTTPS - Caddy integration provides automatic TLS certificates via Let's Encrypt
- Web Management UI - React-based dashboard for managing port mappings
- Multi-Domain Support - Route multiple domains to different internal services
- Automatic Reconnection - Agent maintains persistent connection with exponential backoff
- Gateway Self-Update - Update the Gateway from the Agent UI when version mismatch is detected
- Customizable Landing Page - Edit the HTML shown when no mapping exists for a domain
- Agent Logs - Real-time streaming of Agent process logs in the Web UI with historical retrieval
- Request Logging - Audit trail for all tunneled requests
- Rate Limiting - Built-in protection against brute force attacks
- Startup Banner - Visual configuration display at startup with obfuscated secrets for easy verification
- Import/Export - Export and import port mapping definitions as JSON for backup, migration, or sharing between Agents
- SQLite Backup - Download a consistent SQLite database backup from the Web UI
- Automatic Database Setup - Agent auto-applies migrations on startup, no manual database setup required
- Docker Ready - Multi-arch container images (amd64/arm64) with minimal attack surface
┌─────────────────────────────────────────────────────────────────────────────┐
│ INTERNET │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLOUD / PUBLIC SERVER │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Caddy │──────▶│ Octoporty Gateway │ │
│ │ (HTTPS/TLS) │ │ (WebSocket Hub) │ │
│ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
WebSocket Tunnel
(MessagePack + Lz4)
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PRIVATE NETWORK │
│ ┌─────────────────────┐ ┌─────────────────────────────────────────┐ │
│ │ Octoporty Agent │──────▶│ Internal Services │ │
│ │ (Tunnel Client + │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ Web UI @ :17201) │ │ │ Web App │ │ API │ │ Database│ │ │
│ └─────────────────────┘ │ │ :3000 │ │ :8080 │ │ :5432 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
| Component | Description |
|---|---|
| Octoporty.Gateway | Cloud-deployed WebSocket server that receives external traffic and routes to connected Agents |
| Octoporty.Agent | Runs inside private network, maintains tunnel to Gateway, forwards requests to internal services |
| Octoporty.Agent.Web | React SPA for managing port mappings (embedded in Agent) |
| Octoporty.Shared | Shared entities, contracts, options, and logging extensions |
The tunnel uses WebSocket with MessagePack binary serialization and Lz4 compression for efficient data transfer.
Message Flow:
- Authentication - Agent connects and authenticates with pre-shared API key (includes WebSocket proxy capability negotiation)
- Configuration Sync - Agent sends its port mapping configuration to Gateway
- Heartbeat Loop - Maintains connection health with periodic pings
- Request/Response - Gateway forwards incoming HTTP requests through the tunnel
- WebSocket Proxy - Gateway detects WebSocket upgrade requests and relays frames bidirectionally through the tunnel to the internal service
The fastest way to get started is with Docker Compose:
# Clone the repository
git clone https://github.com/aduggleby/octoporty.git
cd octoporty
# Copy and configure environment
cp .env.example .env
# Edit .env with your settings
# Start the development environment
docker compose -f infrastructure/docker-compose.dev.yml up --buildAccess the Agent web UI at http://localhost:17201
Gateway Deployment:
# docker-compose.gateway.yml
services:
gateway:
image: ghcr.io/aduggleby/octoporty-gateway:latest
environment:
- Gateway__ApiKey=your-secure-api-key-min-32-chars
- Gateway__CaddyAdminUrl=http://caddy:2019
ports:
- "17200:17200"
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
volumes:
caddy_data:Agent Deployment:
# docker-compose.agent.yml
services:
agent:
image: ghcr.io/aduggleby/octoporty-agent:latest
environment:
- Agent__GatewayFqdn=your-gateway-domain.com
- Agent__ApiKey=your-secure-api-key-min-32-chars
- Agent__JwtSecret=your-jwt-secret-min-32-chars
# Generate hash with: openssl passwd -6 "your-password"
- Agent__Auth__PasswordHash=$6$rounds=5000$yoursalt$yourhash
ports:
- "17201:17201"
volumes:
- agent_data:/app/data
volumes:
agent_data:Prerequisites:
- .NET 10 SDK
- Node.js 20+ (for frontend build)
Build from source:
# Clone repository
git clone https://github.com/aduggleby/octoporty.git
cd octoporty
# Build all .NET projects
dotnet build
# Build frontend (outputs to Agent's wwwroot)
cd src/Octoporty.Agent.Web
npm install
npm run build
cd ../..
# Run Gateway
dotnet run --project src/Octoporty.Gateway
# Run Agent (in separate terminal)
dotnet run --project src/Octoporty.AgentUpdate to the latest version with a single command:
Gateway:
curl -fsSL https://octoporty.com/update-gateway.sh | bashAgent:
curl -fsSL https://octoporty.com/update-agent.sh | bashIf you prefer to update manually:
Gateway:
cd /opt/octoporty/gateway
# Pull latest images
docker compose pull
# Restart services
docker compose down
docker compose up -d
# Verify the update
docker compose logs -f gatewayAgent:
cd /opt/octoporty/agent
# Pull latest images
docker compose pull
# Restart services
docker compose down
docker compose up -d
# Verify the update
docker compose logs -f agent# Check Gateway version
docker inspect ghcr.io/aduggleby/octoporty-gateway:latest --format='{{index .Config.Labels "org.opencontainers.image.version"}}'
# Check Agent version
docker inspect ghcr.io/aduggleby/octoporty-agent:latest --format='{{index .Config.Labels "org.opencontainers.image.version"}}'When you update the Agent to a newer version, it can detect that the Gateway is running an older version. The Agent UI will display a notification banner with an "Update Gateway" button.
How it works:
- Update the Agent first (using the methods above)
- When the Agent connects to the Gateway, it compares versions
- If the Agent is newer, a yellow banner appears in the Agent UI
- Click "Update Gateway" to trigger a remote update
- A status modal appears showing real-time update progress, polling the Gateway every 5 seconds until the new version is confirmed
- The Gateway writes a signal file that the host watcher monitors
- Within 30 seconds, the Gateway is automatically pulled and restarted, and the modal auto-closes
Installing the Auto-Updater:
If your Gateway was installed before the auto-updater was included, you can add it to an existing installation:
curl -fsSL https://octoporty.com/install-updater.sh | sudo bashThis installs a systemd timer that checks for update signal files every 30 seconds. New Gateway installations include the auto-updater automatically.
Configuration:
| Variable | Description | Default |
|---|---|---|
Gateway__AllowRemoteUpdate |
Enable/disable remote update requests | true |
Gateway__UpdateSignalPath |
Path to the update signal file | /data/update-signal |
Security: Remote updates are only accepted over authenticated WebSocket connections (API key validated). The update signal file is only writable by the Gateway container, and the host watcher runs separately with access to Docker.
| Variable | Description | Default |
|---|---|---|
Gateway__ApiKey |
Pre-shared key for Agent authentication (min 32 chars) | Required |
Gateway__CaddyAdminUrl |
Caddy Admin API endpoint | http://localhost:2019 |
Gateway__Port |
Gateway listening port | 17200 |
Gateway__AllowRemoteUpdate |
Allow Agents to trigger Gateway self-updates | true |
Gateway__UpdateSignalPath |
Path for update signal file | /data/update-signal |
Gateway__RemoveRoutesOnTunnelUnavailable |
Remove Caddy routes when the tunnel is unavailable. Disabled by default to avoid Caddy reloads dropping long-lived connections (e.g., when the Agent tunnels through Caddy). | false |
| Variable | Description | Default |
|---|---|---|
Agent__GatewayFqdn |
Gateway domain (e.g., gateway.example.com). Recommended over GatewayUrl. |
- |
Agent__GatewayUrl |
WebSocket URL to Gateway. If not set, derived from GatewayFqdn. |
Derived |
Agent__ApiKey |
Pre-shared key matching Gateway | Required |
Agent__JwtSecret |
JWT signing key (min 32 chars) | Required |
Agent__Auth__PasswordHash |
SHA-512 crypt hash for Web UI login (generate with openssl passwd -6) |
Required |
Agent__Port |
Agent web UI port | 17201 |
Logging__FilePath |
Rolling log file path for Agent log streaming | /var/log/octoporty/agent-.log |
ConnectionStrings__DefaultConnection |
Database connection string | SQLite at /app/data/octoporty.db |
Octoporty uses port range 17200-17299:
| Port | Service |
|---|---|
| 17200 | Gateway |
| 17201 | Agent Web UI |
| 17202 | Caddy Admin API |
| 17280 | Caddy HTTP |
| 17243 | Caddy HTTPS |
The Agent includes an embedded React web application for managing port mappings.
Dashboard - Overview of tunnel status and gateway information
Mappings - View and manage port mappings
- Dashboard - Overview of tunnel status and active mappings
- Port Mappings - Create, edit, and delete domain-to-service mappings
- Request Inspector - Debug tunnel routing by comparing Gateway vs Agent responses for any URL, with timing and header analysis
- Settings - Customize the Gateway landing page with your own HTML and branding
- Gateway Logs - Real-time streaming of Gateway logs with historical log retrieval and infinite scroll
- Gateway Log Detail - Detailed log inspection view with search, level filtering, and full message payload display for debugging tunnel issues
- Agent Logs - Real-time streaming of Agent process logs with level filtering, auto-scroll, and infinite scroll for historical entries
- Import/Export - Export and import port mapping definitions as JSON, or download a full SQLite database backup
- Caddy Configuration - View the current Caddy reverse proxy configuration for debugging and monitoring
- Connection Logs - View connection history and status
- Request Logs - Audit trail of all tunneled requests
- Navigate to Mappings in the sidebar
- Click Create Mapping
- Enter the external domain (e.g.,
app.yourdomain.com) - Enter the internal host and port (e.g.,
localhost:3000) - Configure TLS options if needed
- Click Save
The Gateway will automatically configure Caddy to route traffic for the domain through the tunnel.
octoporty/
├── src/
│ ├── Octoporty.Gateway/ # Gateway service
│ ├── Octoporty.Agent/ # Agent service + API
│ ├── Octoporty.Agent.Web/ # React frontend
│ └── Octoporty.Shared/ # Shared library
├── tests/
│ └── Octoporty.Tests.E2E/ # End-to-end tests
├── infrastructure/
│ ├── docker-compose.yml # Production compose
│ ├── docker-compose.dev.yml # Development compose
│ └── Caddyfile # Caddy configuration
└── CLAUDE.md # AI assistant instructions
# Run all E2E tests
cd tests/Octoporty.Tests.E2E
dotnet test
# Run specific test category
dotnet test --filter "FullyQualifiedName~MappingsApi"
dotnet test --filter "FullyQualifiedName~ComprehensiveUi"Backend:
- .NET 10
- FastEndpoints (API framework)
- Entity Framework Core (SQLite)
- SignalR (WebSocket management)
- MessagePack (binary serialization)
- Serilog (structured logging)
Frontend:
- React 19
- TypeScript
- Tailwind CSS 4
- Vite (build tool)
- Motion (animations)
Infrastructure:
- Docker (multi-arch images for amd64/arm64)
- Caddy (reverse proxy + auto HTTPS)
- GitHub Container Registry
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/mappings |
List all port mappings |
| GET | /api/mappings/{id} |
Get mapping by ID |
| POST | /api/mappings |
Create new mapping |
| PUT | /api/mappings/{id} |
Update mapping |
| DELETE | /api/mappings/{id} |
Delete mapping |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/settings/landing-page |
Get current landing page HTML and hash |
| PUT | /api/v1/settings/landing-page |
Update landing page HTML (max 1MB) |
| DELETE | /api/v1/settings/landing-page |
Reset to default Octoporty landing page |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/gateway/caddy-config |
Get current Caddy reverse proxy configuration from the Gateway |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/agent/logs |
Get Agent process logs with pagination (beforeId, count query params) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/import-export/export |
Export port mappings and landing page as JSON |
| POST | /api/v1/import-export/import |
Import port mappings from JSON (merge-only: upserts by domain, does not delete) |
| GET | /api/v1/import-export/sqlite |
Download a consistent SQLite database backup |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/test/diagnose |
Compare Gateway vs Agent responses for a URL to debug routing issues |
The Agent Web UI uses JWT authentication with HttpOnly cookies. Login via:
POST /api/v1/auth/login
Content-Type: application/json
{
"password": "your-password"
}
The password is verified against the SHA-512 crypt hash configured in Agent__Auth__PasswordHash.
- Agent-Gateway: Pre-shared API key with constant-time comparison to prevent timing attacks
- Web UI: JWT with HttpOnly cookies, refresh tokens stored in memory
- Rate Limiting: Login endpoint with exponential backoff lockout (1min, 5min, 15min, 1hr)
- All external traffic should go through Caddy with TLS
- WebSocket tunnel uses secure WebSocket (WSS) in production
- Internal services are never directly exposed to the internet
- Use strong, unique API keys (minimum 32 characters)
- Enable TLS for all external connections
- Regularly rotate credentials
- Monitor request logs for suspicious activity
- Keep all components updated
- Verify
Agent__GatewayUrlis correct and reachable - Check that API keys match on both sides
- Ensure Gateway is running and healthy
- Check firewall rules allow WebSocket connections
- Use the Request Inspector (under Gateway in the sidebar) to compare Gateway vs Agent responses
- Verify port mapping configuration
- Check internal service is running and accessible from Agent
- Review Agent logs for forwarding errors
- Ensure internal host is resolvable from Agent container
If you see an error like Cannot determine the frame size or corrupted frame, the mapping is likely set to HTTPS but the upstream service is speaking plain HTTP (or the port is wrong). Change the mapping's internal protocol to HTTP or fix the port.
- Verify password hash is set correctly (generate with
openssl passwd -6 "your-password") - Check for rate limiting lockout (try again after the lockout period)
- Ensure JWT secret is configured
- Clear browser cookies and try again
If the Agent fails to start with a message about the data directory not being writable, the container cannot write to the mounted volume. The /app/data directory is created at runtime and inherits the container's UID.
For Docker Compose with bind mounts:
# Option 1: Run container as your host user (recommended)
# Add to docker-compose.yml: user: "1000:1000"
# Option 2: Change ownership to match your host user
sudo chown -R 1000:1000 /path/to/your/dataFor TrueNAS SCALE:
- In the app configuration, go to Resources and Devices > Security Context
- Set User ID to
568(the TrueNAS apps user) - Ensure your dataset is owned by the apps user (568:568)
For other NAS platforms: Set the container to run as a user that has write access to the data directory, or change the directory ownership to match the container's runtime UID.
When the Gateway or Agent starts, it displays a startup banner with the current configuration. This helps verify that environment variables are loaded correctly. Sensitive values (API keys, passwords, secrets) are obfuscated, showing only the first 2 and last 2 characters.
Example Agent output:
██████╗ ██████╗████████╗ ██████╗ ██████╗ ██████╗ ██████╗ ████████╗██╗ ██╗
██╔═══██╗██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝╚██╗ ██╔╝
██║ ██║██║ ██║ ██║ ██║██████╔╝██║ ██║██████╔╝ ██║ ╚████╔╝
██║ ██║██║ ██║ ██║ ██║██╔═══╝ ██║ ██║██╔══██╗ ██║ ╚██╔╝
╚██████╔╝╚██████╗ ██║ ╚██████╔╝██║ ╚██████╔╝██║ ██║ ██║ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
Agent v0.9.55
─────────────────────────────────────────────────────────────────────────
GatewayUrl : wss://gateway.example.com/tunnel
ApiKey : my****ey
JwtSecret : se****et
PasswordHash : $6****sh
Environment : Production
Container UID:GID: 1000:1000
The Container UID:GID field displays the effective user and group IDs the process is running as (Linux only). This helps diagnose permission issues when the container can't write to mounted volumes.
# View Gateway logs
docker logs octoporty-gateway
# View Agent logs
docker logs octoporty-agent
# View Caddy logs
docker logs caddyContributions are welcome! Please read the following guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Follow the code style guidelines in
CLAUDE.md - Add tests for new functionality
- Ensure all tests pass
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Every file must have a header comment explaining its purpose
- Document all branching logic with explanations
- Use constant-time comparisons for security-sensitive operations
- Follow existing patterns in the codebase
This project is licensed under the No'Saasy License (based on the O'Saasy License Agreement).
This means you can freely use, modify, and distribute the software, but you cannot offer it as a commercial SaaS product to third parties. Self-hosting for your own use is always permitted.
See LICENSE for the full license text.
- Official Website: https://octoporty.com
- GitHub Repository: https://github.com/aduggleby/octoporty
- Container Registry: ghcr.io/aduggleby/octoporty-gateway, ghcr.io/aduggleby/octoporty-agent
- License: O'Saasy License
Made with care by Alex Duggleby

