Self-Hosting Qwik Applications
This guide provides comprehensive instructions for self-hosting your Qwik application on your own infrastructure, whether it's a VPS, bare metal server, or container platform.
Prerequisites
- Node.js >= 16.8.0 (18.x LTS or later recommended)
- A web server like Nginx, Apache, or similar for production deployments
- (Optional) Docker for containerized deployments
- (Optional) PM2 or similar for process management
Build Options
Qwik provides several adapters suited for self-hosting scenarios. Choose the one that best fits your infrastructure:
Fastify (Node.js)
Fastify offers better performance than Express and is another good option for self-hosting:
npx qwik add fastify
Static Site
For simple applications without dynamic server functionality:
npx qwik add static
When using the static adapter, make sure to update your adapters/static/vite.config.ts
file with your actual domain:
staticAdapter({ origin: 'https://your-actual-domain.com', })
Building for Production
After adding the appropriate adapter, build your application. This will create a dist/
directory with all the necessary files for deployment:
pnpm run build
npm run build
yarn run build
bun run build
The build process will generate:
-
dist/
directory containing:- Vite-generated files:
dist/build/
– JavaScript and CSS bundles (code-split and optimized)dist/assets/
– Fonts, images, and static assets processed by Vite
- Non-Vite files (copied as-is):
- Everything from the
public/
folder (e.g., favicon, robots.txt, etc.)
- Everything from the
- Static site generation output (if SSG is used):
- Pre-rendered
.html
pages - Route-specific
q-data.json
files
- Pre-rendered
- Manifest files:
manifest.json
– For PWA supportq-manifest.json
– Used during development, can be omitted from production
- Vite-generated files:
-
server/
directory:- Contains server entrypoints like
entry.fastify.js
orentry.express.js
depending on the adapter used.
- Contains server entrypoints like
Deployment Methods
Method 1: Node.js Server Deployment
For Express or Fastify adapters, the build process generates a server entry file at server/entry.express.js
or server/entry.fastify.js
.
-
Transfer these files to your production server:
dist/
(client assets)server/
(server code)package.json
(for dependencies)
-
Install production dependencies on the server:
pnpm install --prod
npm install --omit=dev
yarn install --production
bun install --production
- Set the required environment variables:
# Important: This is required for CSRF protection export ORIGIN=https://your-domain.com # Set production mode export NODE_ENV=production # Optional: Define a custom port (default is 3000) export PORT=3000
- Run the server:
# For Express node server/entry.express.js # For Fastify node server/entry.fastify.js
- For production use, use a process manager like PM2:
npm install -g pm2 # For Express pm2 start server/entry.express.js --name qwik-app # For Fastify pm2 start server/entry.fastify.js --name qwik-app
Using systemd
Create a systemd service file:
[Unit] Description=Qwik Application After=network.target [Service] Type=simple User=qwikuser WorkingDirectory=/path/to/your/app ExecStart=/usr/bin/node server/entry.express.js Restart=always Environment=NODE_ENV=production [Install] WantedBy=multi-user.target
Enable and start the service:
sudo systemctl enable qwik-app sudo systemctl start qwik-app
Method 2: Docker Deployment
Using Docker is recommended for containerized deployments. Create a Dockerfile
in your project root:
ARG NODE_VERSION=18.18.2 ################################################################################ # Use node image for base image for all stages. FROM node:${NODE_VERSION}-alpine as base # Set working directory for all build stages. WORKDIR /usr/src/app ################################################################################ # Create a stage for installing production dependencies. FROM base as deps # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.yarn to speed up subsequent builds. RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=yarn.lock,target=yarn.lock \ --mount=type=cache,target=/root/.yarn \ yarn install --frozen-lockfile ################################################################################ # Create a stage for building the application. FROM deps as build # Copy the rest of the source files into the image. COPY . . # Run the build script. RUN yarn run build ################################################################################ # Create a new stage to run the application with minimal runtime dependencies FROM base as final # Use production node environment by default. ENV NODE_ENV production # IMPORTANT: Set your actual domain for CSRF protection ENV ORIGIN https://your-domain.com # Run the application as a non-root user. USER node # Copy package.json so that package manager commands can be used. COPY package.json . # Copy the production dependencies from the deps stage and also # the built application from the build stage into the image. COPY --from=deps /usr/src/app/node_modules ./node_modules COPY --from=build /usr/src/app/dist ./dist COPY --from=build /usr/src/app/server ./server # Expose the port that the application listens on. EXPOSE 3000 # Run the application. CMD yarn serve
Build and run your Docker container:
# Build the image docker build -t qwik-app . # Run the container docker run -p 3000:3000 -e ORIGIN=https://your-domain.com qwik-app
Method 3: Static Site Deployment
If using the static adapter, copy the contents of the dist
directory to your web server's document root:
# Example using rsync rsync -avz dist/ user@your-server:/path/to/webroot/
Web Server Configuration
Web Server Configurations
1. Nginx for Node.js Applications
For Node.js deployments using Express or Fastify, use this Nginx configuration as a reverse proxy:
# HTTP to HTTPS redirect server { listen 80; server_name your-domain.com; return 301 https://$host$request_uri; } # Main server block server { listen 443 ssl http2; server_name your-domain.com; # SSL configuration ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; # Proxy to Node.js server location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; 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; } # Static assets handling (build and assets directories) location ~ ^/(build|assets)/ { root /path/to/your/app/dist; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; } # Security headers add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; }
2. Nginx for Static Sites
For static site deployments, use this Nginx configuration:
# HTTP to HTTPS redirect server { listen 80; server_name your-domain.com; return 301 https://$host$request_uri; } # Main server block server { listen 443 ssl http2; server_name your-domain.com; # SSL configuration ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; # Document root root /path/to/your/app/dist; index index.html; # Security headers add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; # Main location block location / { try_files $uri $uri/ /index.html; } # Cache settings for static assets (build and assets directories) location ~ ^/(build|assets)/ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; try_files $uri =404; gzip_static on; brotli_static on; } # Route loader data caching location ~ /[^/]+/q-data\.json$ { expires 30s; add_header Cache-Control "public, must-revalidate"; try_files $uri =404; } # Security: Deny access to hidden files location ~ /\. { deny all; access_log off; log_not_found off; } # Security: Deny access to .git directory location ^~ /.git/ { deny all; access_log off; log_not_found off; } }
3. Apache Configuration
For Apache with mod_proxy enabled, use this configuration:
# HTTP to HTTPS redirect <VirtualHost *:80> ServerName your-domain.com RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] </VirtualHost> # Main server block <VirtualHost *:443> ServerName your-domain.com # SSL Configuration SSLEngine on SSLCertificateFile /path/to/cert.pem SSLCertificateKeyFile /path/to/key.pem # Proxy to Node.js server ProxyPreserveHost On ProxyPass / http://localhost:3000/ ProxyPassReverse / http://localhost:3000/ # Serve static assets directly (build and assets directories) <Directory /path/to/your/app/dist/build> Options Indexes FollowSymLinks AllowOverride All Require all granted <IfModule mod_expires.c> ExpiresActive On ExpiresDefault "access plus 1 year" Header append Cache-Control "public, immutable" </IfModule> </Directory> # Alias for both build and assets directories AliasMatch ^/(build|assets)/(.*)$ /path/to/your/app/dist/$1/$2 # Security headers Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set X-XSS-Protection "1; mode=block" # SPA fallback <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] </IfModule> </VirtualHost>
Security Best Practices
CSRF Protection
Qwik City applications are protected against CSRF (Cross-Site Request Forgery) attacks by default for all state-mutating HTTP methods (POST, PATCH, DELETE). This protection is crucial for preventing unauthorized actions on behalf of authenticated users.
How It Works
- Default Protection: Enabled by default for all non-idempotent requests
- Origin Checking: Validates the
Origin
andReferer
headers against theORIGIN
environment variable - Configuration: Controlled via the
checkOrigin
option increateQwikCity
Configuration Options
1. Basic Configuration (Recommended)
// src/entry.express.tsx or src/entry.fastify.tsx const { router, notFound } = createQwikCity({ render, qwikCityPlan, manifest, // CSRF protection is enabled by default when ORIGIN is set checkOrigin: true });
2. SSL Offload Proxy Setup
When behind a reverse proxy (Nginx, CloudFront, etc.), use 'lax-proto'
to handle X-Forwarded-Proto
headers:
const { router, notFound } = createQwikCity({ render, qwikCityPlan, manifest, checkOrigin: 'lax-proto' // Required for proper proxy support });
3. Disabling CSRF (Not Recommended)
const { router, notFound } = createQwikCity({ render, qwikCityPlan, manifest, checkOrigin: false // Disables CSRF protection (not recommended for production) });
Essential Security Headers
Configure these security headers in your web server:
# Nginx example add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
Additional Security Measures
-
User Privileges
- Run Node.js as a non-root user
- Use process managers like PM2 with
--user
flag
-
Network Security
- Configure firewalls to expose only necessary ports (typically 80, 443)
- Use fail2ban to protect against brute force attacks
- Implement rate limiting for API endpoints
-
Dependencies
- Regularly update all dependencies
- Use
npm audit
oryarn audit
to identify vulnerabilities - Consider using Dependabot or similar tools
-
HTTPS
- Enforce HTTPS with HSTS header
- Use modern TLS configurations (TLS 1.2/1.3)
- Implement certificate auto-renewal (e.g., Certbot for Let's Encrypt)
-
Application Hardening
- Set appropriate file permissions
- Disable directory listing
- Protect sensitive files (e.g.,
.env
,.git
) - Implement proper CORS policies
-
Monitoring & Logging
- Monitor for suspicious activities
- Implement centralized logging
- Set up alerts for security events
Monitoring and Logging
Application Monitoring
1. Process Management with PM2
# Install PM2 globally npm install -g pm2 # Start application pm2 start dist/server/entry.express.js --name qwik-app # Monitor application pm2 monit # View logs pm2 logs qwik-app # Set up PM2 to start on system boot pm2 startup pm2 save # Set up PM2 dashboard (optional) npm install -g pm2-web pm2-web --config pm2-webrc.json
2. Web Server Logs
Nginx
# Access logs /var/log/nginx/access.log /var/log/nginx/error.log # Enable JSON logging format in nginx.conf log_format json_combined escape=json '{"timestamp":"$time_iso8601",' '"remote_addr":"$remote_addr",' '"remote_user":"$remote_user",' '"request":"$request",' '"status":"$status",' '"body_bytes_sent":"$body_bytes_sent",' '"http_referer":"$http_referer",' '"http_user_agent":"$http_user_agent"}';
Performance Monitoring
1. System-Level Monitoring
# Basic system monitoring top htop nmon # Network monitoring iftop iotop # Disk I/O iostat -x 1
2. Application Performance Monitoring (APM)
Using Prometheus + Grafana
- Set up Prometheus to collect metrics
- Configure Grafana for visualization
- Monitor key metrics:
- Request rates
- Error rates
- Response times
- Resource utilization
Alternative APM Solutions
Performance Optimization
Network Optimization
-
HTTP/2 and HTTP/3
- Enable HTTP/2 in Nginx/Apache
- Consider HTTP/3 for modern browsers
- Configure proper keep-alive settings
-
CDN Integration
- Use Cloudflare, CloudFront, or similar
- Configure edge caching rules
- Enable automatic image optimization
-
Compression
- Enable Brotli compression
- Pre-compress static assets
- Configure proper cache headers
Asset Optimization
-
Caching Strategy
# Immutable assets (hashed filenames) location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; }
-
Lazy Loading
- Use Qwik's built-in lazy loading
- Split large components
- Implement route-based code splitting
-
Image Optimization
- Use modern formats (WebP/AVIF)
- Implement responsive images
- Lazy load below-the-fold images
Server-Side Optimization
-
Node.js Tuning
- Set appropriate
NODE_ENV=production
- Tune V8 garbage collection
- Use worker threads for CPU-intensive tasks
- Set appropriate
-
Database Optimization
- Implement connection pooling
- Add appropriate indexes
- Use query optimization techniques
-
Caching Layer
- Implement Redis/Memcached
- Cache API responses
- Use stale-while-revalidate patterns
Asset Compression
Enable efficient compression for static assets:
- Pre-compress static files during build:
# Install compression tools npm install -g brotli-cli gzip-cli # Compress build assets in parallel with maximum compression find dist/build dist/assets -type f -regex '.*\.\(js\|css\|html\|json\)$' -print0 | xargs -0 gzip -9 -k & find dist/build dist/assets -type f -regex '.*\.\(js\|css\|html\|json\)$' -print0 | xargs -0 brotli -15 -k & wait
- Configure Nginx to serve pre-compressed files:
location ~ ^/(build|assets)/ { root /path/to/your/app/dist; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable, no-transform" always; access_log off; # Enable pre-compressed file serving gzip_static on; brotli_static on; try_files $uri =404; }
Scaling Considerations
For high-traffic applications:
- Set up load balancing with multiple Node.js instances
- Use container orchestration (Kubernetes, Docker Swarm)
- Consider a microservices architecture for larger applications
- Implement Redis or similar for session management across instances
- Use a serverless approach for predictable scaling
Troubleshooting Common Issues
-
404 Not Found:
- Check your Nginx/Apache configuration paths
- Verify that URL rewriting is properly configured
- Check file permissions: Ensure the web server user has read access to all files and execute permission on all parent directories
# Check permissions for a specific file sudo -u nginx ls -ld /path/to/static/file # Check directory permissions (go up the path to find where access is denied) sudo -u nginx ls -ld /path/to/static/ sudo -u nginx ls -ld /path/to/ sudo -u nginx ls -ld /path/ # NixOS-specific: Web server users typically don't have access to /home by default # You may need to adjust the permissions or move the files to /var/www or similar
-
503 Service Unavailable:
- Verify the Node process is running (
pm2 list
orps aux | grep node
) - Check server logs for memory issues or crashes
- Verify the Node process is running (
-
CSRF Errors:
- Ensure the
ORIGIN
environment variable is correctly set - Check that form submissions include the correct CSRF token
- Ensure the
-
Performance Issues:
- Enable compression in your web server
- Check for memory leaks using Node.js profiling
- Verify that static assets are served with proper cache headers
-
Missing Assets:
- Ensure your build process is complete
- Check that the
/build/
and/assets/
directories are correctly exposed