Deploying ElasticMQ as a Self-Hosted AWS SQS Alternative

작성자

카테고리:

← 피드로
DEV Community · Sanskriti Harmukh · 2026-08-07 개발(SW)

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-ui container 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

Enter fullscreen mode Exit fullscreen mode

Configure ElasticMQ. Replace DOMAIN-NAME:

$ nano elasticmq.conf

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Create .env:

$ nano .env

Enter fullscreen mode Exit fullscreen mode

DOMAIN=DOMAIN-NAME
UI_DOMAIN=UI-DOMAIN-NAME
LETSENCRYPT_EMAIL=ADMIN-EMAIL
UI_USERNAME=UI-USERNAME
UI_PASSWORD_HASH=UI-PASSWORD-HASH

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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:

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Three containers running; Traefik logs should show Register... for myresolver.acme with no ERR lines.

Configure Queues

$ nano ~/elasticmq/elasticmq.conf

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

$ docker compose restart elasticmq

Enter fullscreen mode Exit fullscreen mode

Set Up Dead-Letter Queues

$ nano ~/elasticmq/elasticmq.conf

Enter fullscreen mode Exit fullscreen mode

  orders-queue {
    defaultVisibilityTimeout = 60 seconds
    delay = 0 seconds
    receiveMessageWait = 20 seconds
    deadLettersQueue {
      name = "orders-dlq"
      maxReceiveCount = 3
    }
  }

  orders-dlq {
    defaultVisibilityTimeout = 60 seconds
  }

Enter fullscreen mode Exit fullscreen mode

maxReceiveCount (1–1000) is how many receive attempts happen before a message moves to the DLQ.

$ docker compose restart elasticmq

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

messages-storage {
  enabled = true
}

Enter fullscreen mode Exit fullscreen mode

$ docker compose restart elasticmq

Enter fullscreen mode Exit fullscreen mode

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 fullscreen mode Exit fullscreen mode

Enter placeholder credentials (ElasticMQ accepts anything): Access Key test, Secret Key test, region us-east-1, output json.

$ curl -I https://DOMAIN-NAME

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

401 without credentials confirms basic auth is active:

$ curl -I -u UI-USERNAME:UI-PASSWORD https://UI-DOMAIN-NAME

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Migrating from AWS SQS

Queue config: aws sqs list-queues + aws sqs get-queue-attributes --attribute-names All per queue, then map:

  • VisibilityTimeoutdefaultVisibilityTimeout
  • DelaySecondsdelay
  • ReceiveMessageWaitTimeSecondsreceiveMessageWait
  • RedrivePolicydeadLettersQueue

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

  1. Drain existing AWS SQS messages
  2. Dual-write to both AWS SQS and ElasticMQ during the cutover window
  3. 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_urlhttp://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.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다