https://github.com/aheissenberger/mjml-docker
Self-hosted MJML render API with Node.js 25+, native TypeScript, and Docker support.
https://github.com/aheissenberger/mjml-docker
api docker email-templates html-email mjml self-hosted
Last synced: about 1 month ago
JSON representation
Self-hosted MJML render API with Node.js 25+, native TypeScript, and Docker support.
- Host: GitHub
- URL: https://github.com/aheissenberger/mjml-docker
- Owner: aheissenberger
- License: mit
- Created: 2026-03-25T22:18:32.000Z (2 months ago)
- Default Branch: master
- Last Pushed: 2026-03-26T01:55:29.000Z (2 months ago)
- Last Synced: 2026-03-26T21:45:04.053Z (2 months ago)
- Topics: api, docker, email-templates, html-email, mjml, self-hosted
- Language: TypeScript
- Homepage:
- Size: 180 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
- Support: SUPPORT.md
Awesome Lists containing this project
README
# MJML Render API
A minimal, self-hosted REST API that converts [MJML](https://mjml.io)
email templates to responsive HTML. Built on Node.js 25+ with
native TypeScript support — no build step required.
## Requirements
- Node.js 25 or newer
- pnpm
## Setup
```bash
pnpm install
cp .env.example .env
```
Set configuration variables in `.env` (or export them in your shell):
```bash
export API_KEY="your-secret-api-key" # required
```
## Run
```bash
pnpm start
```
The server listens on `http://localhost:3000` by default. Set `PORT` to change it.
Optional worker-pool tuning:
- `RENDER_WORKERS` (integer >= 1): number of render worker threads
- `RENDER_QUEUE_SIZE` (integer >= 0): bounded queue length before HTTP 503 backpressure
- `RENDER_TIMEOUT_MS` (integer >= 1): per-request render timeout in milliseconds
- `RATE_LIMIT_MAX_REQUESTS` (integer >= 1): max requests per client in each rate-limit window
- `RATE_LIMIT_WINDOW_MS` (integer >= 1): rate-limit window size in milliseconds
## Docker
```bash
docker build -t mjml-api .
docker run -p 3000:3000 -e API_KEY="your-secret-api-key" mjml-api
```
### Use Published GitHub Image (GHCR)
Publish first (manual dispatch):
```bash
pnpm gh:docker-publish --
```
Use `` as a branch or tag name (for example `master` or `v1.1.0`).
Reference the published image as:
```bash
ghcr.io/aheissenberger/mjml-docker:
```
Examples:
```bash
# Pin to a release tag
docker pull ghcr.io/aheissenberger/mjml-docker:v1.1.0
# Or use latest
docker pull ghcr.io/aheissenberger/mjml-docker:latest
docker run --rm -p 3000:3000 \
-e API_KEY="your-secret-api-key" \
ghcr.io/aheissenberger/mjml-docker:latest
```
Compose (without local build):
```yaml
services:
api:
image: ghcr.io/aheissenberger/mjml-docker:v1.1.0
environment:
API_KEY: ${API_KEY:?API_KEY is required}
PORT: ${PORT:-3000}
ports:
- "${PORT:-3000}:${PORT:-3000}"
```
If the package is private, authenticate first:
```bash
echo "$GITHUB_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
```
### Docker Compose
Compose loads `.env` by default (same directory as `compose.yaml`) and
passes the configured variables into the `api` service via
`environment` in `compose.yaml`.
```bash
cp .env.example .env
docker compose up --build
```
#### Local Presets
Development-style preset (fewer workers, shorter timeout):
```bash
cp .env.example .env
cat > .env <<'EOF'
API_KEY=dev-secret-key
PORT=3000
RENDER_WORKERS=1
RENDER_QUEUE_SIZE=16
RENDER_TIMEOUT_MS=10000
RATE_LIMIT_MAX_REQUESTS=120
RATE_LIMIT_WINDOW_MS=60000
EOF
docker compose up --build
```
Production-style preset (more workers, larger queue):
```bash
cp .env.example .env
cat > .env <<'EOF'
API_KEY=replace-with-strong-secret
PORT=3000
RENDER_WORKERS=4
RENDER_QUEUE_SIZE=128
RENDER_TIMEOUT_MS=30000
RATE_LIMIT_MAX_REQUESTS=240
RATE_LIMIT_WINDOW_MS=60000
EOF
docker compose up -d --build
```
Check health status:
```bash
docker compose ps
```
Expected result: the `api` service status becomes `healthy`.
Stop and remove containers:
```bash
docker compose down
```
Troubleshooting:
- Missing `API_KEY`:
```bash
cp .env.example .env
# then edit .env and set API_KEY
docker compose up --build
```
- Port `3000` already in use:
```bash
lsof -nP -iTCP:3000 -sTCP:LISTEN
```
Stop the process using port `3000`, or change the mapping in
`compose.yaml` from `3000:3000` to `3001:3000` and restart Compose.
## Quick usage example
```bash
curl -X POST http://localhost:3000/v1/render \
-H "Authorization: Bearer your-secret-api-key" \
-H "Content-Type: application/json" \
-d '{
"mjml": "Hello!"
}'
```
```json
{
"html": "...",
"errors": []
}
```
All endpoints except `GET /health` require
`Authorization: Bearer `.
See [docs/api.md](docs/api.md) for the full API reference,
including all request/response shapes and error codes.
## Development
```bash
pnpm dev # watch mode
pnpm typecheck # type check
pnpm lint # lint
pnpm lint:ts # TypeScript lint
pnpm format # format
pnpm test # run tests
```