Server Deployment
Learn how to deploy the Portr admin dashboard and tunnel server with Docker Compose to self-host your own tunneling infrastructure.
For quick setup, use the production compose file from the Portr repository:
https://github.com/amalshaji/portr/blob/main/docker-compose.prod.yaml
Services Overview
The production compose file includes 3 services:
- caddy: The reverse proxy for routing and SSL termination
- server: The combined admin and tunnel server
- postgres: The PostgreSQL database
Deployment Steps
Download the compose file
Download docker-compose.prod.yaml from the repository and place it in your server directory.
Set up environment variables
Create a .env file in the same directory with the following values:
Generate unique credentials for each deployment. Replace every placeholder in
the .env file, and use the same database secret in POSTGRES_PASSWORD and
PORTR_DB_URL.
Environment Variable Reference
| Variable | Description | Default |
|---|---|---|
PORTR_DOMAIN | Your server domain | localhost:8001 |
PORTR_DB_URL | PostgreSQL connection URL | Required |
PORTR_SERVER_URL | Public admin/API server URL for clients, without a scheme | localhost:8000 |
PORTR_TRANSPORT | Transport to emit in generated client configs: ssh or websocket | ssh |
PORTR_SSH_URL | Public SSH tunnel URL for clients when PORTR_TRANSPORT=ssh | localhost:2222 |
PORTR_SSH_PORT | SSH tunnel listener port | 2222 |
PORTR_SSH_HOST_KEY | Stable PEM-encoded Ed25519 host private key when PORTR_TRANSPORT=ssh; strongly recommended to preserve identity across restarts | Ephemeral key |
PORTR_WS_URL | WebSocket control hostname routed to the tunnel proxy when PORTR_TRANSPORT=websocket | localhost:8001 |
PORTR_ADMIN_PORT | Admin server port | 8000 |
PORTR_PROXY_PORT | Tunnel proxy port; also the WebSocket endpoint port when PORTR_TRANSPORT=websocket | 8001 |
PORTR_ADMIN_GITHUB_CLIENT_ID | GitHub OAuth client ID | Optional |
PORTR_ADMIN_GITHUB_CLIENT_SECRET | GitHub OAuth client secret | Optional |
PORTR_RESERVED_SUBDOMAIN_LIMIT | Maximum reserved subdomains per team membership; use 0 to disable new reservations | 3 |
PORTR_AUTO_MIGRATE | Auto-run database migrations | false |
CLOUDFLARE_API_TOKEN | Cloudflare API token for SSL | Required (bundled Caddy) |
If you run on SQLite instead of PostgreSQL, see SQLite Backups for the LITESTREAM_* variables that replicate the database to object storage.
If you already run a reverse proxy or manage TLS certificates manually, see Custom Reverse Proxy — the Cloudflare token is only needed by the bundled Caddy service.
Generate SSH Host Key
PORTR_SSH_HOST_KEY is what gives your server a stable SSH identity. If it is unset, the server generates a temporary key in memory each time it starts, so every restart or container recreation presents a different host key. Clients with insecure_skip_host_key_verification: false will then reject the server with "host key verification failed". The client config generated by portr auth set only includes insecure_skip_host_key_verification: false when this variable is set; clients that already have a config file need to add it by hand.
Generate an Ed25519 private key and set it as an environment variable:
You can also generate a key with Portr:
Store the generated key as a persistent multi-line environment value or Docker secret. Do not generate a new key each time the server starts.
Start the services
Run the following command to start all services:
The server container runs as a non-root user. If you keep the default ./data
bind mount, make sure that directory is writable by the container user before
starting the service.
Access the admin dashboard
Once the servers are running, navigate to your domain (e.g., https://example.com) and log in to the admin dashboard. The first login will be treated as a superuser account.
Routing Model
The server process starts one tunnel transport based on PORTR_TRANSPORT:
PORTR_ADMIN_PORTserves the admin dashboard and API, including/api/v1/healthcheck.- With
PORTR_TRANSPORT=ssh,PORTR_SSH_PORTserves SSH tunnel clients. - With
PORTR_TRANSPORT=websocket,PORTR_PROXY_PORTserves the WebSocket tunnel endpoint at/_portr/tunnel/connectand routes wildcard tunnel traffic such ashttps://my-app.example.com.
The production compose file routes PORTR_DOMAIN to the admin listener and
*.PORTR_DOMAIN to the tunnel listener. PORTR_TRANSPORT controls both the
server-side tunnel listener and the generated client config, so clients receive
either ssh_url or ws_url to match the running transport.
For WebSocket transport, set PORTR_WS_URL to a hostname covered by the
wildcard route, such as tunnel.example.com. Generated configs keep
tunnel_url: example.com separate so public application URLs remain
https://<subdomain>.example.com.
No database migration is required for the WebSocket tunnel transport itself; the existing connection tables continue to store HTTP and TCP tunnel metadata.
Alternative Database Setup
If you want to run PostgreSQL separately and not as a Docker service, you can exclude the following environment variables from your .env file:
POSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_DB
Troubleshooting
Service Health Checks
Check if all services are running properly:
View Service Logs
Check logs for any specific service:
SSL Certificate Issues
If you're having SSL certificate issues, ensure:
- Your DNS records are properly configured
- Your Cloudflare API token has the correct permissions
- Your domain is added to Cloudflare
Port Accessibility
Verify that the required ports are accessible: