Self-hosting
Docker Compose
Install manually with prebuilt release images, or build from source.
Download a release
The one-line installer handles these steps for you. For manual setup, use Docker Engine with the Compose v2 plugin and choose a release from GitHub releases.
The release assets are compose.yaml, compose.caddy.yaml, Caddyfile, and install.sh. Download the Compose files and put Caddyfile at docker/caddy/Caddyfile:
mkdir -p opensend/docker/caddy
cd opensend
RELEASE_TAG=v0.1.0
RELEASE_URL="https://github.com/PanaraStudios/opensend.cc/releases/download/$RELEASE_TAG"
curl -fsSL "$RELEASE_URL/compose.yaml" -o compose.yaml
curl -fsSL "$RELEASE_URL/compose.caddy.yaml" -o compose.caddy.yaml
curl -fsSL "$RELEASE_URL/Caddyfile" -o docker/caddy/CaddyfileWrite the environment file
Create .env in this directory. Compose reads it automatically. The required credentials are INSTANCE_NAME, INSTANCE_SECRET, and CONVEX_SELF_HOSTED_ADMIN_KEY. Set BETTER_AUTH_SECRET and SSO_ENCRYPTION_KEY for authentication and encrypted SSO settings. For public HTTPS, also set all three public URLs and a backend origin reachable inside Docker.
umask 077
cat > .env <<EOF
OPENSEND_VERSION=v0.1.0
COMPOSE_FILE=compose.yaml:compose.caddy.yaml
INSTANCE_NAME=opensend
INSTANCE_SECRET=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
BETTER_AUTH_SECRET=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
SSO_ENCRYPTION_KEY=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
SITE_URL=https://mail.example.com
CONVEX_PUBLIC_URL=https://realtime.mail.example.com
CONVEX_PUBLIC_SITE_URL=https://api.mail.example.com
CONVEX_BACKEND_ORIGIN=https://realtime.mail.example.com
EOF
chmod 600 .envOPENSEND_VERSION selects the release's images; it matches the release tag. The app, migrate, and optional SMTP images come from ghcr.io/panarastudios and support Linux amd64 and arm64. The release Compose file pins the Convex backend by digest.
Point the three hostnames at your server and open ports 80 and 443 before starting Caddy. If your platform already provides HTTPS, set COMPOSE_FILE=compose.yaml and route the app to port 3000, Convex queries/WebSockets to 3210, and HTTP actions to 3211. See reverse proxy and TLS and environment variables for optional settings.
Mint the admin key
Use the backend image selected by this release, as the installer does. This example reads values from the generated file as data:
CONVEX_IMAGE=$(docker compose config --images convex)
INSTANCE_NAME=$(awk -F= '$1 == "INSTANCE_NAME" { print $2; exit }' .env)
INSTANCE_SECRET=$(awk -F= '$1 == "INSTANCE_SECRET" { print $2; exit }' .env)
ADMIN_KEY=$(docker run --rm --entrypoint ./generate_key "$CONVEX_IMAGE" "$INSTANCE_NAME" "$INSTANCE_SECRET")
printf 'CONVEX_SELF_HOSTED_ADMIN_KEY=%s\n' "$ADMIN_KEY" >> .env
unset ADMIN_KEY INSTANCE_SECRETDo this once. Preserve the existing secrets and admin key when restarting or upgrading.
Start the stack
docker compose up -d --wait
docker compose ps -aThe one-shot migrate service waits for Convex, sets non-empty backend environment values, and deploys functions. The app waits for it to succeed. An exited migrate container with code 0 is expected. If startup fails, inspect docker compose logs migrate app.
Open your dashboard's /signup page. The first account becomes the installation administrator. Read its verification link with docker compose run --rm migrate logs --history 50. Continue with AWS SES setup.
Persist and back up
Preserve .env and convex-data. The app container does not receive the deployment admin key, Better Auth secret, or SSO encryption key. See self-hosted Convex for exports and restores, and upgrading before changing images.
Build from source
Building or developing also needs Node.js 22 and pnpm 11.7.0. Clone the product repository, open its directory, and set production URLs in .env.docker before setup if needed:
pnpm install --frozen-lockfile
pnpm setup
docker compose --env-file .env.docker ps -aSetup generates .env.docker with mode 0600, fills missing secrets, mints the admin key, builds local app and migrate images, and starts the stack. It saves local image names so later Compose commands keep using those builds. It preserves existing values and data on rerun. For local development, the app is at http://localhost:3000; the Convex dashboard is opt-in.