ElasticMQ is an in-memory message queue with an SQS-compatible API, a lightweight alternative to AWS SQS for dev, test, or infrastructure-independent production use, supporting standard/FIFO queues, dead-letter queues, visibility timeouts, and optional persistence. This guide deploys it via Docker behind Traefik with TLS, configures queues and DLQs, sets up the web UI, enables persistence, and covers migrating off AWS SQS.
ElasticMQ vs. AWS SQS
- Deployment: SQS is fully managed across AWS AZs. ElasticMQ runs on any infrastructure you control.
- API: ElasticMQ implements the SQS query API, point your existing AWS SDK client at it with minimal changes.
- Pricing: SQS charges per request + data transfer. ElasticMQ is free beyond your infra cost.
- Persistence: SQS replicates across data centers. ElasticMQ is in-memory by default, with optional H2 database persistence.
- Feature parity: SQS has server-side encryption, VPC endpoints, CloudWatch. ElasticMQ focuses on core queue functionality only.
Concept Mapping
| AWS SQS | ElasticMQ | Description |
|---|---|---|
| Standard Queues | Standard Queues | At-least-once delivery, best-effort ordering |
| FIFO Queues | FIFO Queues | Exactly-once, strict ordering |
| Dead-Letter Queues | Dead-Letter Queues | Stores messages exceeding max receive count |
| Visibility Timeout | Visibility Timeout | Time a message is hidden after receipt |
| Message Delay | Delay Seconds | Time before a message becomes consumable |
| Long Polling | Receive Message Wait | Waits for messages instead of returning empty |
Architecture:
- REST-SQS interface — HTTP API on port 9324, SQS-compatible
- Queue storage — in-memory by default; optional H2 persistence
-
Web UI — separate
elasticmq-uicontainer on port 3000 - Config — HOCON files define queues, DLQ policies, server settings at startup
Prerequisites: a Linux server, Docker + Docker Compose, a domain A record for the REST API, and a second subdomain (e.g.
ui.example.com) for the web UI, it serves static assets from absolute root paths so it can't share a path prefix with the API domain.
Deploy with Docker
$ mkdir -p ~/elasticmq/{data,letsencrypt}
$ cd ~/elasticmq
Configure ElasticMQ. Replace DOMAIN-NAME:
$ nano elasticmq.conf
include classpath("application.conf")
node-address {
protocol = https
host = DOMAIN-NAME
port = 443
context-path = ""
}
rest-sqs {
enabled = true
bind-port = 9324
bind-hostname = "0.0.0.0"
sqs-limits = strict
}
queues {
default-queue {
defaultVisibilityTimeout = 30 seconds
delay = 0 seconds
receiveMessageWait = 0 seconds
}
}
aws {
region = us-east-1
accountId = 000000000000
}
node-address is the external URL embedded in queue URLs the API returns — must match what Traefik serves.
Generate a UI password hash:
$ openssl passwd -apr1 UI-PASSWORD
Create .env:
$ nano .env
DOMAIN=DOMAIN-NAME
UI_DOMAIN=UI-DOMAIN-NAME
LETSENCRYPT_EMAIL=ADMIN-EMAIL
UI_USERNAME=UI-USERNAME
UI_PASSWORD_HASH=UI-PASSWORD-HASH
Double every $ in the password hash (e.g. $apr1$abc123 → $$apr1$$abc123). Docker Compose treats a single $ as a variable reference.
Docker Compose:
$ nano docker-compose.yml
services:
traefik:
image: "traefik:v3.7.0"
container_name: "traefik"
restart: unless-stopped
command:
- "--log.level=INFO"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
- elasticmq
elasticmq:
image: softwaremill/elasticmq-native:1.7.1
container_name: elasticmq
volumes:
- ./elasticmq.conf:/opt/elasticmq.conf
- ./data:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.elasticmq-api.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.elasticmq-api.entrypoints=websecure"
- "traefik.http.routers.elasticmq-api.tls.certresolver=myresolver"
- "traefik.http.services.elasticmq-api.loadbalancer.server.port=9324"
restart: unless-stopped
networks:
- elasticmq
elasticmq-ui:
image: softwaremill/elasticmq-ui:1.7.1
container_name: elasticmq-ui
environment:
- SQS_ENDPOINT=http://elasticmq:9324
depends_on:
- elasticmq
labels:
- "traefik.enable=true"
- "traefik.http.routers.elasticmq-ui.rule=Host(`${UI_DOMAIN}`)"
- "traefik.http.routers.elasticmq-ui.entrypoints=websecure"
- "traefik.http.routers.elasticmq-ui.tls.certresolver=myresolver"
- "traefik.http.services.elasticmq-ui.loadbalancer.server.port=3000"
- "traefik.http.middlewares.elasticmq-ui-auth.basicauth.users=${UI_USERNAME}:${UI_PASSWORD_HASH}"
- "traefik.http.routers.elasticmq-ui.middlewares=elasticmq-ui-auth"
restart: unless-stopped
networks:
- elasticmq
networks:
elasticmq:
Neither app container publishes a host port. Traefik reaches both over the internal elasticmq network and terminates TLS for each domain separately.
$ docker compose up -d
$ docker compose ps
$ docker compose logs traefik
Three containers running; Traefik logs should show Register... for myresolver.acme with no ERR lines.
Configure Queues
$ nano ~/elasticmq/elasticmq.conf
queues {
default-queue {
defaultVisibilityTimeout = 30 seconds
delay = 0 seconds
receiveMessageWait = 0 seconds
}
orders-queue {
defaultVisibilityTimeout = 60 seconds
delay = 0 seconds
receiveMessageWait = 20 seconds
}
notifications-queue {
defaultVisibilityTimeout = 30 seconds
delay = 5 seconds
receiveMessageWait = 10 seconds
}
batch-processing-queue {
defaultVisibilityTimeout = 300 seconds
delay = 0 seconds
receiveMessageWait = 20 seconds
}
}
$ docker compose restart elasticmq
Set Up Dead-Letter Queues
$ nano ~/elasticmq/elasticmq.conf
orders-queue {
defaultVisibilityTimeout = 60 seconds
delay = 0 seconds
receiveMessageWait = 20 seconds
deadLettersQueue {
name = "orders-dlq"
maxReceiveCount = 3
}
}
orders-dlq {
defaultVisibilityTimeout = 60 seconds
}
maxReceiveCount (1–1000) is how many receive attempts happen before a message moves to the DLQ.
$ docker compose restart elasticmq
Access the Web UI
Visit https://UI-DOMAIN-NAME, authenticate with your configured username/password. The dashboard shows per-queue message counts (available/in-flight/delayed); click a queue for details or to send/receive test messages.
Enable Persistence
By default ElasticMQ is in-memory only, everything's lost on restart.
$ nano ~/elasticmq/elasticmq.conf
messages-storage {
enabled = true
}
$ docker compose restart elasticmq
Queues and messages now persist to an H2 database file and restore automatically on restart.
Verify the Deployment
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install
$ aws configure
Enter placeholder credentials (ElasticMQ accepts anything): Access Key test, Secret Key test, region us-east-1, output json.
$ curl -I https://DOMAIN-NAME
HTTP/2 400 is correct here — ElasticMQ rejecting a bare request confirms TLS worked and the request reached it.
$ curl -I https://UI-DOMAIN-NAME
401 without credentials confirms basic auth is active:
$ curl -I -u UI-USERNAME:UI-PASSWORD https://UI-DOMAIN-NAME
200 with credentials.
Exercise the queue:
$ aws --endpoint-url https://DOMAIN-NAME sqs create-queue --queue-name test-queue
$ aws --endpoint-url https://DOMAIN-NAME sqs send-message --queue-url https://DOMAIN-NAME/000000000000/test-queue --message-body "Hello from ElasticMQ"
$ aws --endpoint-url https://DOMAIN-NAME sqs receive-message --queue-url https://DOMAIN-NAME/000000000000/test-queue
$ aws --endpoint-url https://DOMAIN-NAME sqs delete-message --queue-url https://DOMAIN-NAME/000000000000/test-queue --receipt-handle RECEIPT-HANDLE
$ aws --endpoint-url https://DOMAIN-NAME sqs list-queues
Migrating from AWS SQS
Queue config: aws sqs list-queues + aws sqs get-queue-attributes --attribute-names All per queue, then map:
-
VisibilityTimeout→defaultVisibilityTimeout -
DelaySeconds→delay -
ReceiveMessageWaitTimeSeconds→receiveMessageWait -
RedrivePolicy→deadLettersQueue
FIFO queues: name ending in .fifo, fifo = true, contentBasedDeduplication = true for content-hash dedup. Group IDs and explicit dedup IDs both work.
DLQs: mirror your AWS redrive policies, matching maxReceiveCount.
Message migration (for queues with data to preserve):
- Drain existing AWS SQS messages
- Dual-write to both AWS SQS and ElasticMQ during the cutover window
- Once consumers are migrated and AWS queues are empty, disable AWS producers
App code: point your SDK's endpoint at ElasticMQ (e.g. boto3 endpoint_url → http://SERVER-IP:9324), set placeholder credentials, keep the region. Env-var-driven configs: set AWS_ENDPOINT_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY.
Things to watch:
- Ordering: match SQS's best-effort default; use FIFO if you need strict ordering
-
Message size: 256KB cap on both, assuming
sqs-limits = strict - Visibility timeouts: keep them matched to avoid duplicate processing
-
Auth: ElasticMQ accepts any access key/secret and never validates the AWS SigV4 signature — a reverse proxy alone doesn't restrict callers. Lock down network access instead:
ufw allow from CLIENT-IP to any port 443 proto tcp, or an equivalent cloud firewall/security-group rule. - Monitoring: swap CloudWatch for the web UI, or export metrics to Prometheus via a custom exporter
- HA: ElasticMQ has no built-in SQS-style redundancy — evaluate whether that matters for your production workload
Next Steps
ElasticMQ is running as an SQS-compatible queue with TLS, DLQs, persistence, and a monitored web UI. From here:
- Add more queues per your app's needs, following the same DLQ pattern
- Wire Prometheus scraping for queue depth alerting
- Restrict the API domain to known client IPs at the firewall level given the weak default auth
For the full guide, visit the original article on Vultr Docs.
Top comments (1)
I found the section on deploying ElasticMQ via Docker behind Traefik with TLS to be particularly interesting, as it highlights the importance of securing the communication between services. The use of Traefik as a reverse proxy and load balancer simplifies the process of obtaining and renewing TLS certificates, making it a great choice for self-hosted solutions. One potential consideration when using ElasticMQ is the trade-off between the in-memory storage and the optional H2 database persistence, as the latter may introduce additional complexity and overhead. Have you explored any other persistence options for ElasticMQ, such as using a separate message store like Redis or Apache Kafka?