Overview

MapLibre ArcGIS applications can be deployed as static HTML files or integrated into larger applications. This guide covers deployment options from simple static hosting to enterprise-grade infrastructure.

flowchart TB
    subgraph Build["Build Process"]
        YAML[YAML Config]
        TS[TypeScript App]
        CLI[Python CLI]
    end
    
    subgraph Outputs["Output Options"]
        STATIC[Static HTML]
        BUNDLE[JS Bundle]
        DOCKER[Docker Image]
    end
    
    subgraph Hosting["Hosting Platforms"]
        S3[AWS S3]
        NETLIFY[Netlify/Vercel]
        CDN[CDN]
        ENTERPRISE[Enterprise Server]
    end
    
    YAML --> CLI --> STATIC
    TS --> BUNDLE
    
    STATIC --> Hosting
    BUNDLE --> Hosting
    DOCKER --> ENTERPRISE
          

Static Hosting

The simplest deployment option is static HTML hosting. Works with any web server or CDN.

Build Static HTML

Terminal
# Build from YAML configuration
maplibre-arcgis build map.yaml --output dist/index.html

# Or build TypeScript application
npm run build

AWS S3 + CloudFront

Terminal
# Create S3 bucket
aws s3 mb s3://my-map-app

# Enable static website hosting
aws s3 website s3://my-map-app --index-document index.html

# Upload files
aws s3 sync dist/ s3://my-map-app --delete

# Set bucket policy for public access
aws s3api put-bucket-policy --bucket my-map-app --policy file://policy.json

S3 Bucket Policy

policy.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "PublicReadGetObject",
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-map-app/*"
  }]
}

Netlify

netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[[headers]]
  for = "/*"
  [headers.values]
    # Required for DuckDB WASM
    Cross-Origin-Opener-Policy = "same-origin"
    Cross-Origin-Embedder-Policy = "require-corp"
    
    # Security headers
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"

Vercel

vercel.json
{
  "headers": [{
    "source": "/(.*)",
    "headers": [
      { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
      { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
    ]
  }]
}

CDN Usage

Load MapLibre ArcGIS directly from a CDN for quick prototyping or simple deployments.

HTML
<!-- MapLibre GL JS -->
<script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet" />

<!-- MapLibre ArcGIS -->
<script src="https://unpkg.com/maplibre-arcgis@latest/dist/maplibre-arcgis.js"></script>
<link href="https://unpkg.com/maplibre-arcgis@latest/dist/maplibre-arcgis.css" rel="stylesheet" />

<script>
  const map = new maplibregl.Map({
    container: 'map',
    style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
  });
  
  map.on('load', () => {
    const auth = new maplibreArcgis.ArcGISAuthManager({
      portalUrl: 'https://www.arcgis.com',
      authMethod: 'apikey',
      apiKey: 'YOUR_API_KEY',
    });
    
    map.addControl(new maplibreArcgis.EsriFeatureServiceControl({
      id: 'layer',
      url: 'https://.../FeatureServer/0',
      auth,
    }), 'top-left');
  });
</script>

Docker Deployment

Containerize your application for consistent deployments across environments.

Dockerfile

Dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx.conf

nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Required for DuckDB WASM
    add_header Cross-Origin-Opener-Policy same-origin;
    add_header Cross-Origin-Embedder-Policy require-corp;
    
    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Docker Compose

docker-compose.yml
version: '3.8'

services:
  map-app:
    build: .
    ports:
      - "8080:80"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/"]
      interval: 30s
      timeout: 10s
      retries: 3

Kubernetes Deployment

Deploy to Kubernetes for scalable, production-grade hosting.

Deployment

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: maplibre-arcgis-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: maplibre-arcgis
  template:
    metadata:
      labels:
        app: maplibre-arcgis
    spec:
      containers:
      - name: nginx
        image: maplibre-arcgis:latest
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 30

Service and Ingress

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: maplibre-arcgis-service
spec:
  selector:
    app: maplibre-arcgis
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: maplibre-arcgis-ingress
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
spec:
  rules:
  - host: maps.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: maplibre-arcgis-service
            port:
              number: 80

Enterprise Deployment

For enterprise deployments behind firewalls with ArcGIS Enterprise.

Reverse Proxy Configuration

When deploying behind a corporate reverse proxy, ensure proper header forwarding:

nginx-proxy.conf
server {
    listen 443 ssl;
    server_name maps.company.com;

    ssl_certificate /etc/ssl/certs/company.crt;
    ssl_certificate_key /etc/ssl/private/company.key;

    # Proxy to ArcGIS Enterprise
    location /arcgis/ {
        proxy_pass https://enterprise.company.com/arcgis/;
        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;
    }

    # Serve static map app
    location / {
        root /var/www/map-app;
        try_files $uri $uri/ /index.html;
        
        # DuckDB WASM headers
        add_header Cross-Origin-Opener-Policy same-origin;
        add_header Cross-Origin-Embedder-Policy require-corp;
    }
}

Internal ArcGIS Enterprise

map.yaml
auth:
  portal_url: https://enterprise.company.com/portal
  method: oauth2
  client_id: INTERNAL_CLIENT_ID

layers:
  - id: internal-parcels
    type: feature_service
    url: https://enterprise.company.com/arcgis/rest/services/Parcels/FeatureServer/0

CORS Configuration

⚠ DuckDB WASM Requirements

DuckDB WASM requires specific CORS headers. Without these headers, the DuckDB functionality will not work.

Required Headers

Header Value Purpose
Cross-Origin-Opener-Policy same-origin Isolates browsing context
Cross-Origin-Embedder-Policy require-corp Enables SharedArrayBuffer

Apache Configuration

.htaccess
# DuckDB WASM headers
Header set Cross-Origin-Opener-Policy "same-origin"
Header set Cross-Origin-Embedder-Policy "require-corp"

# Cache static assets
<FilesMatch "\.(js|css|png|jpg|svg|woff2)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

IIS Configuration

web.config
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Cross-Origin-Opener-Policy" value="same-origin" />
        <add name="Cross-Origin-Embedder-Policy" value="require-corp" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Security Considerations

API Keys

Never expose API keys in client-side code for production applications. Use OAuth2 instead, or proxy requests through your backend.

Token Storage

Tokens are stored in memory by default. For persistent sessions, consider secure storage options:

TypeScript
const auth = new ArcGISAuthManager({
  portalUrl: 'https://www.arcgis.com',
  authMethod: 'oauth2',
  clientId: 'CLIENT_ID',
  tokenStorage: 'sessionStorage', // or 'localStorage', 'memory'
});

Content Security Policy

CSP Header
Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'wasm-unsafe-eval' cdn.jsdelivr.net unpkg.com;
  style-src 'self' 'unsafe-inline' fonts.googleapis.com;
  font-src 'self' fonts.gstatic.com;
  img-src 'self' data: blob: *.arcgis.com *.tile.openstreetmap.org basemaps.cartocdn.com;
  connect-src 'self' *.arcgis.com s3.amazonaws.com;

Performance Optimization

Bundle Size

Tree-shaking removes unused code. Import only what you need:

TypeScript
// Good - tree-shakeable
import { ArcGISAuthManager, EsriFeatureServiceControl } from 'maplibre-arcgis';

// Avoid - imports everything
import * as MapLibreArcGIS from 'maplibre-arcgis';

Lazy Loading

Load heavy dependencies (DuckDB, Pyodide) on demand:

TypeScript
const loadDuckDB = async () => {
  const { DuckDBLayerControl } = await import('maplibre-arcgis/duckdb');
  return new DuckDBLayerControl();
};

// Load when user clicks a button
button.addEventListener('click', async () => {
  const control = await loadDuckDB();
  map.addControl(control, 'top-left');
});

Caching Strategies

Asset Type Cache Strategy Duration
JS/CSS Bundles Cache with version hash 1 year (immutable)
Map Tiles Cache-Control 7 days
COG/COPC Data Range requests 1 day
index.html No cache Always fresh
✓ Ready for Production

With proper configuration, your MapLibre ArcGIS application is ready for production deployment. For enterprise-specific requirements, contact TMG Custom Solutions.