-
Notifications
You must be signed in to change notification settings - Fork 0
Deploying TMI Web Application
This guide covers deploying 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:
- Static Hosting - Fastest, CDN-friendly (recommended)
- Node.js Server - Custom server configuration
- Docker - Containerized deployment
- Heroku - Platform-as-a-Service deployment
- Development Server - Local development
- Node.js 20+ (LTS recommended)
- pnpm (package manager)
- Access to TMI API server
- OAuth provider credentials (if self-hosting authentication)
# Clone repository
git clone https://github.com/ericfitz/tmi-ux.git
cd tmi-ux
# Install pnpm if needed
npm install -g pnpm
# Install dependencies
pnpm installCreate environment file based on your deployment:
// src/environments/environment.prod.ts
export const environment = {
production: true,
logLevel: 'ERROR',
apiUrl: 'https://api.tmi.example.com', // Your TMI API server
authTokenExpiryMinutes: 60,
operatorName: 'TMI Operator',
operatorContact: 'support@example.com',
// Server config (for Node.js deployment)
serverPort: 4200,
serverInterface: '0.0.0.0',
enableTLS: false, // Handle TLS at reverse proxy
tlsKeyPath: undefined,
tlsCertPath: undefined,
};# Production build (optimized, minified)
pnpm run build:prod
# Output: dist/tmi-ux/Build output includes:
-
index.html- Main HTML file -
*.js- Bundled JavaScript (hashed for cache-busting) -
*.css- Compiled stylesheets -
assets/- Static assets (images, fonts, etc.)
Best for most deployments - serve pre-built files via 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 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/* /var/www/tmi-ux/
# Set permissions
sudo chown -R www-data:www-data /var/www/tmi-ux
# Enable 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 and restart:
sudo a2enmod rewrite ssl deflate expires headers
sudo a2ensite tmi-ux
sudo apache2ctl configtest
sudo systemctl reload apache2# Create S3 bucket
aws s3 mb s3://tmi-ux-prod
# Upload files
aws s3 sync dist/tmi-ux/ s3://tmi-ux-prod/ \
--delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "index.html"
# Upload index.html without cache
aws s3 cp dist/tmi-ux/index.html s3://tmi-ux-prod/ \
--cache-control "no-cache, no-store, must-revalidate"
# Configure CloudFront distribution
# - Origin: S3 bucket
# - Default root object: index.html
# - Error pages: 403, 404 -> /index.html (for Angular routing)
# - SSL certificate: ACM certificate
# - Compress objects automatically: Yes# Install Netlify CLI
npm install -g netlify-cli
# Deploy
pnpm run build:prod
netlify deploy --prod --dir=dist/tmi-uxCreate netlify.toml:
[build]
publish = "dist/tmi-ux"
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"
}
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/index.html"
}
]
}For custom server configuration or when you need server-side features.
TMI-UX includes a production-ready Express server.
# Build application
pnpm run build:prod
# Start production server
pnpm run start:prodCreate custom server script (server.js):
const express = require('express');
const path = require('path');
const compression = require('compression');
const helmet = require('helmet');
const app = express();
const PORT = process.env.PORT || 4200;
const DIST_DIR = path.join(__dirname, 'dist/tmi-ux');
// Security middleware
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.tmi.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
}
}));
// Compression
app.use(compression());
// Static files with cache headers
app.use(express.static(DIST_DIR, {
maxAge: '1y',
setHeaders: (res, filePath) => {
// Don't cache index.html
if (path.basename(filePath) === 'index.html') {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
}
}
}));
// Angular routing
app.get('*', (req, res) => {
res.sendFile(path.join(DIST_DIR, 'index.html'));
});
app.listen(PORT, () => {
console.log(`TMI-UX server running on port ${PORT}`);
});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=4200
# Security
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetStart service:
sudo systemctl daemon-reload
sudo systemctl enable tmi-ux
sudo systemctl start tmi-uxContainerized deployment for consistency across environments.
Create Dockerfile:
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm run build:prod
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist/tmi-ux /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Create nginx.conf:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css text/xml text/javascript
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;
# Static assets cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Angular routing
location / {
try_files $uri $uri/ /index.html;
}
}# Build image
docker build -t tmi-ux:latest .
# Run container
docker run -d \
--name tmi-ux \
-p 4200:80 \
tmi-ux:latestAdd to existing docker-compose.yml:
services:
tmi-ux:
build:
context: ../tmi-ux
dockerfile: Dockerfile
ports:
- "4200:80"
restart: unless-stopped
depends_on:
- tmiDeploy TMI-UX to Heroku using the included Express server and pnpm buildpack.
- Heroku CLI installed and authenticated
- Access to a Heroku app configured for TMI-UX
Angular applications require environment variables to be baked into the build at compile time (they cannot use runtime environment variables). TMI-UX uses a hosted-container-specific environment file that contains all configuration:
-
Environment File:
src/environments/environment.hosted-container.tscontains the Heroku configuration -
Build Configuration: Angular's
hosted-containerbuild configuration uses this environment file -
Server Start: Express server (
server.js) serves the built Angular app
git push heroku mainThis triggers:
- Heroku installs dependencies using pnpm (via buildpack)
- Runs
heroku-postbuildwhich builds Angular with--configuration=hosted-container - Starts the Express server via
npm start
Note: The
PORTenvironment variable is automatically set by Heroku and should not be configured manually.
Check the app status:
heroku logs --tail --app tmi-uxVisit the app:
heroku open --app tmi-uxThe app uses two buildpacks (in order):
-
heroku/nodejs- Node.js support -
https://github.com/unfold/heroku-buildpack-pnpm- pnpm package manager
View configured buildpacks:
heroku buildpacks --app tmi-uxTo change the Heroku environment configuration:
- Edit
src/environments/environment.hosted-container.tsdirectly, or - Edit values in
scripts/configure-heroku-env.shand run it to regenerate the file:bash scripts/configure-heroku-env.sh
- Commit the changes:
git add src/environments/environment.hosted-container.ts git commit -m "Update Heroku configuration" - Deploy:
git push heroku main
| File | Purpose |
|---|---|
scripts/configure-heroku-env.sh |
Script to generate environment.hosted-container.ts |
src/environments/environment.hosted-container.ts |
Heroku environment config (committed to repo) |
angular.json |
Contains hosted-container build configuration |
Procfile |
Tells Heroku how to run the app (web: npm start) |
package.json |
Contains heroku-postbuild and build:hosted-container scripts |
server.js |
Express server that serves the Angular app |
Check build logs:
heroku logs --tail --app tmi-uxCommon issues:
- Missing buildpack: Ensure pnpm buildpack is added
- Build timeout: Check for large dependencies
The server uses process.env.PORT which Heroku sets automatically. If the server fails to start, check that:
-
Procfilecontains:web: npm start -
server.jsuses:const port = process.env.PORT || 8080
Unlike typical server apps that read environment variables at runtime, Angular applications require environment configuration at build time. The configuration values are compiled directly into the JavaScript bundle.
The environment.hosted-container.ts file is committed to the repository and contains the configuration for Heroku deployments. Since it only contains public API URLs and operator information (no secrets), it is safe to commit.
For local development and testing.
# Install dependencies
pnpm install
# Start development server
pnpm run dev
# Application available at http://localhost:4200# Use specific environment file
pnpm run dev:staging # environment.staging.ts
pnpm run dev:test # environment.test.ts
pnpm run dev:prod # environment.prod.ts
pnpm run dev:local # environment.local.ts# Set custom port and interface
export TMI_INTERFACE=0.0.0.0
export TMI_PORT=8080
pnpm run dev:custom# Generate self-signed certificate
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
# Configure environment
export TMI_SSL=true
export TMI_SSL_KEY=./key.pem
export TMI_SSL_CERT=./cert.pem
pnpm run dev:custom
# Access at https://localhost:4200Available environment settings:
| Setting | Description | Default |
|---|---|---|
production |
Enable production mode | false |
logLevel |
Logging verbosity | 'ERROR' |
apiUrl |
API server URL | 'http://localhost:8080' |
authTokenExpiryMinutes |
Token validity | 60 |
operatorName |
Service operator name | 'TMI Operator' |
operatorContact |
Contact information | 'contact@example.com' |
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 example environment:
cp src/environments/environment.example.ts src/environments/environment.custom.ts- Edit configuration:
export const environment = {
production: true,
apiUrl: 'https://api.tmi.example.com',
logLevel: 'ERROR',
// ... other settings
};- Update
angular.json(if needed) or use existing configurations.
Important: 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 app loads
curl -I https://tmi.example.com
# Check static assets
curl -I https://tmi.example.com/main.js
# Test Angular routing
curl https://tmi.example.com/threat-models
# Should return index.html (200 OK)# Run e2e tests against deployed app
pnpm run e2e --base-url=https://tmi.example.com- Open DevTools Console
- Verify no errors on page load
- Check API URL in Network tab
- Verify WebSocket connections (if 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. Monitor chunk sizes:
# Analyze bundle
pnpm run build:prod --stats-json
npx webpack-bundle-analyzer dist/tmi-ux/stats.json- Enable compression (gzip/brotli)
- Set cache headers (1 year for assets, no-cache for index.html)
- Enable HTTP/2
- Configure custom 404 page to serve index.html
Check browser console for errors:
- API connection failed → Verify
apiUrlin environment - 404 on assets → Check server routing configuration
- CORS errors → Configure API server CORS headers
Verify OAuth configuration:
- Redirect URI matches exactly (no trailing slash issues)
- OAuth application has correct callback URL
- Client ID matches environment configuration
Check network tab:
# Test API connectivity from browser
fetch('https://api.tmi.example.com/version')
.then(r => r.json())
.then(console.log)Common issues:
- CORS not configured on API server
- API URL incorrect in environment
- Network firewall blocking requests
Server configuration issue:
- Nginx: Check
try_files $uri $uri/ /index.html - Apache: Enable
mod_rewriteand add.htaccess - S3/CloudFront: Configure error pages to serve index.html
- Content Security Policy: Configure CSP headers to restrict resource loading
- HTTPS Only: Never serve in production without TLS
- Security Headers: Enable X-Frame-Options, X-Content-Type-Options, etc.
- Dependency Updates: Regularly update npm packages
- Environment Secrets: Never commit API keys or secrets to git
- Subresource Integrity: Consider using SRI for CDN resources
- Build new version:
pnpm run build:prod- Backup current deployment:
sudo cp -r /var/www/tmi-ux /var/www/tmi-ux.backup- Deploy new version:
sudo cp -r dist/tmi-ux/* /var/www/tmi-ux/- Clear CDN cache (if using CDN)
- Test deployment
- Roll back if needed:
sudo rm -rf /var/www/tmi-ux
sudo mv /var/www/tmi-ux.backup /var/www/tmi-uxUse blue-green deployment:
- Deploy to new directory (
/var/www/tmi-ux-v2) - Update nginx config to point to new directory
- Test new deployment
- Reload nginx:
sudo systemctl reload nginx - Remove old deployment after verification
- Setup Authentication - Configure OAuth providers
- Component Integration - Connect web app to API server
- Post-Deployment - Verify deployment and test features
- Using TMI for Threat Modeling
- Accessing TMI
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Metadata and Extensions
- Planning Your Deployment
- Deploying TMI Server
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Monitoring and Health
- Database Operations
- Security Operations
- Performance and Scaling
- Maintenance Tasks