Skip to content

Deploying TMI Web Application

Eric Fitzgerald edited this page Jan 26, 2026 · 2 revisions

Deploying TMI Web Application

This guide covers deploying the TMI-UX Angular-based web application for production and development environments.

Overview

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

Quick Start

Choose your deployment method:

Prerequisites

  • Node.js 20+ (LTS recommended)
  • pnpm (package manager)
  • Access to TMI API server
  • OAuth provider credentials (if self-hosting authentication)

Building the Application

Install Dependencies

# 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 install

Environment Configuration

Create 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,
};

Build for Production

# 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.)

Static Hosting

Best for most deployments - serve pre-built files via CDN or web server.

Nginx Configuration

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;
}

Deploy to Nginx

# 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 nginx

Apache Configuration

For 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

CDN Deployment

AWS S3 + CloudFront

# 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

Netlify

# Install Netlify CLI
npm install -g netlify-cli

# Deploy
pnpm run build:prod
netlify deploy --prod --dir=dist/tmi-ux

Create 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"

Vercel

# Install Vercel CLI
npm install -g vercel

# Deploy
pnpm run build:prod
vercel --prod

Create vercel.json:

{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "dist/tmi-ux"
      }
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ]
}

Node.js Server

For custom server configuration or when you need server-side features.

Production Server

TMI-UX includes a production-ready Express server.

# Build application
pnpm run build:prod

# Start production server
pnpm run start:prod

Custom Configuration

Create 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}`);
});

Systemd Service

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.target

Start service:

sudo systemctl daemon-reload
sudo systemctl enable tmi-ux
sudo systemctl start tmi-ux

Docker Deployment

Containerized deployment for consistency across environments.

Dockerfile

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;"]

Nginx Config for Docker

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 and Run

# Build image
docker build -t tmi-ux:latest .

# Run container
docker run -d \
  --name tmi-ux \
  -p 4200:80 \
  tmi-ux:latest

Docker Compose

Add to existing docker-compose.yml:

services:
  tmi-ux:
    build:
      context: ../tmi-ux
      dockerfile: Dockerfile
    ports:
      - "4200:80"
    restart: unless-stopped
    depends_on:
      - tmi

Heroku Deployment

Deploy TMI-UX to Heroku using the included Express server and pnpm buildpack.

Prerequisites

  • Heroku CLI installed and authenticated
  • Access to a Heroku app configured for TMI-UX

How It Works

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:

  1. Environment File: src/environments/environment.hosted-container.ts contains the Heroku configuration
  2. Build Configuration: Angular's hosted-container build configuration uses this environment file
  3. Server Start: Express server (server.js) serves the built Angular app

Deploy to Heroku

git push heroku main

This triggers:

  1. Heroku installs dependencies using pnpm (via buildpack)
  2. Runs heroku-postbuild which builds Angular with --configuration=hosted-container
  3. Starts the Express server via npm start

Note: The PORT environment variable is automatically set by Heroku and should not be configured manually.

Verify Deployment

Check the app status:

heroku logs --tail --app tmi-ux

Visit the app:

heroku open --app tmi-ux

Buildpacks

The app uses two buildpacks (in order):

  1. heroku/nodejs - Node.js support
  2. https://github.com/unfold/heroku-buildpack-pnpm - pnpm package manager

View configured buildpacks:

heroku buildpacks --app tmi-ux

Modifying Configuration

To change the Heroku environment configuration:

  1. Edit src/environments/environment.hosted-container.ts directly, or
  2. Edit values in scripts/configure-heroku-env.sh and run it to regenerate the file:
    bash scripts/configure-heroku-env.sh
  3. Commit the changes:
    git add src/environments/environment.hosted-container.ts
    git commit -m "Update Heroku configuration"
  4. Deploy:
    git push heroku main

Files Involved

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

Troubleshooting

Build Failures

Check build logs:

heroku logs --tail --app tmi-ux

Common issues:

  • Missing buildpack: Ensure pnpm buildpack is added
  • Build timeout: Check for large dependencies

Server Won't Start

The server uses process.env.PORT which Heroku sets automatically. If the server fails to start, check that:

  • Procfile contains: web: npm start
  • server.js uses: const port = process.env.PORT || 8080

Architecture Note

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.

Development Server

For local development and testing.

Quick Start

# Install dependencies
pnpm install

# Start development server
pnpm run dev

# Application available at http://localhost:4200

Environment-Specific Development

# 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

Custom Development Server

# Set custom port and interface
export TMI_INTERFACE=0.0.0.0
export TMI_PORT=8080
pnpm run dev:custom

Development with TLS

# 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:4200

Configuration

Environment Variables

Available 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

Creating Custom Environment

  1. Copy example environment:
cp src/environments/environment.example.ts src/environments/environment.custom.ts
  1. Edit configuration:
export const environment = {
  production: true,
  apiUrl: 'https://api.tmi.example.com',
  logLevel: 'ERROR',
  // ... other settings
};
  1. Update angular.json (if needed) or use existing configurations.

API URL Configuration

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'

Testing Deployment

Smoke Tests

# 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)

Integration Tests

# Run e2e tests against deployed app
pnpm run e2e --base-url=https://tmi.example.com

Browser Console Checks

  1. Open DevTools Console
  2. Verify no errors on page load
  3. Check API URL in Network tab
  4. Verify WebSocket connections (if using collaboration features)

Performance Optimization

Build Optimization

# Production build with optimization
pnpm run build:prod

# Check bundle sizes
ls -lh dist/tmi-ux/*.js

Lazy Loading

Angular 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

CDN Configuration

  • 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

Troubleshooting

Blank Page on Load

Check browser console for errors:

  • API connection failed → Verify apiUrl in environment
  • 404 on assets → Check server routing configuration
  • CORS errors → Configure API server CORS headers

OAuth Redirect Fails

Verify OAuth configuration:

  1. Redirect URI matches exactly (no trailing slash issues)
  2. OAuth application has correct callback URL
  3. Client ID matches environment configuration

API Calls Fail

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

Angular Routing Not Working

Server configuration issue:

  • Nginx: Check try_files $uri $uri/ /index.html
  • Apache: Enable mod_rewrite and add .htaccess
  • S3/CloudFront: Configure error pages to serve index.html

Security Considerations

  1. Content Security Policy: Configure CSP headers to restrict resource loading
  2. HTTPS Only: Never serve in production without TLS
  3. Security Headers: Enable X-Frame-Options, X-Content-Type-Options, etc.
  4. Dependency Updates: Regularly update npm packages
  5. Environment Secrets: Never commit API keys or secrets to git
  6. Subresource Integrity: Consider using SRI for CDN resources

Updating Deployment

Rolling Update Process

  1. Build new version:
pnpm run build:prod
  1. Backup current deployment:
sudo cp -r /var/www/tmi-ux /var/www/tmi-ux.backup
  1. Deploy new version:
sudo cp -r dist/tmi-ux/* /var/www/tmi-ux/
  1. Clear CDN cache (if using CDN)
  2. Test deployment
  3. Roll back if needed:
sudo rm -rf /var/www/tmi-ux
sudo mv /var/www/tmi-ux.backup /var/www/tmi-ux

Zero-Downtime Deployment

Use blue-green deployment:

  1. Deploy to new directory (/var/www/tmi-ux-v2)
  2. Update nginx config to point to new directory
  3. Test new deployment
  4. Reload nginx: sudo systemctl reload nginx
  5. Remove old deployment after verification

Next Steps

Related Pages

Clone this wiki locally