Coder Reverse Proxy for Local Development
How to use the local reverse proxy to access Coder workspaces in development
Overview
For local development, we use a reverse proxy to access Coder workspaces at coder.nolapse.tech through a local domain. This allows you to:
- Test workspace launching locally without modifying production
- Set cookies on
localhostdomain instead of.nolapse.tech - Debug issues with workspace sessions and authentication
- Access full VS Code functionality including WebSockets for terminals
Quick commands:
# Start (automatically runs with pnpm dev)
pnpm dev:proxy
# Or using package filter
pnpm --filter @package/dev-proxy start
# Stop
pnpm --filter @package/dev-proxy stop
# Logs (with cookies for debugging)
pnpm --filter @package/dev-proxy logs
# Status
pnpm --filter @package/dev-proxy status
# Direct docker commands (if needed)
docker ps | grep reverse-proxy
docker logs -f reverse-proxy
# Test cookie setter
curl http://coder.localhost/set-cookie.html
# Test main proxy
curl -H "Cookie: coder_session_token=test" http://coder.localhostHow It Works
The Problem
In production, when users launch a workspace:
- A session token is set as a cookie on domain
.nolapse.tech - The user is redirected to
https://coder.nolapse.tech/workspace/... - The browser sends the cookie with the request, authenticating the user
In local development, we face several challenges:
- Your app runs on
localhost:3000 - You can't set cookies for
.nolapse.techfrom localhost - Redirecting to production Coder would lose the session context
- Cookies set on
localhost:3000won't be sent tocoder.localhost(different hostname) - Coder validates origin headers and expects HTTPS connections
The Solution
The reverse proxy acts as a local gateway to the production Coder instance with a special cookie-setting flow:
Browser (localhost:3000)
↓
Click "Launch Workspace"
↓
Redirect to http://coder.localhost/set-cookie.html?token=...&redirect=...
↓
JavaScript sets cookie on coder.localhost domain
↓
Redirect to http://coder.localhost/workspace/...
↓
Nginx Reverse Proxy (makes it look like HTTPS from coder.nolapse.tech)
↓
https://coder.nolapse.tech (actual Coder instance)Architecture Components:
- Docker container runs nginx on port 80
- nginx forwards all requests to
https://coder.nolapse.tech - nginx rewrites headers to make requests appear as HTTPS from the real domain
- Browser thinks it's talking to
coder.localhost - Cookies are set via an intermediate HTML page served by nginx
- WebSockets work because nginx properly upgrades connections
Why .localhost Domains Work
According to RFC 6761, all .localhost domains automatically resolve to 127.0.0.1 on modern operating systems and browsers. This means:
- No need to edit
/etc/hosts - No DNS configuration required
- Works out of the box on macOS, Linux, and Windows
Setup
Automatic Start with pnpm dev
The reverse proxy automatically starts when you run:
pnpm devThis is the recommended way as it ensures the proxy is running before your Next.js app starts.
Manual Control
You can also manage the proxy independently:
Start the proxy:
pnpm dev:proxy
# or
pnpm --filter @package/dev-proxy startStop the proxy:
pnpm --filter @package/dev-proxy stopView logs:
pnpm --filter @package/dev-proxy logsCheck status:
pnpm --filter @package/dev-proxy statusRestart the proxy:
pnpm --filter @package/dev-proxy restartWhat Happens on Start
The reverse proxy is organized as a package at packages/dev-proxy/:
packages/dev-proxy/
├── package.json # Package configuration with scripts
├── docker-compose.yml # Docker Compose configuration
├── nginx.conf # Nginx reverse proxy config
├── set-coder-cookie.html # Cookie-setting bridge page
└── README.md # Quick referenceConfiguration Files
docker-compose.yml
Located at packages/dev-proxy/docker-compose.yml:
services:
reverse-proxy:
image: nginx:alpine
container_name: reverse-proxy
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./set-coder-cookie.html:/usr/share/nginx/html/set-cookie.html:ro
restart: unless-stoppedKey points:
- Maps host port
80to container port80 - Mounts
nginx.confas read-only - Mounts
set-coder-cookie.htmlfor the cookie-setting flow - Auto-restarts unless manually stopped
1. WebSocket Support
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}This mapping ensures that:
- Regular HTTP requests get
Connection: close - WebSocket upgrade requests get
Connection: upgrade - Without this, all connections would be forced to upgrade, breaking regular HTTP
2. Header Rewriting for Coder Authentication
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Origin https://coder.nolapse.tech;Why this is critical:
- Coder's signed app tokens validate the request origin and protocol
- Without these headers, you get 403 Forbidden errors
X-Forwarded-Proto httpsmakes Coder think the request came over HTTPSOrigin https://coder.nolapse.techmakes Coder accept the request as legitimate
3. Long Timeouts for WebSockets
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;WebSocket connections (like VS Code terminals) are long-lived. The 7-day timeout ensures:
- Terminal sessions don't disconnect
- File watchers stay connected
- Language server protocol (LSP) connections remain stable
4. Cookie Logging
log_format debug_format '... Cookies: "$http_cookie"';This logs all cookies sent with each request, making it easy to debug authentication issues.
set-coder-cookie.html
Located at packages/dev-proxy/set-coder-cookie.html:
The cookie-setter page solves a critical problem: cookies can't be shared between different hostnames, even if both are localhost-based.
- When you're on
localhost:3000, cookies set there won't be sent tocoder.localhost - This intermediate page runs on
coder.localhostdomain - JavaScript sets the cookie directly on that domain
- Then redirects to the actual workspace URL
- Now the cookie is available for all subsequent requests
Application Integration
Development vs Production Detection
The launchWorkspace action in apps/nextjs/src/app/app/actions.ts automatically detects the environment:
const isDevelopment =
process.env.NODE_ENV === "development" ||
process.env.VERCEL_ENV !== "production";Testing
1. Start the Reverse Proxy
The proxy should start automatically with pnpm dev, but you can also start it manually:
pnpm dev:proxy2. Verify It's Running
docker ps | grep reverse-proxyYou should see:
CONTAINER ID IMAGE PORTS NAMES
abc123def456 nginx:alpine 0.0.0.0:80->80/tcp reverse-proxy3. Test in Browser
- Start your Next.js app:
pnpm dev - Navigate to an assignment
- Click "Launch Workspace"
- You should see the "Setting up workspace..." page briefly
- Then redirect to VS Code at
http://coder.localhost/... - Terminal and other WebSocket features should work
4. Verify Cookies
Open browser DevTools → Application → Cookies → http://coder.localhost
You should see:
coder_session_tokencookiecoder_signed_app_tokencookie (set by Coder)- Domain:
coder.localhost
5. Check Logs for Debugging
docker logs -f reverse-proxyLook for:
- Requests to
/set-cookie.html(should return 200) - Subsequent requests with
coder_session_tokenin cookies - No 403 errors (which indicate authentication problems)
Troubleshooting
WebSocket Errors (1006)
Symptom: "WebSocket close with status code 1006" in VS Code
Causes:
- Origin header not set correctly
- Check nginx.conf has
proxy_set_header Origin https://coder.nolapse.tech;
- Check nginx.conf has
- X-Forwarded-Proto not HTTPS
- Check nginx.conf has
proxy_set_header X-Forwarded-Proto https;
- Check nginx.conf has
- Connection header hardcoded to "upgrade"
- Should use
$connection_upgradevariable from the map
- Should use
Solution: Ensure nginx.conf matches the configuration above exactly.
403 Forbidden Errors
Symptom: Requests fail with 403, or you see "Access Denied" in Coder
Causes:
- Missing Origin header
- Coder validates the Origin header against signed app tokens
- Wrong protocol in X-Forwarded-Proto
- Coder expects HTTPS, not HTTP
Check logs:
docker logs reverse-proxy | grep 403Solution: Verify these headers in nginx.conf:
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Origin https://coder.nolapse.tech;Cookie Not Being Set
Symptom: Redirected to Coder login page, no authentication
Debug steps:
-
Check if cookie-setter page loads:
curl http://coder.localhost/set-cookie.htmlShould return the HTML page
-
Verify cookie in browser:
- Open DevTools → Application → Cookies
- Look under
http://coder.localhost - Should see
coder_session_token
-
Check if cookie is sent:
docker logs reverse-proxy | grep coder_session_tokenYou should see the cookie in subsequent requests
Common issues:
- JavaScript disabled in browser
- Browser blocking cookies
- Redirect happening too fast (increase delay in set-cookie.html)
Port 80 Already in Use
Symptom: "port already allocated" error
Solution:
Find what's using the port:
sudo lsof -i :80Options:
- Stop the conflicting service
- Use a different port:
# packages/dev-proxy/docker-compose.yml
ports:
- "8080:80"Then update apps/nextjs/src/app/app/actions.ts:
const intermediateUrl = `http://coder.localhost:8080/set-cookie.html?...`;Can't Access coder.localhost
-
Verify DNS resolution:
ping coder.localhostShould show:
PING coder.localhost (127.0.0.1) -
Check container is running:
docker ps | grep reverse-proxy -
Check nginx logs:
docker logs reverse-proxy -
Test nginx directly:
curl -v http://coder.localhost
Workspace Loads But Terminal Doesn't Work
Symptom: VS Code interface loads, but terminal shows connection error
Likely cause: WebSocket upgrade not working
Check:
-
Browser DevTools → Network → WS tab
- Look for WebSocket connections
- Status should be 101 (Switching Protocols)
- If 403 or other error, check Origin header
-
Verify nginx WebSocket config:
docker exec reverse-proxy cat /etc/nginx/nginx.conf | grep -A 2 "WebSocket" -
Check timeouts aren't too short: Should be 7 days, not seconds
Security Notes
Development Only
This setup is only for local development. Never use it in production because:
- No HTTPS/SSL certificates (uses HTTP)
- Cookies set with
secure: false - Direct proxy to production backend exposes it
- Debug logging may log sensitive information
- No rate limiting or protection
Production Configuration
In production (when NODE_ENV !== "development"):
- Direct connection to
https://coder.nolapse.tech - Cookies with
secure: true(HTTPS only) - Domain set to
.nolapse.tech - HttpOnly cookies prevent JavaScript access
- No reverse proxy needed
Sensitive Data in Logs
The nginx configuration logs cookies for debugging. In a more secure setup, you might want to:
-
Remove cookie logging:
log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent'; -
Disable access logs entirely:
access_log off;
Advanced Configuration
Custom Coder Backend
To proxy to a different Coder instance, edit nginx.conf:
proxy_pass https://your-custom-coder.example.com;
proxy_set_header Host your-custom-coder.example.com;
proxy_set_header Origin https://your-custom-coder.example.com;Also update apps/nextjs/src/app/app/actions.ts to use the new domain in development.
Additional Logging
To log more details (like request/response headers):
http {
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'Cookies: "$http_cookie" '
'Upgrade: "$http_upgrade" '
'Connection: "$connection_upgrade"';
access_log /var/log/nginx/access.log detailed;
}Then view logs:
docker logs -f reverse-proxyPerformance Tuning
For heavy usage, adjust worker connections:
events {
worker_connections 2048; # Increase from 1024
}HTTPS with Self-Signed Certificate (Optional)
If you want to test with HTTPS locally:
-
Generate self-signed certificate:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout localhost.key -out localhost.crt \ -subj "/CN=coder.localhost" -
Update
packages/dev-proxy/docker-compose.yml:volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./set-coder-cookie.html:/usr/share/nginx/html/set-cookie.html:ro - ./localhost.crt:/etc/nginx/ssl/cert.crt:ro - ./localhost.key:/etc/nginx/ssl/cert.key:ro -
Update
packages/dev-proxy/nginx.conf:server { listen 443 ssl; server_name coder.localhost; ssl_certificate /etc/nginx/ssl/cert.crt; ssl_certificate_key /etc/nginx/ssl/cert.key; # ... rest of config } -
Update actions.ts to use
https://coder.localhost
Note: You'll need to accept the self-signed certificate in your browser.