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
# Build from YAML configuration
maplibre-arcgis build map.yaml --output dist/index.html
# Or build TypeScript application
npm run build
AWS S3 + CloudFront
# 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
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-map-app/*"
}]
}
Netlify
[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
{
"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.
<!-- 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
# 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
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
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
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
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:
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
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 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
# 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
<?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:
const auth = new ArcGISAuthManager({
portalUrl: 'https://www.arcgis.com',
authMethod: 'oauth2',
clientId: 'CLIENT_ID',
tokenStorage: 'sessionStorage', // or 'localStorage', 'memory'
});
Content Security Policy
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:
// 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:
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 |
With proper configuration, your MapLibre ArcGIS application is ready for production deployment. For enterprise-specific requirements, contact TMG Custom Solutions.