---
title: "Deploy Plunk | (Just Updated) Resend Alternative Email API On Your AWS SES, Admin And API Keys Ready"
description: "Plunk self-hosted email API on AWS SES, dashboard and API keys set"
category: "Other"
url: https://railway.com/deploy/plunk-or-just-updated-resend-alternative
---

# Deploy Plunk | (Just Updated) Resend Alternative Email API On Your AWS SES, Admin And API Keys Ready

Plunk self-hosted email API on AWS SES, dashboard and API keys set

**[Deploy Plunk | (Just Updated) Resend Alternative Email API On Your AWS SES, Admin And API Keys Ready on Railway](https://railway.com/template/plunk-or-just-updated-resend-alternative)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/plunk-or-just-updated-resend-alternative/manifest.json

- **Creator:** SuperSlowSloth
- **Category:** Other

## Template content

### plunk-api

- **Image:** ghcr.io/useplunk/plunk:0.18.0@sha256:a5c2c691f0d895489e067d971fca404a70dfd19a1380295e3459c3dfe9d86e04
- **Start command:** `/bin/sh -c 'echo IyEvYmluL3NoCiMgUmFpbHdheSBib290IGZvciB0aGUgUGx1bmsgQVBJIHNlcnZpY2UgKEFQSSArIHF1ZXVlIHdvcmtlciBpbiBvbmUgY29udGFpbmVyKS4KIwojIFVwc3RyZWFtJ3MgYWxsLWluLW9uZSBpbWFnZSBmcm9udHMgZm91ciBhcHBzIHdpdGggbmdpbnggb24gZm91ciBob3N0bmFtZXMsIHdoaWNoCiMgUmFpbHdheSBjYW5ub3QgZ2l2ZSBvbmUgc2VydmljZS4gVGhpcyBydW5zIHRoZSBBUEkgKGFuZCBpdHMgd29ya2VyKSBkaXJlY3RseSwKIyBzbyB0aGUgZGFzaGJvYXJkIGlzIGEgc2VwYXJhdGUgc2VydmljZSB3aXRoIGl0cyBvd24gZG9tYWluLgpzZXQgLWV1CgojIFNldHRpbmdzIGEgdGVtcGxhdGUgdmFyaWFibGUgY2Fubm90IGNhcnJ5ICh0ZW1wbGF0ZUdlbmVyYXRlIGRyb3BzIGxpdGVyYWxzKS4KZXhwb3J0IE5PREVfRU5WPXByb2R1Y3Rpb24KZXhwb3J0IExBTkRJTkdfVVJJPSIke0xBTkRJTkdfVVJJOi1odHRwczovL3d3dy51c2VwbHVuay5jb219IgpleHBvcnQgV0lLSV9VUkk9IiR7V0lLSV9VUkk6LWh0dHBzOi8vZG9jcy51c2VwbHVuay5jb219IgpleHBvcnQgQVdTX1NFU19SRUdJT049IiR7QVdTX1NFU19SRUdJT046LXVzLWVhc3QtMX0iCiMgVGhlIEFQSSByZWZ1c2VzIHRvIGJvb3Qgd2l0aG91dCBTRVMgY3JlZGVudGlhbHMuIFRoZXNlIHBsYWNlaG9sZGVycyBsZXQgaXQgc3RhcnQ7CiMgc2VuZGluZyBuZWVkcyB0aGUgZGVwbG95ZXIncyByZWFsIHZhbHVlcyBzZXQgb24gdGhpcyBzZXJ2aWNlLgpleHBvcnQgQVdTX1NFU19BQ0NFU1NfS0VZX0lEPSIke0FXU19TRVNfQUNDRVNTX0tFWV9JRDotbm90LWNvbmZpZ3VyZWR9IgpleHBvcnQgQVdTX1NFU19TRUNSRVRfQUNDRVNTX0tFWT0iJHtBV1NfU0VTX1NFQ1JFVF9BQ0NFU1NfS0VZOi1ub3QtY29uZmlndXJlZH0iCiMgVGhlIGFkbWluIGFjY291bnQgYmVsb3cgaXMgY3JlYXRlZCBhdCBib290LCBzbyBwdWJsaWMgc2lnbnVwIHN0YXlzIGNsb3NlZC4KZXhwb3J0IERJU0FCTEVfU0lHTlVQUz0iJHtESVNBQkxFX1NJR05VUFM6LXRydWV9IgpleHBvcnQgRElSRUNUX0RBVEFCQVNFX1VSTD0iJERBVEFCQVNFX1VSTCIKCmVjaG8gIltyYWlsd2F5XSBwbHVuayBhcGkgdWlkPSQoaWQgLXUpIHBvcnQ9JHtQT1JUOi11bnNldH0gYXBpX3VyaT0kQVBJX1VSSSBkYXNoYm9hcmRfdXJpPSREQVNIQk9BUkRfVVJJIgoKY2QgL2FwcAp1bnRpbCAvYXBwL25vZGVfbW9kdWxlcy8uYmluL3ByaXNtYSBtaWdyYXRlIGRlcGxveSAtLXNjaGVtYT0vYXBwL3BhY2thZ2VzL2RiL3ByaXNtYS9zY2hlbWEucHJpc21hOyBkbwogIGVjaG8gIltyYWlsd2F5XSBtaWdyYXRlIGZhaWxlZCwgcmV0cnlpbmcgaW4gM3MiCiAgc2xlZXAgMwpkb25lCgpjZCAvYXBwL2FwcHMvYXBpCm5vZGUgLWUgJwpjb25zdCB7IFByaXNtYUNsaWVudCB9ID0gcmVxdWlyZSgiQHByaXNtYS9jbGllbnQiKTsKY29uc3QgYmNyeXB0ID0gcmVxdWlyZSgiYmNyeXB0Iik7CmNvbnN0IHByaXNtYSA9IG5ldyBQcmlzbWFDbGllbnQoKTsKKGFzeW5jICgpID0+IHsKICBjb25zdCBlbWFpbCA9IHByb2Nlc3MuZW52LlBMVU5LX0FETUlOX0VNQUlMLnRyaW0oKS50b0xvd2VyQ2FzZSgpOwogIGNvbnN0IGhhc2ggPSBhd2FpdCBiY3J5cHQuaGFzaChwcm9jZXNzLmVudi5QTFVOS19BRE1JTl9QQVNTV09SRCwgMTApOwogIGNvbnN0IHVzZXIgPSBhd2FpdCBwcmlzbWEudXNlci51cHNlcnQoewogICAgd2hlcmU6IHsgZW1haWwgfSwKICAgIHVwZGF0ZTogeyBwYXNzd29yZDogaGFzaCwgdHlwZTogIlBBU1NXT1JEIiwgZW1haWxWZXJpZmllZDogdHJ1ZSB9LAogICAgY3JlYXRlOiB7IGVtYWlsLCBwYXNzd29yZDogaGFzaCwgdHlwZTogIlBBU1NXT1JEIiwgZW1haWxWZXJpZmllZDogdHJ1ZSB9LAogIH0pOwogIGxldCBwcm9qZWN0ID0gYXdhaXQgcHJpc21hLnByb2plY3QuZmluZEZpcnN0KHsgd2hlcmU6IHsgbmFtZTogIkRlZmF1bHQiLCBtZW1iZXJzOiB7IHNvbWU6IHsgdXNlcklkOiB1c2VyLmlkIH0gfSB9IH0pOwogIGNvbnN0IGtleXMgPSB7IHB1YmxpYzogcHJvY2Vzcy5lbnYuUExVTktfUFVCTElDX0tFWSwgc2VjcmV0OiBwcm9jZXNzLmVudi5QTFVOS19TRUNSRVRfS0VZIH07CiAgaWYgKCFwcm9qZWN0KSB7CiAgICBwcm9qZWN0ID0gYXdhaXQgcHJpc21hLnByb2plY3QuY3JlYXRlKHsKICAgICAgZGF0YTogeyBuYW1lOiAiRGVmYXVsdCIsIC4uLmtleXMsIG1lbWJlcnM6IHsgY3JlYXRlOiB7IHVzZXJJZDogdXNlci5pZCwgcm9sZTogIk9XTkVSIiB9IH0gfSwKICAgIH0pOwogICAgY29uc29sZS5sb2coIltyYWlsd2F5XSBjcmVhdGVkIGFkbWluIiwgZW1haWwsICJhbmQgcHJvamVjdCIsIHByb2plY3QuaWQpOwogIH0gZWxzZSB7CiAgICBwcm9qZWN0ID0gYXdhaXQgcHJpc21hLnByb2plY3QudXBkYXRlKHsgd2hlcmU6IHsgaWQ6IHByb2plY3QuaWQgfSwgZGF0YToga2V5cyB9KTsKICAgIGNvbnNvbGUubG9nKCJbcmFpbHdheV0gYWRtaW4iLCBlbWFpbCwgInByb2plY3QiLCBwcm9qZWN0LmlkLCAia2V5cyByZS1hcHBsaWVkIik7CiAgfQogIGF3YWl0IHByaXNtYS4kZGlzY29ubmVjdCgpOwp9KSgpLmNhdGNoKChlKSA9PiB7IGNvbnNvbGUuZXJyb3IoIltyYWlsd2F5XSBib290c3RyYXAgZmFpbGVkIiwgZSk7IHByb2Nlc3MuZXhpdCgxKTsgfSk7CicKCiMgUXVldWUgd29ya2VyOiByZXN0YXJ0IGl0IGlmIGl0IGRpZXMsIHRoZSBBUEkga2VlcHMgc2VydmluZyBlaXRoZXIgd2F5LgooCiAgY2QgL2FwcAogIHdoaWxlIHRydWU7IGRvCiAgICBub2RlIC9hcHAvYXBwcy9hcGkvZGlzdC9qb2JzL3dvcmtlci5qcyB8fCB0cnVlCiAgICBlY2hvICJbcmFpbHdheV0gd29ya2VyIGV4aXRlZCwgcmVzdGFydGluZyBpbiAycyIKICAgIHNsZWVwIDIKICBkb25lCikgJgoKY2QgL2FwcApleGVjIG5vZGUgL2FwcC9hcHBzL2FwaS9kaXN0L2FwcC5qcwo= | base64 -d > /tmp/boot.sh; exec /bin/sh /tmp/boot.sh'`
- **Health check:** /health
- **Public domain:** Yes

### postgres

- **Image:** postgres:17-alpine

### plunk-dashboard

- **Image:** ghcr.io/useplunk/plunk:0.18.0@sha256:a5c2c691f0d895489e067d971fca404a70dfd19a1380295e3459c3dfe9d86e04
- **Start command:** `/bin/sh -c 'echo IyEvYmluL3NoCiMgUmFpbHdheSBib290IGZvciB0aGUgUGx1bmsgZGFzaGJvYXJkIChOZXh0LmpzIHN0YW5kYWxvbmUgYnVpbGQpLgojCiMgVGhlIGJ1aWxkIGJha2VzIHBsYWNlaG9sZGVyIFVSTHMgaW50byB0aGUgYnVuZGxlOyB1cHN0cmVhbSdzIG93biBzY3JpcHQgcmV3cml0ZXMKIyB0aGVtIGZyb20gQVBJX1VSSSAvIERBU0hCT0FSRF9VUkkgYXQgc3RhcnQuIFJ1biBpdCwgdGhlbiBzdGFydCB0aGUgc2VydmVyIGRpcmVjdGx5LgpzZXQgLWUKCmV4cG9ydCBOT0RFX0VOVj1wcm9kdWN0aW9uCmV4cG9ydCBMQU5ESU5HX1VSST0iJHtMQU5ESU5HX1VSSTotaHR0cHM6Ly93d3cudXNlcGx1bmsuY29tfSIKZXhwb3J0IFdJS0lfVVJJPSIke1dJS0lfVVJJOi1odHRwczovL2RvY3MudXNlcGx1bmsuY29tfSIKZXhwb3J0IEhPU1ROQU1FPTAuMC4wLjAKCmVjaG8gIltyYWlsd2F5XSBwbHVuayBkYXNoYm9hcmQgdWlkPSQoaWQgLXUpIHBvcnQ9JHtQT1JUOi11bnNldH0gYXBpX3VyaT0kQVBJX1VSSSBkYXNoYm9hcmRfdXJpPSREQVNIQk9BUkRfVVJJIgoKLiAvYXBwL2RvY2tlci9yZXBsYWNlLXVybHMtb3B0aW1pemVkLnNoCnJlcGxhY2VfdXJsc19pbl9hcHAgIndlYiIgIi9hcHAvYXBwcy93ZWIvLm5leHQvc3RhbmRhbG9uZS9hcHBzL3dlYiIKCmNkIC9hcHAvYXBwcy93ZWIvLm5leHQvc3RhbmRhbG9uZQpleGVjIG5vZGUgYXBwcy93ZWIvc2VydmVyLmpzCg== | base64 -d > /tmp/boot.sh; exec /bin/sh /tmp/boot.sh'`
- **Health check:** /auth/login
- **Public domain:** Yes

### redis

- **Image:** redis:7-alpine
- **Start command:** `/bin/sh -c 'chown -R redis:redis /data && exec docker-entrypoint.sh redis-server --requirepass "$REDIS_PASSWORD" --appendonly yes --dir /data'`

## Documentation

# Deploy and Host Plunk on Railway

**Built for an LLM agent to deploy and use.** There is no signup page and nothing to fill in: the admin login, a public API key and a secret API key are generated at deploy time and are valid from the first boot. The contract below lists every URL, variable name and one script that proves it works.

Plunk is a self-hosted email platform and an open source alternative to Resend and Mailchimp. It sends transactional email through your own AWS SES account, tracks events, and runs automations and campaigns from one dashboard and one REST API.

## About Hosting Plunk

This template runs four services built from the official **Plunk 0.18.0** image (`ghcr.io/useplunk/plunk`, pinned by digest): `plunk-api` (the REST API plus the background queue worker), `plunk-dashboard` (the web UI), a **Postgres 17** database on a volume and a **Redis 7** queue on a volume. The upstream image bundles everything behind one nginx on four hostnames, which Railway cannot route, so each service starts only the part it needs. On first boot `plunk-api` applies the database migrations and creates the admin user and a project named `Default` with the keys from the service variables. The same values apply after every redeploy. Public signup is closed, so nobody else can register on your instance. Allow about two minutes for the first deploy to turn healthy.

Sending real email needs your own AWS SES account: set `AWS_SES_REGION`, `AWS_SES_ACCESS_KEY_ID` and `AWS_SES_SECRET_ACCESS_KEY` on the `plunk-api` service and redeploy it. Until you do, the API, dashboard, contacts and event tracking all work, and only the SES calls log errors.

### Agent contract

- Public URL: `API_URL`, which is `https://` plus the `RAILWAY_PUBLIC_DOMAIN` of the `plunk-api` service. The dashboard is `https://` plus the `RAILWAY_PUBLIC_DOMAIN` of the `plunk-dashboard` service.
- Private URL: `http://plunk-api.railway.internal:8080`
- Credentials: dashboard login is `PLUNK_ADMIN_EMAIL` with the password in `PLUNK_ADMIN_PASSWORD`; the public API key is `PLUNK_PUBLIC_KEY`; the secret API key is `PLUNK_SECRET_KEY`. All four are variables on the `plunk-api` service.
- Auth: send `Authorization: Bearer` followed by `PLUNK_PUBLIC_KEY` to `POST /v1/track`. Send `Authorization: Bearer` followed by `PLUNK_SECRET_KEY` to `GET /contacts`, `POST /v1/send` and `POST /v1/verify`.
- Health: `GET /health` returns 200 with no credentials

```bash
# run with API_URL, PLUNK_PUBLIC_KEY and PLUNK_SECRET_KEY set; needs curl
curl -s -o /dev/null -w 'health %{http_code}\n' "$API_URL/health"
curl -fsS -X POST "$API_URL/v1/track" \
  -H "Authorization: Bearer $PLUNK_PUBLIC_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"event":"agent-contract-test","email":"agent@example.com"}' -o /dev/null -w 'track %{http_code}\n'
sleep 3
curl -fsS -H "Authorization: Bearer $PLUNK_SECRET_KEY" "$API_URL/contacts" | grep -o 'agent@example.com' | head -1
```

A fresh deploy prints `health 200`, `track 200` and `agent@example.com`. Sign in to the dashboard with the admin email and password, or call the API with the keys above.

## Why Deploy Plunk on Railway?

Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.

By deploying Plunk on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.

- **Agent-ready** - generated admin password and API keys that work on first boot, a contract block and one working script; no signup step.
- **Stable credentials** - the login and both API keys are the same after every redeploy.
- **State survives redeploys** - contacts, events and campaigns live in Postgres and the queue in Redis, each on a volume.
- **Closed signup** - public registration is disabled, so only your admin account exists.
- **Pinned image** - pinned by digest, so a redeploy never changes the server version underneath you.

## Common Use Cases

- **Resend alternative for transactional email** - send password resets and receipts through your own AWS SES at SES prices.
- **Product event tracking and automations** - post events from your app and trigger emails from them.
- **Agent-driven email** - let an AI agent create contacts, track events and send mail through the REST API.

## Dependencies for Plunk Hosting

- A Postgres database and a Redis instance, each with a volume (both included in this template).
- An AWS SES account with verified sending identity, to deliver real email (set on `plunk-api` after deploy).

### Deployment Dependencies

- [useplunk/plunk](https://github.com/useplunk/plunk) - upstream project
- [Plunk documentation](https://docs.useplunk.com/) - API reference and self-hosting notes
- [AWS SES](https://aws.amazon.com/ses/) - the email delivery provider


## Similar templates

- [Rocky Linux](https://railway.com/deploy/rocky-linux) — Hosted Rocky Linux 9 workspace with SSH and persistent storage. 🚀
- [Foundry Virtual Tabletop](https://railway.com/deploy/X5tR6G) — A Self-Hosted & Modern Roleplaying Platform
- [Letta Code Remote](https://railway.com/deploy/letta-code-remote) — Run a Letta Code agent 24/7. No inbound ports, just deploy.

Open this page in a browser: https://railway.com/deploy/plunk-or-just-updated-resend-alternative
