# Webserver Configuration
This guide covers webserver setup for serving the teXt0wnz application and proxying WebSocket connections for the collaboration server.
## Overview
The teXt0wnz deployment consists of two components:
1. **Static files** - The built application in `dist/` directory
2. **Collaboration server** - Node.js server for real-time collaboration (optional)
A webserver like nginx serves the static files and proxies WebSocket connections to the collaboration server.
## Nginx Configuration
### Basic Setup
**1. Install nginx:**
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install nginx
# CentOS/RHEL
sudo yum install nginx
# macOS
brew install nginx
```
**2. Create configuration file:**
`/etc/nginx/sites-available/text0wnz`
```nginx
server {
listen 80;
listen 443 ssl;
root /path/to/text0wnz/dist;
index index.html;
server_name text.0w.nz; # Replace with your domain
# Include your SSL configuration
include snippets/ssl.conf;
location ~ /.well-known {
allow all;
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
# Proxy WebSocket connections for collaboration
location /server {
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_read_timeout 86400;
proxy_redirect off;
proxy_pass http://localhost:1337/; # Note the trailing slash
}
}
```
**3. Enable the site:**
```bash
sudo ln -s /etc/nginx/sites-available/text0wnz /etc/nginx/sites-enabled/
sudo nginx -t # Test configuration
sudo systemctl reload nginx
```
### Key Configuration Points
**Document Root:**
- Must point to the built `dist/` directory
- Contains `index.html`, `ui/`, and other assets
- Not the `src/` directory
**WebSocket Proxy:**
- `proxy_pass` must match your collaboration server port
- Trailing slash is **required** (`http://localhost:1337/`)
- WebSocket upgrade headers are essential
- Long timeout (86400 = 24 hours) for persistent connections
**Try Files:**
- `try_files $uri $uri/ /index.html` enables client-side routing
- Falls back to index.html for SPA behavior
## SSL/HTTPS Configuration
### SSL Snippet File
Create `/etc/nginx/snippets/ssl.conf`:
```nginx
# Certificate files
ssl_certificate /etc/ssl/private/letsencrypt-domain.pem;
ssl_certificate_key /etc/ssl/private/letsencrypt-domain.key;
# SSL protocols and ciphers
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# SSL session cache
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
```
### Let's Encrypt (Certbot)
**1. Install Certbot:**
```bash
# Ubuntu/Debian
sudo apt install certbot python3-certbot-nginx
# CentOS/RHEL
sudo yum install certbot python3-certbot-nginx
```
**2. Obtain certificate:**
```bash
sudo certbot --nginx -d text.0w.nz
```
**3. Auto-renewal:**
Certbot automatically sets up renewal. Verify with:
```bash
sudo certbot renew --dry-run
```
**4. Copy certificates to expected location:**
```bash
sudo cp /etc/letsencrypt/live/text.0w.nz/fullchain.pem /etc/ssl/private/letsencrypt-domain.pem
sudo cp /etc/letsencrypt/live/text.0w.nz/privkey.pem /etc/ssl/private/letsencrypt-domain.key
sudo chmod 644 /etc/ssl/private/letsencrypt-domain.pem
sudo chmod 600 /etc/ssl/private/letsencrypt-domain.key
```
**5. Set up renewal hook:**
Create `/etc/letsencrypt/renewal-hooks/deploy/copy-certs.sh`:
```bash
#!/bin/bash
cp /etc/letsencrypt/live/text.0w.nz/fullchain.pem /etc/ssl/private/letsencrypt-domain.pem
cp /etc/letsencrypt/live/text.0w.nz/privkey.pem /etc/ssl/private/letsencrypt-domain.key
chmod 644 /etc/ssl/private/letsencrypt-domain.pem
chmod 600 /etc/ssl/private/letsencrypt-domain.key
systemctl reload nginx
```
Make executable:
```bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/copy-certs.sh
```
## Advanced Nginx Configuration
### Full Production Configuration
```nginx
# Map for WebSocket upgrade
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=text0wnz:10m rate=10r/s;
server {
listen 80;
server_name text.0w.nz;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name text.0w.nz;
# Document root
root /var/www/text0wnz/dist;
index index.html;
# SSL configuration
include snippets/ssl.conf;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/javascript application/json image/svg+xml;
# Brotli compression (if enabled)
# brotli on;
# brotli_comp_level 6;
# brotli_types text/plain text/css text/xml text/javascript
# application/x-javascript application/xml+rss
# application/javascript application/json image/svg+xml;
# Let's Encrypt challenge
location ~ /.well-known {
allow all;
}
# Static assets with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Service worker and manifest - no cache
location ~* (service\.js|site\.webmanifest)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
expires 0;
}
# Main application
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
# WebSocket proxy for collaboration
location /server {
# Rate limiting
limit_req zone=text0wnz burst=20 nodelay;
# Proxy settings
proxy_pass http://localhost:1337/;
proxy_http_version 1.1;
# WebSocket headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Forwarding headers
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Timeouts
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
# Buffering
proxy_buffering off;
proxy_redirect off;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
```
### Performance Optimizations
**Worker processes:**
```nginx
# In nginx.conf
worker_processes auto;
worker_connections 1024;
```
**Caching:**
```nginx
# In http block
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
```
**Buffer sizes:**
```nginx
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 8m;
large_client_header_buffers 2 1k;
```
## Apache Configuration
Alternative setup using Apache:
### Virtual Host Configuration
`/etc/apache2/sites-available/text0wnz.conf`
```apache
ServerName text.0w.nz
Redirect permanent / https://text.0w.nz/
ServerName text.0w.nz
DocumentRoot /var/www/text0wnz/dist
# SSL Configuration
SSLEngine on
SSLCertificateFile /etc/ssl/private/letsencrypt-domain.pem
SSLCertificateKeyFile /etc/ssl/private/letsencrypt-domain.key
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!aNULL:!MD5
# Enable required modules
# a2enmod proxy proxy_http proxy_wstunnel rewrite headers ssl
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
# Enable SPA routing
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
# WebSocket proxy for collaboration
ProxyPreserveHost On
ProxyRequests Off
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /server/(.*) ws://localhost:1337/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /server/(.*) http://localhost:1337/$1 [P,L]
ProxyPass /server http://localhost:1337/
ProxyPassReverse /server http://localhost:1337/
# Compression
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
# Caching
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType application/x-font-woff "access plus 1 year"
# Security headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
```
**Enable site:**
```bash
sudo a2ensite text0wnz
sudo systemctl reload apache2
```
## Caddy Configuration
Modern alternative with automatic HTTPS:
### Caddyfile
```caddy
text.0w.nz {
# Automatic HTTPS via Let's Encrypt
# Document root
root * /var/www/text0wnz/dist
# Enable file server
file_server
# SPA routing
try_files {path} /index.html
# WebSocket proxy for collaboration
handle /server* {
reverse_proxy localhost:1337
}
# Compression
encode gzip zstd
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
}
# Cache static assets
@static {
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2
}
header @static Cache-Control "public, max-age=31536000, immutable"
}
```
**Start Caddy:**
```bash
sudo caddy start
```
## Troubleshooting
### WebSocket Connection Issues
**Problem: WebSocket fails to upgrade**
Check nginx error log:
```bash
sudo tail -f /var/log/nginx/error.log
```
Common fixes:
1. Ensure trailing slash in `proxy_pass`: `http://localhost:1337/`
2. Verify WebSocket headers are set
3. Check long timeout values
4. Test direct connection (bypass nginx)
**Problem: Connection drops after short time**
Increase timeouts:
```nginx
proxy_read_timeout 86400;
proxy_send_timeout 86400;
```
**Problem: 502 Bad Gateway**
Collaboration server not running:
```bash
# Check if server is running
ps aux | grep node
# Start server
bun server 1337
```
### SSL Certificate Issues
**Problem: Certificate errors in browser**
1. Verify certificate files exist and are readable
2. Check certificate validity:
```bash
openssl x509 -in /etc/ssl/private/letsencrypt-domain.pem -noout -dates
```
3. Ensure certificate matches domain
4. Check intermediate certificates are included
**Problem: Mixed content warnings**
Ensure all assets load over HTTPS:
1. Check service worker registration
2. Verify all URLs are relative or use HTTPS
3. Add CSP header to enforce HTTPS
### Static File Issues
**Problem: 404 errors for assets**
1. Verify document root points to `dist/` directory
2. Check file permissions:
```bash
ls -la /var/www/text0wnz/dist
```
3. Ensure nginx user can read files:
```bash
sudo chown -R www-data:www-data /var/www/text0wnz/dist
sudo chmod -R 755 /var/www/text0wnz/dist
```
**Problem: CSS/JS not loading**
1. Check MIME types in nginx:
```nginx
include /etc/nginx/mime.types;
default_type application/octet-stream;
```
2. Verify gzip compression doesn't break files
3. Check browser DevTools Network tab
### Performance Issues
**Problem: Slow page loads**
1. Enable gzip/brotli compression
2. Set proper cache headers
3. Enable HTTP/2
4. Optimize buffer sizes
5. Use CDN for static assets
**Problem: High memory usage**
1. Reduce worker connections
2. Limit request body size
3. Enable buffering for proxy
4. Monitor with `htop`
## Monitoring
### Nginx Access Logs
```bash
# Real-time access log
sudo tail -f /var/log/nginx/access.log
# Filter for errors
sudo grep "error" /var/log/nginx/error.log
# Count requests per IP
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
```
### Nginx Status Module
Enable in nginx.conf:
```nginx
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
```
Check status:
```bash
curl http://localhost/nginx_status
```
### Log Analysis Tools
- **GoAccess** - Real-time web log analyzer
- **AWStats** - Advanced web statistics
- **Webalizer** - Web server log analysis
## See Also
- [Collaboration Server](collaboration-server.md) - Server setup and configuration
- [Building and Developing](building-and-developing.md) - Build process
- [Testing](testing.md) - Testing setup