Install and run¶
This page is about running the server — the SESKit instance itself. If you are looking for how to send mail from your application once it is running, that is Sending from your app.
Requires Docker. No AWS account needed — local mail is captured by Mailpit instead of being sent through SES.
git clone https://github.com/Otitodev/seskit.git && cd seskit
cp .env.example .env
docker compose up
| Dashboard | http://localhost:8000 |
| API docs | http://localhost:8000/docs |
| Mailpit inbox | http://localhost:8025 |
Then send your first email.
You do not install PostgreSQL or Redis¶
docker compose up starts all of it. Nothing on this list is something you
install, configure or start yourself:
| Service | What it is | |
|---|---|---|
db |
PostgreSQL 16 | Every message, project, key and event |
redis |
Redis 7 | The job queue, the auth cache and rate limits |
mailpit |
Mailpit | Catches local mail so no AWS account is needed |
migrate |
Runs once and exits | alembic upgrade head, before anything else starts |
api |
The web process | /v1 and the dashboard, on port 8000 |
worker |
The background process | Sends the mail, delivers webhooks, polls for events |
api and worker wait for migrate to finish successfully, so the schema is
always in place before either of them accepts anything. A failed migration
stops the stack rather than starting an instance with no tables.
The database and Redis keep their data in named Docker volumes, so
docker compose down and back up again does not lose anything.
docker compose down -v does — that deletes the volumes.
Bring your own PostgreSQL and Redis instead if you would rather: point
DATABASE_URL and REDIS_URL at them and run only the services you need.
Running without Docker is the same idea further
along.
On a server¶
Clones the repository, writes a .env with a generated SECRET_KEY, and
brings the stack up. It refuses rather than guesses: where Docker is missing or
not running it says so and stops, instead of installing things on your machine.
Safe to run twice — an existing checkout is left alone and an existing .env
is never overwritten, so a rerun cannot rotate SECRET_KEY and lock every
project out of its stored AWS credentials.
It installs the latest published release, not the tip of main, so a
commit pushed five minutes ago cannot become your production instance. Pin it
yourself, or take main deliberately:
curl -fsSL https://otitodev.github.io/seskit/install.sh | SESKIT_VERSION=v0.1.0 sh
curl -fsSL https://otitodev.github.io/seskit/install.sh | SESKIT_VERSION=main sh
| Variable | Default |
|---|---|
SESKIT_VERSION |
The latest release, or main if none is published |
SESKIT_DIR |
./seskit |
SESKIT_PUBLIC_URL |
Guessed from this machine's public IP |
SESKIT_PUBLIC_URL becomes PUBLIC_BASE_URL, which is what puts a working
one-click unsubscribe link on every message. The guess is right often enough to
be worth making and wrong in ways you can see — behind a proxy or on a real
domain, change the one line in .env.
If piping a script to a shell is not something you do — reasonable — the three commands above are the whole of it, and read the script first.
Or hand the whole thing to a coding agent.
Why Postgres and Redis are on unusual ports¶
55432 and 56379, not 5432 and 6379
Machines with PostgreSQL installed frequently already have clusters on 5432 and 5433. Those bind before Docker does, and the container then looks perfectly healthy while every connection quietly reaches the wrong database — which is a genuinely unpleasant afternoon.
Override with POSTGRES_HOST_PORT and REDIS_HOST_PORT if the high ports
clash with something of yours.
Inside the Compose network these are still plain db:5432 and redis:6379, so
nothing else changes.
Running without Docker¶
Useful if you are working on SESKit itself, or already run Postgres and Redis.
uv sync
docker compose up -d db redis mailpit # dependencies only
export DATABASE_URL="postgresql+asyncpg://seskit:seskit@localhost:55432/seskit"
export REDIS_URL="redis://localhost:56379/0"
export SECRET_KEY="dev"
uv run alembic upgrade head # Compose does this for you
uv run uvicorn seskit_api.main:app --reload
uv run arq seskit_worker.main.WorkerSettings # in a second shell
Two processes, one codebase. The API serves /v1 and the dashboard; the
worker sends the mail, delivers webhooks and polls for delivery events. They
never talk to each other — the API records the message and puts a job in Redis,
and the worker picks it up. That is why a send answers queued rather than
sent.
The worker is not optional. Sending is queued, so with no worker running a
message stays at queued for ever and nothing tells you why.
Both need the same DATABASE_URL, REDIS_URL and SECRET_KEY — which is
why they are exported once above and inherited by both shells. See
how the two processes fit together
before running this anywhere real.
Creating the owner account¶
Open http://localhost:8000 and register. The first registration claims the instance and signup closes behind you — an instance you deploy is yours, not an open sign-up page someone else can find.
Set ALLOW_SIGNUP=true if you want it to stay open.
What next¶
- Your first email — about a minute, still no AWS account.
- Connect an AWS account — when you want real mail to leave the building.
- Deploying — running this somewhere other than your laptop.