-
Notifications
You must be signed in to change notification settings - Fork 2
Deploying TMI Web Application
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 |
| Heroku | Platform-as-a-Service deployment | Heroku 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: 'support@example.com',
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:hosted-container # Heroku / hosted container deployments
pnpm run build:oci # Oracle Cloud Infrastructure
pnpm run build:container # Generic container (Chainguard, etc.)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 apache2# Create S3 bucket
aws s3 mb s3://tmi-ux-prod
# Upload files with long-lived cache headers
aws s3 sync dist/tmi-ux/browser/ s3://tmi-ux-prod/ \
--delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "index.html"
# Upload index.html without cache (so users always get the latest version)
aws s3 cp dist/tmi-ux/browser/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-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 includes several Dockerfiles for different deployment targets:
| File | Base Image | Target |
|---|---|---|
Dockerfile |
node:22-alpine |
Google Cloud Run / general container |
Dockerfile.oci |
Oracle Linux 9 | Oracle Cloud Infrastructure |
Dockerfile.chainguard |
Chainguard | Hardened container image |
All Dockerfiles use a multi-stage build: the first stage builds the Angular app, and the second stage runs the Express server.js (not nginx). This means runtime configuration through TMI_* environment variables is supported in all container deployments.
The default Dockerfile looks like this:
# Build stage
FROM --platform=$BUILDPLATFORM node:22-alpine AS builder
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm@10.18.3
# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm run build:hosted-container
# Production stage
FROM node:22-alpine
WORKDIR /app
# Create minimal package.json for runtime dependencies only
RUN echo '{"name":"tmi-ux-server","version":"1.0.0","type":"module","dependencies":{"express":"^5.1.0","express-rate-limit":"^8.1.0"}}' > package.json
RUN npm install --omit=dev --production
# Copy built application and server
COPY --from=builder /app/dist ./dist
COPY server.js ./
EXPOSE 8080
ENV PORT=8080
ENV NODE_ENV=production
CMD ["server.js"]# Build the image
docker build -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="support@example.com" \
tmi-ux:latestAdd TMI-UX to an existing docker-compose.yml:
services:
tmi-ux:
build:
context: ../tmi-ux
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- TMI_API_URL=http://tmi:8080
- TMI_OPERATOR_NAME=My Organization
restart: unless-stopped
depends_on:
- tmiYou can deploy 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 read runtime environment variables directly). 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: The Express server (
server.js) serves the built Angular app.
git push heroku mainThis triggers the following steps:
- Heroku installs dependencies using pnpm (through the buildpack).
- The
heroku-postbuildscript builds Angular with--configuration=hosted-container. - Heroku starts the Express server through
npm start.
Note: Heroku sets the
PORTenvironment variable automatically. Do not configure it manually.
Check the app status:
heroku logs --tail --app tmi-uxVisit the app:
heroku open --app tmi-uxThe app uses two buildpacks, in this order:
-
heroku/nodejs-- Node.js support -
https://github.com/unfold/heroku-buildpack-pnpm-- pnpm package manager
View the configured buildpacks:
heroku buildpacks --app tmi-uxTo change the Heroku environment configuration:
- Edit
src/environments/environment.hosted-container.tsdirectly, or run the configuration script 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 the 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 the build logs:
heroku logs --tail --app tmi-uxCommon causes:
- Missing buildpack: Ensure the pnpm buildpack is added.
- Build timeout: Check for large dependencies that slow the build.
The server uses process.env.PORT, which Heroku sets automatically. If the server fails to start, verify:
-
Procfilecontains:web: npm start -
server.jsuses:const port = process.env.PORT || 8080
Unlike typical server applications that read environment variables at runtime, Angular applications require environment configuration at build time. 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. Because it contains only public API URLs and operator information (no secrets), it is safe to commit.
Use 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 | '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 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
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS, OCI, GCP, Azure)
- Deploying TMI Server
- OCI Container Deployment
- Certificate Automation
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Configuration Management
- Config Migration Guide
- Database Operations
- Database Security Strategies
- Security Operations
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions