A simulator that provides endpoints to mimic the functionality of Azure Event Grid.
10K+
The Azure Event Grid Simulator provides a local development environment that mimics Azure Event Grid functionality. This guide covers everything you need to run the simulator using Docker.
GitHub Repository: https://github.com/pm7y/AzureEventGridSimulator
Docker Hub: pmcilreavy/azureeventgridsimulator
Supported Platforms: linux/amd64, linux/arm64
The simulator requires HTTPS. Generate a development certificate:
# Trust the certificate (one-time setup)
dotnet dev-certs https --trust
# Export the certificate (dev-certs won't create the folder)
mkdir -p certs
dotnet dev-certs https \
--export-path ./certs/eventgrid.pfx \
--password password123
# The container runs as UID 1654 and dev-certs exports the file as 0600
chmod a+r ./certs/eventgrid.pfx
Create config/appsettings.json:
{
"topics": [
{
"name": "my-topic",
"port": 60101,
"key": "TheLocal+DevelopmentKey=",
"subscribers": {
"http": [
{
"name": "webhook-subscriber",
"endpoint": "https://webhook.site/your-unique-id",
"disableValidation": true
}
]
}
}
]
}
docker run -d \
--name eventgrid \
-p 60101:60101 \
-v $(pwd)/certs:/certs:ro \
-v $(pwd)/config:/config:ro \
-e ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/eventgrid.pfx \
-e ASPNETCORE_Kestrel__Certificates__Default__Password=password123 \
-e AEGS_ConfigFile=/config/appsettings.json \
pmcilreavy/azureeventgridsimulator:latest
curl -k \
-H "Content-Type: application/json" \
-H "aeg-sas-key: TheLocal+DevelopmentKey=" \
-X POST "https://localhost:60101/api/events?api-version=2018-01-01" \
-d '[{
"id": "test-event-1",
"subject": "/orders/12345",
"eventType": "Order.Created",
"eventTime": "2025-01-15T10:00:00Z",
"data": {
"orderId": "12345",
"customerId": "cust-789",
"total": 99.99
},
"dataVersion": "1.0"
}]'
The simulator can be configured through multiple methods (in order of precedence):
AEGS_)AEGS_ConfigFile)Mount your configuration file and set the AEGS_ConfigFile environment variable:
docker run -d \
-v /path/to/config:/config:ro \
-e AEGS_ConfigFile=/config/appsettings.json \
pmcilreavy/azureeventgridsimulator:latest
Configure topics and subscribers directly via environment variables. Use double underscores (__) for nested properties:
docker run -d \
-e AEGS_Topics__0__name=my-topic \
-e AEGS_Topics__0__port=60101 \
-e AEGS_Topics__0__key=MySecretKey= \
-e AEGS_Topics__0__subscribers__http__0__name=webhook \
-e AEGS_Topics__0__subscribers__http__0__endpoint=https://example.com/webhook \
-e AEGS_Topics__0__subscribers__http__0__disableValidation=true \
pmcilreavy/azureeventgridsimulator:latest
Each topic listens on its own port and can have multiple subscribers.
Publish each topic's port on the same host port, e.g. -p 60101:60101 for a topic with "port": 60101. The validation URL that the simulator sends to HTTP subscribers (https://<address>:<port>/validate?id=...) is built from the topic's configured port, not the published host port, so with -p 8443:60101 a subscriber that follows it through the Docker host can't complete the validation handshake. Events posted through a remapped port still reach the topic, because the simulator picks the topic by the port the connection arrives on inside the container. Subscribers with disableValidation: true aren't sent a validation URL.
| Property | Required | Description |
|---|---|---|
name | Yes | Topic name (letters, numbers, dashes only) |
port | Yes | Port number the topic listens on |
key | No | SAS key for authentication (null = no validation) |
disabled | No | Set to true to disable the topic |
inputSchema | No | EventGridSchema or CloudEventV1_0 (auto-detect if null) |
outputSchema | No | Schema for delivery to subscribers |
serviceBusConnectionString | No | Default connection string for Service Bus subscribers |
storageQueueConnectionString | No | Default connection string for Storage Queue subscribers |
eventHubConnectionString | No | Default connection string for Event Hub subscribers |
{
"topics": [
{
"name": "orders-topic",
"port": 60101,
"key": "OrdersTopicKey=",
"inputSchema": "EventGridSchema",
"subscribers": { }
},
{
"name": "notifications-topic",
"port": 60102,
"key": "NotificationsKey=",
"inputSchema": "CloudEventV1_0",
"subscribers": { }
}
]
}
A topic can deliver to four subscriber types, each in its own array under subscribers: http, serviceBus, storageQueue and eventHub. This page has one example of each; the wiki has the full reference.
Every subscriber type accepts these settings:
| Property | Required | Description |
|---|---|---|
name | Yes | Subscriber name (letters, numbers, dashes only; unique within the topic) |
disabled | No | Set to true to disable the subscriber |
deliverySchema | No | EventGridSchema or CloudEventV1_0 (defaults to the topic's outputSchema, then the schema the event arrived in) |
filter | No | Event filtering rules, see Filtering |
retryPolicy | No | Retry settings, see Retry and Dead-Letter |
deadLetter | No | Where undeliverable events are written. Without it they're dropped, see Retry and Dead-Letter |
Needs an endpoint. Set disableValidation to true to skip the subscription validation handshake. Reference: HTTP Subscribers.
{
"http": [
{
"name": "order-processor",
"endpoint": "https://myapp.local/api/events",
"disableValidation": true,
"filter": { "includedEventTypes": ["Order.Created", "Order.Updated"] }
}
]
}
Needs a connectionString (or namespace, sharedAccessKeyName and sharedAccessKey, or the topic's serviceBusConnectionString) and either a queue or a topic. Reference: Service Bus Subscribers.
{
"serviceBus": [
{
"name": "orders-queue-subscriber",
"connectionString": "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...",
"queue": "orders-queue",
"properties": {
"EventType": { "type": "dynamic", "value": "EventType" },
"CustomerId": { "type": "dynamic", "value": "data.customerId" },
"Source": { "type": "static", "value": "EventGridSimulator" }
}
}
]
}
properties (Service Bus and Event Hub) become application properties on each message. A static value is used as-is. A dynamic value is a path into the event: Id, Subject, EventType, EventTime, DataVersion, Source, Topic, or data.propertyName / data.nested.property.
Needs a queueName and a connectionString (or the topic's storageQueueConnectionString). Reference: Storage Queue Subscribers.
{
"storageQueue": [
{
"name": "audit-queue-subscriber",
"connectionString": "DefaultEndpointsProtocol=https;AccountName=mystorageaccount;AccountKey=...;EndpointSuffix=core.windows.net",
"queueName": "audit-events"
}
]
}
Needs an eventHubName and a connectionString (or namespace, sharedAccessKeyName and sharedAccessKey, or the topic's eventHubConnectionString). Reference: Event Hub Subscribers.
{
"eventHub": [
{
"name": "orders-eventhub-subscriber",
"connectionString": "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...",
"eventHubName": "orders-events",
"properties": {
"OrderId": { "type": "dynamic", "value": "data.orderId" }
}
}
]
}
Failed deliveries are retried with Azure Event Grid's exponential backoff, and a subscriber's deadLetter settings write undeliverable events to JSON files (see Container User and File Permissions below to keep them outside the container). Dead-lettering is opt-in: it only happens when a subscriber has a deadLetter object with at least one setting, e.g. { "enabled": true } (an empty {} is ignored; enabled: true and folderPath: ./dead-letters are the defaults inside it), and without one, events that exhaust their retries or get a 400, 401, 403 or 413 response are dropped. Subscribers can also filter on event type, subject and advanced conditions on event fields and data. The settings, schedules, operators and limits are in Retry and Dead-Letter and Filtering.
The simulator serves a web dashboard showing received events, delivery attempts and rejected requests. It's served on each enabled topic's port, for example https://localhost:60101/dashboard, and also on dashboardPort if you set one (publish that port too, e.g. -p 5000:5000). Turn it off with -e AEGS_dashboardEnabled=false. The dashboard and its API have no authentication, so on an untrusted network either turn it off or publish ports on the host's loopback address only, e.g. -p 127.0.0.1:60101:60101 (see Dashboard security). More: Dashboard.
The simulator requires HTTPS (matching Azure Event Grid's behavior).
# Trust the certificate locally
dotnet dev-certs https --trust
# Export for Docker (dev-certs won't create the folder)
mkdir -p certs
dotnet dev-certs https \
--export-path ./certs/eventgrid.pfx \
--password YourSecurePassword123
# The container runs as UID 1654 and dev-certs exports the file as 0600
chmod a+r ./certs/eventgrid.pfx
Use any PFX certificate:
docker run -d \
-v /path/to/certs:/certs:ro \
-e ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/mycert.pfx \
-e ASPNETCORE_Kestrel__Certificates__Default__Password=certpassword \
pmcilreavy/azureeventgridsimulator:latest
When subscribers use self-signed certificates, enable acceptance in the simulator:
-e AEGS_dangerousAcceptAnyServerCertificateValidator=true
Warning: Only use this in development environments.
The image runs as the non-root app user (UID 1654), not as root. Inside the image, the /app folder and the default dead-letter folder /app/dead-letters belong to that user. Bind mounts keep their host ownership and permissions, so on Linux hosts (Docker Desktop on macOS and Windows usually handles this for you):
Certificates and config files you mount must be readable by UID 1654. A .pfx that only your host user can read (mode 0600) fails to load, and dotnet dev-certs https --export-path writes exactly that on Linux and macOS; make it readable, e.g. chmod a+r certs/eventgrid.pfx (the Quick Start does this).
Dead-letter and log folders you mount must be writable by UID 1654. For example:
mkdir -p dead-letters && sudo chown 1654 dead-letters
docker run ... -v $(pwd)/dead-letters:/app/dead-letters pmcilreavy/azureeventgridsimulator:latest
If the folder isn't writable, the dead-letter file is not written and the simulator only logs an error.
Alternatively, run as your own user with --user "$(id -u):$(id -g)" (Compose: user:). That user can't write to the image's own /app/dead-letters, so mount a dead-letter folder as shown above.
Topic ports below 1024 need extra privileges on some container runtimes (Docker Engine 20.10+ allows them), so prefer ports above 1024, as the examples do.
# docker-compose.yml
services:
eventgrid:
image: pmcilreavy/azureeventgridsimulator:latest
ports:
- "60101:60101"
volumes:
- ./certs:/certs:ro
- ./config:/config:ro
environment:
- ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/eventgrid.pfx
- ASPNETCORE_Kestrel__Certificates__Default__Password=password123
- AEGS_ConfigFile=/config/appsettings.json
This setup includes:
# docker-compose.yml
services:
eventgrid:
image: pmcilreavy/azureeventgridsimulator:latest
container_name: eventgrid
ports:
- "60101:60101"
- "60102:60102"
volumes:
# on Linux the pfx and config must be readable by UID 1654 (see Container User and File Permissions)
- ./docker:/aegs:ro
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_Kestrel__Certificates__Default__Path=/aegs/azureEventGridSimulator.pfx
- ASPNETCORE_Kestrel__Certificates__Default__Password=Y0urSup3rCrypt1cPa55w0rd!
- TZ=UTC
- AEGS_ConfigFile=/aegs/appsettings.json
- AEGS_dangerousAcceptAnyServerCertificateValidator=true
depends_on:
- seq
- azurite
- servicebus-emulator
seq:
image: datalust/seq:latest
container_name: seq
ports:
- "8081:80"
- "5341:5341"
environment:
- ACCEPT_EULA=Y
azurite:
image: mcr.microsoft.com/azure-storage/azurite:latest
container_name: azurite
ports:
- "10000:10000" # Blob
- "10001:10001" # Queue
- "10002:10002" # Table
command: "azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0"
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: mssql
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=YourStrong@Passw0rd!
servicebus-emulator:
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
container_name: servicebus-emulator
ports:
- "5672:5672"
volumes:
- ./docker/servicebus-config.json:/ServiceBus_Emulator/ConfigFiles/Config.json:ro
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=YourStrong@Passw0rd!
- SQL_SERVER=mssql
depends_on:
- mssql
docker/appsettings.json:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information"
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Seq",
"Args": { "serverUrl": "http://seq:5341" }
}
]
},
"topics": [
{
"name": "orders-topic",
"port": 60101,
"key": "OrdersTopicKey=",
"subscribers": {
"http": [
{
"name": "order-webhook",
"endpoint": "https://webhook.site/your-unique-id",
"disableValidation": true,
"filter": {
"includedEventTypes": ["Order.Created"]
}
}
],
"serviceBus": [
{
"name": "order-queue",
"connectionString": "Endpoint=sb://servicebus-emulator;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
"queue": "orders",
"properties": {
"OrderId": { "type": "dynamic", "value": "data.orderId" }
}
}
],
"storageQueue": [
{
"name": "order-audit",
"connectionString": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://azurite:10001/devstoreaccount1;",
"queueName": "order-audit"
}
]
}
},
{
"name": "notifications-topic",
"port": 60102,
"key": "NotificationsKey=",
"subscribers": {
"http": [
{
"name": "notification-handler",
"endpoint": "https://myapp.local/notifications",
"disableValidation": true
}
]
}
}
]
}
docker/servicebus-config.json:
{
"UserConfig": {
"Namespaces": [
{
"Name": "default",
"Queues": [
{ "Name": "orders" },
{ "Name": "notifications" }
]
}
],
"Logging": {
"Type": "Console"
}
}
}
No config file required - configure everything via environment variables:
# docker-compose.yml
services:
eventgrid:
image: pmcilreavy/azureeventgridsimulator:latest
ports:
- "60101:60101"
volumes:
- ./certs:/certs:ro
environment:
# Certificate
- ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/eventgrid.pfx
- ASPNETCORE_Kestrel__Certificates__Default__Password=password123
# Topic configuration
- AEGS_Topics__0__name=my-topic
- AEGS_Topics__0__port=60101
- AEGS_Topics__0__key=MyTopicKey=
# HTTP subscriber
- AEGS_Topics__0__subscribers__http__0__name=webhook
- AEGS_Topics__0__subscribers__http__0__endpoint=https://webhook.site/your-id
- AEGS_Topics__0__subscribers__http__0__disableValidation=true
| Variable | Description |
|---|---|
ASPNETCORE_ENVIRONMENT | Environment name (Development, Production) |
ASPNETCORE_Kestrel__Certificates__Default__Path | Path to HTTPS certificate |
ASPNETCORE_Kestrel__Certificates__Default__Password | Certificate password |
| Variable | Description |
|---|---|
AEGS_ConfigFile | Path to configuration JSON file |
AEGS_dangerousAcceptAnyServerCertificateValidator | Accept self-signed subscriber certs |
AEGS_dashboardEnabled | Set to false to turn off the dashboard (default: true) |
AEGS_dashboardPort | Extra port to serve the dashboard on (publish it too) |
AEGS_eventValidationLimits__maximumOverallMessageSizeInBytes | Maximum request body size (default: 1536000) |
AEGS_eventValidationLimits__maximumEventSizeInBytes | Maximum size of a single event (default: 1049600) |
AEGS_Topics__[index]__name | Topic name |
AEGS_Topics__[index]__port | Topic port |
AEGS_Topics__[index]__key | Topic SAS key |
AEGS_Topics__[index]__disabled | Disable topic |
AEGS_Topics__[index]__inputSchema | Input schema |
AEGS_Topics__[index]__outputSchema | Output schema |
AEGS_Serilog__MinimumLevel__Default | Log level (Verbose, Debug, Information, Warning, Error) |
| Variable | Description |
|---|---|
TZ | Timezone (e.g., UTC, America/New_York, Europe/London) |
Endpoint: POST https://localhost:{port}/api/events?api-version=2018-01-01
Headers:
Content-Type: application/jsonaeg-sas-key: {your-topic-key} (if key is configured)Event Grid Schema:
[
{
"id": "unique-event-id",
"subject": "/orders/12345",
"eventType": "Order.Created",
"eventTime": "2025-01-15T10:30:00Z",
"data": {
"orderId": "12345",
"amount": 99.99
},
"dataVersion": "1.0"
}
]
CloudEvents Schema:
[
{
"specversion": "1.0",
"id": "unique-event-id",
"source": "/orders",
"type": "Order.Created",
"time": "2025-01-15T10:30:00Z",
"data": {
"orderId": "12345",
"amount": 99.99
}
}
]
Endpoint: GET https://localhost:{port}/api/health
Response: OK
The simulator integrates with Seq for structured logging.
Add Seq configuration:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information"
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Seq",
"Args": {
"serverUrl": "http://seq:5341"
}
}
]
}
}
Access the Seq UI at http://localhost:8081 (when using the Docker Compose example above).
Problem: The remote certificate is invalid
Solution: Ensure your certificate is trusted or enable self-signed cert acceptance:
-e AEGS_dangerousAcceptAnyServerCertificateValidator=true
Problem: Address already in use
Solution: Ensure each topic uses a unique port and that ports are not in use by other applications.
Problem: The certificate can't be read, or dead-letter or log files aren't written (Access to the path ... is denied)
Solution: The container runs as UID 1654. See Container User and File Permissions above.
Checklist:
disableValidation is true for developmentWhen subscribers run in other containers, use Docker network names:
{
"endpoint": "https://myapp:5000/webhook"
}
For external endpoints, ensure the container can reach them (check DNS, firewalls).
Content type
Image
Digest
sha256:6ffe00820…
Size
50.7 MB
Last updated
3 days ago
docker pull pmcilreavy/azureeventgridsimulator