Deploying TMI Web Application - ericfitz/tmi GitHub Wiki
This guide covers how to deploy the TMI-UX Angular-based web application for production and development environments.
TMI-UX is an Angular/TypeScript single-page application (SPA) that provides:
- Interactive threat modeling interface
- Real-time collaborative diagram editing
- OAuth authentication flow
- RESTful API integration
- WebSocket support for collaboration
Choose your deployment method:
| Method | Best For | Link |
|---|---|---|
| Static Hosting | Fastest, CDN-friendly (recommended) | Static Hosting |
| Node.js Server | Custom server configuration | Node.js Server |
| Docker | Containerized deployment | Docker Deployment |
| Development Server | Local development | Development Server |
Before you begin, ensure you have:
-
Node.js 22 (required; see
.nvmrcfor the exact version) -
pnpm (package manager; version pinned in the
package.jsonpackageManagerfield) - Access to a TMI API server (see Deploying-TMI-Server)
- OAuth provider credentials, if you are self-hosting authentication (see Setting-Up-Authentication)
# Clone the repository
git clone https://github.com/ericfitz/tmi-ux.git
cd tmi-ux
# Install pnpm if needed
npm install -g pnpm
# Install dependencies
pnpm installTMI-UX uses Angular file replacements to manage environment configuration. Each build configuration in angular.json swaps src/environments/environment.ts with a target-specific file at compile time.
To create a custom environment, copy the example file:
cp src/environments/environment.example.ts src/environments/environment.myenv.tsEdit the new file with your deployment settings. A reference for all available properties is in src/environments/environment.interface.ts. Here is a typical production configuration:
// src/environments/environment.prod.ts
import { Environment } from './environment.interface';
export const environment: Environment = {
production: true,
logLevel: 'ERROR',
apiUrl: 'https://api.tmi.example.com', // Your TMI API server
authTokenExpiryMinutes: 60,
operatorName: 'TMI Operator',
operatorContact: '[email protected]',
operatorJurisdiction: '',
securityConfig: {
enableHSTS: true,
hstsMaxAge: 31536000,
hstsIncludeSubDomains: true,
hstsPreload: false,
frameOptions: 'DENY',
referrerPolicy: 'strict-origin-when-cross-origin',
permissionsPolicy: 'camera=(), microphone=(), geolocation=()',
},
};Note: Angular bakes environment values into the JavaScript bundle at build time. However, when you use the Express server (
server.js), you can override many settings at runtime throughTMI_*environment variables. See Configuration-Reference for details.
# Production build (optimized, minified)
pnpm run build:prod
# Output: dist/tmi-ux/browser/The build uses Angular's application builder, which places output in a browser/ subdirectory. Build output includes:
-
index.html-- main HTML file -
*.js-- bundled JavaScript (hashed filenames for cache-busting) -
*.css-- compiled stylesheets - Image and font assets
Other build configurations are available for specific deployment targets:
pnpm run build:container # Container image build (Dockerfile.chainguard, used for k3s)Static hosting is the best option for most deployments. You serve pre-built files through a CDN or web server.
Create /etc/nginx/sites-available/tmi-ux:
server {
listen 443 ssl http2;
server_name tmi.example.com;
ssl_certificate /etc/ssl/certs/tmi.crt;
ssl_private_key /etc/ssl/private/tmi.key;
root /var/www/tmi-ux;
index index.html;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/javascript application/json;
# Security headers
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 "no-referrer-when-downgrade" always;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Angular routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# API proxy (optional - if not using a separate API domain)
location /api/ {
proxy_pass http://localhost:8080/;
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name tmi.example.com;
return 301 https://$server_name$request_uri;
}# Copy build files
sudo mkdir -p /var/www/tmi-ux
sudo cp -r dist/tmi-ux/browser/* /var/www/tmi-ux/
# Set permissions
sudo chown -R www-data:www-data /var/www/tmi-ux
# Enable the site
sudo ln -s /etc/nginx/sites-available/tmi-ux /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxFor Apache, create /etc/apache2/sites-available/tmi-ux.conf:
<VirtualHost *:443>
ServerName tmi.example.com
DocumentRoot /var/www/tmi-ux
SSLEngine on
SSLCertificateFile /etc/ssl/certs/tmi.crt
SSLCertificateKeyFile /etc/ssl/private/tmi.key
<Directory /var/www/tmi-ux>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
# Angular routing
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</Directory>
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
</IfModule>
# Cache static assets
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/* "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
</VirtualHost>
<VirtualHost *:80>
ServerName tmi.example.com
Redirect permanent / https://tmi.example.com/
</VirtualHost>Enable the required modules and restart:
sudo a2enmod rewrite ssl deflate expires headers
sudo a2ensite tmi-ux
sudo apache2ctl configtest
sudo systemctl reload apache2This is the supported production path; www.tmi.dev is served this way. The tmi-ux repository defines the infrastructure in terraform/aws/ (S3 buckets, CloudFront, ACM, Route 53) and the build configuration in src/environments/environment.aws.ts.
# Build with the aws configuration, sync to S3, and invalidate CloudFront
pnpm run deploy:aws
# Deploy an existing dist/ without rebuilding
bash scripts/deploy-aws.sh --no-buildpnpm run build:aws builds without deploying. The tmi-ux repository's docs/reference/aws-deployment.md covers Terraform backend setup, infrastructure changes, and teardown.
# Install Netlify CLI
npm install -g netlify-cli
# Deploy
pnpm run build:prod
netlify deploy --prod --dir=dist/tmi-ux/browserCreate netlify.toml:
[build]
publish = "dist/tmi-ux/browser"
command = "pnpm run build:prod"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/*.js"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/*.css"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"# Install Vercel CLI
npm install -g vercel
# Deploy
pnpm run build:prod
vercel --prodCreate vercel.json:
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"distDir": "dist/tmi-ux/browser"
}
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/index.html"
}
]
}TMI-UX includes a production-ready Express server (server.js) that serves the built Angular application and provides runtime configuration overrides.
# Build the application
pnpm run build:prod
# Start the production server (uses server.js via the package.json "start" script)
pnpm startThe included server.js is an ES module that:
- Serves the built Angular app from
dist/tmi-ux/browser/ - Provides a
/config.jsonendpoint for runtime configuration overrides throughTMI_*environment variables (see Configuration-Reference) - Applies rate limiting (1,000 requests per 15-minute window per IP)
- Falls back to
index.htmlfor all unmatched routes (SPA routing) - Defaults to port 8080 (configurable through the
PORTenvironment variable)
Create /etc/systemd/system/tmi-ux.service:
[Unit]
Description=TMI Web Application
After=network.target
[Service]
Type=simple
User=tmi
Group=tmi
WorkingDirectory=/opt/tmi-ux
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5
# Environment
Environment=NODE_ENV=production
Environment=PORT=8080
# Security
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetStart the service:
sudo systemctl daemon-reload
sudo systemctl enable tmi-ux
sudo systemctl start tmi-uxUse Docker for containerized deployment with consistent behavior across environments.
The repository's container image is built from Dockerfile.chainguard (Chainguard Node base image). It is a multi-stage build: the first stage runs pnpm run build:container to build the Angular app, and the second stage runs the Express server.js (not nginx) on port 8080. This means runtime configuration through TMI_* environment variables is supported. The k3s deployment script (scripts/deploy-k3s.sh) builds this same file.
# Build the image
docker build -f Dockerfile.chainguard -t tmi-ux:latest .
# Run the container
docker run -d \
--name tmi-ux \
-p 8080:8080 \
tmi-ux:latest
# Run with runtime configuration overrides
docker run -d \
--name tmi-ux \
-p 8080:8080 \
-e TMI_API_URL=https://api.tmi.example.com \
-e TMI_OPERATOR_NAME="My Organization" \
-e TMI_OPERATOR_CONTACT="[email protected]" \
tmi-ux:latestAdd TMI-UX to an existing docker-compose.yml:
services:
tmi-ux:
build:
context: ../tmi-ux
dockerfile: Dockerfile.chainguard
ports:
- "8080:8080"
environment:
- TMI_API_URL=http://tmi:8080
- TMI_OPERATOR_NAME=My Organization
restart: unless-stopped
depends_on:
- tmiUse the development server for local development and testing.
# Install dependencies
pnpm install
# Start the development server (opens the browser automatically)
pnpm run dev
# Application available at http://localhost:4200# Use a specific environment file
pnpm run dev:test # environment.test.ts
pnpm run dev:prod # environment.prod.tsThe dev command uses the development configuration by default, which loads environment.dev.ts. Additional build configurations (staging, local, shannon) are defined in angular.json and can be served with ng serve --configuration=<name>.
The following table lists the available environment settings. For the full configuration reference, including runtime overrides through TMI_* environment variables, see Configuration-Reference.
| Setting | Description | Default |
|---|---|---|
production |
Enable production mode | false |
logLevel |
Logging verbosity | 'ERROR' |
apiUrl |
API server URL | 'http://localhost:8080' |
authTokenExpiryMinutes |
Token validity in minutes | 60 |
operatorName |
Service operator name | 'TMI Operator' |
operatorContact |
Contact information | '[email protected]' |
serverPort |
Server listening port | 4200 |
serverInterface |
Server listening interface | '0.0.0.0' |
enableTLS |
Enable HTTPS | false |
tlsKeyPath |
TLS private key path | undefined |
tlsCertPath |
TLS certificate path | undefined |
-
Copy the example environment:
cp src/environments/environment.example.ts src/environments/environment.custom.ts
-
Edit the configuration:
export const environment = { production: true, apiUrl: 'https://api.tmi.example.com', logLevel: 'ERROR', // ... other settings };
-
Update
angular.jsonif needed, or use an existing configuration.
Set apiUrl to point to your TMI API server:
// Development (local API server)
apiUrl: 'http://localhost:8080'
// Production (separate API domain)
apiUrl: 'https://api.tmi.example.com'
// Production (same domain, different path)
apiUrl: 'https://tmi.example.com/api'# Check if the app loads
curl -I https://tmi.example.com
# Check static assets
curl -I https://tmi.example.com/main.js
# Test Angular routing (should return index.html with 200 OK)
curl https://tmi.example.com/threat-models# Run end-to-end tests against the deployed app
pnpm run e2e --base-url=https://tmi.example.com- Open DevTools Console.
- Verify that no errors appear on page load.
- Check the API URL in the Network tab.
- Verify WebSocket connections if you are using collaboration features.
# Production build with optimization
pnpm run build:prod
# Check bundle sizes
ls -lh dist/tmi-ux/*.jsAngular modules are already configured for lazy loading. To monitor chunk sizes:
# Analyze the bundle
pnpm run build:prod --stats-json
npx webpack-bundle-analyzer dist/tmi-ux/stats.jsonWhen deploying behind a CDN, follow these recommendations:
- Enable compression (gzip or Brotli)
-
Set cache headers: 1 year for hashed assets, no-cache for
index.html - Enable HTTP/2
-
Configure a custom 404 page to serve
index.html(required for Angular routing)
Check the browser console for errors:
-
API connection failed: Verify
apiUrlin your environment configuration. - 404 on assets: Check your server routing configuration.
- CORS errors: Configure CORS headers on your API server.
Verify your OAuth configuration:
- Confirm the redirect URI matches exactly (watch for trailing slash mismatches).
- Confirm the OAuth application has the correct callback URL.
- Confirm the Client ID matches your environment configuration.
For details, see Setting-Up-Authentication.
Check the Network tab in your browser's developer tools:
// Test API connectivity from the browser console
fetch('https://api.tmi.example.com/version')
.then(r => r.json())
.then(console.log)Common causes:
- CORS is not configured on the API server.
- The API URL is incorrect in your environment configuration.
- A network firewall is blocking requests.
This is a server configuration issue. Verify that your server falls back to index.html for unknown routes:
-
Nginx: Check that
try_files $uri $uri/ /index.htmlis present. -
Apache: Enable
mod_rewriteand add the.htaccessrules. -
S3/CloudFront: Configure error pages to serve
index.html.
For a comprehensive treatment, see Security-Best-Practices. The following are the key security measures for web application deployments:
- Content Security Policy: Configure CSP headers to restrict resource loading.
- HTTPS only: Never serve the application in production without TLS.
-
Security headers: Enable
X-Frame-Options,X-Content-Type-Options, and related headers. - Dependency updates: Regularly update npm packages.
- Environment secrets: Never commit API keys or secrets to version control.
- Subresource Integrity: Consider using SRI for CDN-hosted resources.
-
Build the new version:
pnpm run build:prod
-
Back up the current deployment:
sudo cp -r /var/www/tmi-ux /var/www/tmi-ux.backup
-
Deploy the new version:
sudo cp -r dist/tmi-ux/browser/* /var/www/tmi-ux/ -
Clear your CDN cache (if applicable).
-
Test the deployment.
-
Roll back if needed:
sudo rm -rf /var/www/tmi-ux sudo mv /var/www/tmi-ux.backup /var/www/tmi-ux
Use a blue-green deployment strategy:
- Deploy to a new directory (for example,
/var/www/tmi-ux-v2). - Update the nginx configuration to point to the new directory.
- Test the new deployment.
- Reload nginx:
sudo systemctl reload nginx - Remove the old deployment after verification.
- Setting-Up-Authentication -- Configure OAuth providers
- Component-Integration -- Connect the web app to the API server
- Post-Deployment -- Verify the deployment and test features