NemoMemo

Deploy

One-click cloud buttons, Docker, Compose, source builds, and backups.

Prerequisites

  • Docker deploys: Docker Engine on any Linux box, NAS, Raspberry Pi, or VPS. No repository clone needed — the image is on GitHub Container Registry.
  • Source deploys: git, Node.js 22+, pnpm.
  • One-click cloud deploys: just an account with the provider.

One-click cloud deploys

NemoMemo is a stateful app (SQLite + uploads on disk), so it needs a host with a persistent volume — which is why you won't see Vercel/Netlify buttons here; those platforms are for static and serverless apps and would lose your data.

Deploy to Render   Deploy to Koyeb

  • Render — the button reads this repo's render.yaml: one Docker web service with a 1 GB persistent disk mounted at /app/data and a health check, preconfigured. Persistent disks require Render's paid Starter instance (about $7/month). Sign in, click Apply, done.
  • Koyeb — the button prefills a Dockerfile build from this repo. After the first deploy, attach a volume to the service mounted at /app/data in the Koyeb dashboard (Volumes → attach) so your data survives redeploys.

Both providers inject a PORT environment variable, which NemoMemo respects automatically.

Docker

docker run -d --name nemomemo \
  -p 5230:5230 \
  -v nemomemo-data:/app/data \
  ghcr.io/davidallmon/nemomemo:latest

Add --env-file .env for any of the optional settings below.

Everything lives in the /app/data volume: nemomemo.db (SQLite) and uploads/. Images are published for amd64 and arm64 (Raspberry Pi 4/5 works). Tags: latest tracks main; version tags appear on releases.

Upgrading:

docker pull ghcr.io/davidallmon/nemomemo:latest
docker rm -f nemomemo && docker run -d --name nemomemo \
  -p 5230:5230 -v nemomemo-data:/app/data ghcr.io/davidallmon/nemomemo:latest

Docker Compose

services:
  nemomemo:
    image: ghcr.io/davidallmon/nemomemo:latest
    ports:
      - "5230:5230"
    volumes:
      - nemomemo-data:/app/data
    env_file: [.env]          # optional — delete this line if you have no .env
    restart: unless-stopped
volumes:
  nemomemo-data:

Build from source

git clone https://github.com/DavidAllmon/nemomemo.git
cd nemomemo
pnpm install --filter '!@nemomemo/site'
pnpm build
NEMOMEMO_WEB_DIST=web/dist node server/dist/index.js

Or build the image yourself: docker build -t nemomemo . (the image contains only the app — never the marketing/docs site).

Configuration

Nothing here is required — NemoMemo runs with no configuration at all. When you do want something, it all comes from one env file. Start from the annotated template in the repo, which lists every setting below with its default, commented out:

curl -fsSLO https://raw.githubusercontent.com/DavidAllmon/nemomemo/main/.env.example
cp .env.example .env

Then apply it: --env-file .env on docker run, env_file: [.env] on a Compose service, NEMOMEMO_ENV_FILE=.env for the install script, or set -a; . ./.env; set +a from source. Recreating a container to pick up new variables is safe — your memos live in the data volume, not the container.

Docker and Compose users: leave the first three alone. The image already sets NEMOMEMO_PORT, NEMOMEMO_DATA and NEMOMEMO_WEB_DIST correctly. Pointing NEMOMEMO_DATA at a relative path writes your memos inside the container rather than the mounted volume, and the next upgrade throws them away. To serve on a different port, change the mapping (-p 8080:5230) instead. Those three are for from-source installs.

Env varDefaultPurpose
NEMOMEMO_PORT (or PORT)5230HTTP port
NEMOMEMO_DATA./dataData directory
DORY_TTL_SECONDS86400How long Dory remembers
NEMOMEMO_WEB_DIST—Path to the built SPA (source installs)
NEMOMEMO_SMTP_HOST—SMTP server for outbound email (optional — needs all four of HOST/USER/PASS/FROM; see below)
NEMOMEMO_SMTP_PORT587SMTP port (465 switches to implicit TLS)
NEMOMEMO_SMTP_USER—SMTP login
NEMOMEMO_SMTP_PASS—SMTP password / API key
NEMOMEMO_SMTP_FROM—From address, e.g. "NemoMemo <[email protected]>"
NEMOMEMO_OCR1Read text inside image attachments so search finds it — set 0 to turn off
NEMOMEMO_OCR_LANGSengOCR languages, comma-separated tesseract codes (e.g. eng,deu)
NEMOMEMO_TRANSCRIBE_URL—OpenAI-compatible /audio/transcriptions endpoint for voice memo transcripts (optional — see below)
NEMOMEMO_TRANSCRIBE_KEY—API key for that endpoint (omit for keyless local servers)
NEMOMEMO_TRANSCRIBE_MODELwhisper-1Model name sent to the endpoint
NEMOMEMO_DICTATE_KEY—OpenAI API key that turns on live dictation in the editor (optional — see below)
NEMOMEMO_DICTATE_MODELgpt-live-transcribeOpenAI Realtime transcription model for dictation
NEMOMEMO_TELEGRAM_BOT_TOKEN—Turns on the Telegram capture bot (optional — see below)

Image text search (OCR)

Since v1.19, search can read your screenshots: when someone attaches an image, the reef quietly extracts any text in it (receipts, whiteboards, signs) and adds it to the search index. It runs entirely on your server — no cloud API, nothing leaves the reef.

The first image after a fresh install downloads the OCR language data (a few MB) into data/ocr-cache and reuses it from then on. If your reef runs air-gapped with no outbound network, set NEMOMEMO_OCR=0 — everything else works exactly the same, images just aren't searchable by their contents.

Live dictation (optional)

Since v1.21 the editor has a microphone button: tap it, talk, and your words appear in the memo as you speak — tap again to stop. It needs an OpenAI API key, because dictation uses OpenAI's Realtime transcription service:

NEMOMEMO_DICTATE_KEY=sk-...

Your API key stays on your server. When someone starts dictating, the reef mints a single-use key that expires in minutes, and the browser streams audio straight to OpenAI with only that. Audio is transcribed live and isn't stored by your reef — only the text you keep in the memo.

Without a key, the microphone button records a voice clip instead (no setup, no external services) — the audio attaches to your memo and plays back inline. With dictation on, voice clips move to the little arrow next to the microphone, so you can always do either.

Session requests are capped at 20 per 10 minutes per IP — far more than anyone tapping the mic will reach, and enough to stop a script. That limits how many sessions get minted, though: audio streams from the browser straight to the provider, so if your reef is open to the public, put a spend cap on the API project too.

Voice transcription (optional)

Since v1.20 you can record voice memos right in the editor — they play back inline with no setup at all. Point NEMOMEMO_TRANSCRIBE_URL at any OpenAI-compatible /audio/transcriptions endpoint and the reef also transcribes each recording: the transcript appears under the player and the words become searchable, just like text you typed.

# OpenAI:
NEMOMEMO_TRANSCRIBE_URL=https://api.openai.com/v1/audio/transcriptions
NEMOMEMO_TRANSCRIBE_KEY=sk-...
# …or a local whisper.cpp / LocalAI / Speaches server on your own hardware:
NEMOMEMO_TRANSCRIBE_URL=http://localhost:8080/v1/audio/transcriptions

Leave it unset and voice memos still record and play — they just aren't searchable by what was said.

Capture from Telegram (optional)

Since v1.31, your reef can have a Telegram bot: message it and what you send becomes a memo — text, photos, and voice notes, with #tags working inline. It's the cheapest way to capture from a phone, because there's no app to install and you're already signed in.

Make a bot, then point your reef at it:

  1. Message @BotFather on Telegram and send /newbot.
  2. Pick a name and a username; BotFather replies with a token that looks like 123456789:AAE….
  3. Set it and restart:
NEMOMEMO_TELEGRAM_BOT_TOKEN=123456789:AAE...

Each member then connects their own chat: Settings → Access → Connect Telegram hands them a code, and they send /link CODE to the bot. One bot serves the whole reef — nobody shares anything with anyone else, and memos land in the chatter's own reef with their default visibility. /unlink in the chat (or Disconnect in Settings) ends it immediately.

Photos go through OCR and voice notes through transcription if you've turned those on above, so a captured whiteboard is searchable by what's written on it.

Messages to a Telegram bot travel through Telegram's servers and are not end-to-end encrypted — that's how the Bot API works, not something NemoMemo chooses. It's fine for "buy milk on the way home"; keep genuinely private things in the app itself. Your bot token is a credential too: anyone holding it can read messages sent to your bot, so treat it like a password.

Only single-instance (self-hosted) reefs run the bot: the hosted service shares one process across many reefs, and several pollers on one token would fight over Telegram's update queue.

Set all five NEMOMEMO_SMTP_* variables and your reef can send email — any SMTP provider works (Brevo and Postmark have friendly free tiers, or your own relay):

NEMOMEMO_SMTP_HOST=smtp-relay.example.com
NEMOMEMO_SMTP_PORT=587
NEMOMEMO_SMTP_USER=[email protected]
NEMOMEMO_SMTP_PASS=your-smtp-key
NEMOMEMO_SMTP_FROM="NemoMemo <[email protected]>"

With email on, your reef gets: welcome + verification emails for new members, self-serve password reset ("Forgot your password?" on the sign-in page), member invites (add someone by email and they pick their own password), and security heads-ups when a password or email changes.

Without it, everything still works: accounts still sign up with an email address (it's part of every account since v1.8), but nothing sends — members who forget a password ask their reefkeeper, who can set a new one in Settings → Members.

Exposing it to the internet

Put NemoMemo behind HTTPS — either a reverse proxy (Caddy, nginx, Traefik) terminating TLS in front of port 5230, or a Cloudflare Tunnel pointing at http://localhost:5230 (no open ports needed; great for homelabs).

Backups

Your whole reef is two things inside the data volume: nemomemo.db (SQLite) and uploads/. Back up both. (On NemoMemo Cloud we do this for you nightly — self-hosting means this part is yours.)

The easy way: the button

As the reefkeeper (admin), open Settings → Backups → Download backup. You get one zip containing a consistent database snapshot (safe even while people are writing) and every uploaded file. Do that on a schedule you'll actually keep, store the zip somewhere that isn't the server, and you're covered. Everything below is for automating it.

The readable way: Markdown export

Any memo can be saved as a plain .md file from its ⋯ → Copy → Download as .md menu — created/updated times, visibility, and tags ride along as YAML frontmatter, so it reads correctly in any Markdown app. Power users can grab everything at once: while signed in, GET /api/v1/memos/export/markdown streams a zip of all your own memos — one dated .md file each, comments in their own folder, plus an attachments/ folder with inline links rewritten to relative paths. Either way it's a copy for humans and other tools, not a restore format — the backup zip above is the one the Restore button accepts.

A safe snapshot (Docker)

Don't copy nemomemo.db while the app is running — a plain cp can catch it mid-write. SQLite's .backup takes a consistent snapshot even while live:

mkdir -p ~/nemomemo-backups
docker exec <container> sh -c \
  'apk add --no-cache sqlite >/dev/null 2>&1 || true; sqlite3 /app/data/nemomemo.db ".backup /app/data/backup.db"'
docker cp <container>:/app/data/backup.db ~/nemomemo-backups/nemomemo-$(date +%F).db
docker cp <container>:/app/data/uploads   ~/nemomemo-backups/uploads-$(date +%F)
docker exec <container> rm /app/data/backup.db

(Running from source instead? Just point sqlite3 and cp at your data directory directly.)

Make it nightly

Put those commands in a script and add a cron line on the host:

17 3 * * * /home/you/backup-nemomemo.sh >> /home/you/nemomemo-backups/backup.log 2>&1

A backup on the same disk dies with the disk. restic — the same tool NemoMemo Cloud's own backups use — encrypts your backup folder and pushes it to almost anywhere: another machine over SFTP, or any S3-compatible bucket.

Getting a free bucket (either of these comfortably fits a reef in its free tier):

  • Cloudflare R2 (pricing: 10 GB + free egress): in the Cloudflare dashboard go to R2 → Create bucket (pick any name, e.g. my-reef-backups). Then R2 → Manage R2 API Tokens → Account API Tokens → Create: permissions Object Read & Write, apply to your bucket only, TTL Forever. The confirmation screen shows an Access Key ID, Secret Access Key, and your account's S3 endpoint URL — copy all three (the secret is shown only once).
  • Backblaze B2 (pricing: first 10 GB always free): create a bucket, then Application Keys → Add a New Application Key scoped to that bucket; the key ID/key are your S3 credentials and the endpoint is shown on the bucket page.

Then wire restic to it:

export AWS_ACCESS_KEY_ID=<access key id>
export AWS_SECRET_ACCESS_KEY=<secret access key>
export RESTIC_REPOSITORY=s3:https://<endpoint>/<bucket>
export RESTIC_PASSWORD=<pick a strong passphrase and KEEP IT SAFE>

restic init                                            # once
restic backup ~/nemomemo-backups                       # each night (add to your cron script)
restic forget --keep-daily 14 --keep-weekly 8 --prune  # retention

Without the restic passphrase a backup can never be restored — store it (and the keys) somewhere that isn't this server, like a password manager.

Restoring

The easy way: as the reefkeeper, Settings → Backups → Restore from backup — upload a backup zip, and the reef verifies it, sets the current data aside as a safety copy, swaps the backup in, and restarts itself. Done.

By hand (if the app won't start, or you prefer the shell): stop the container, put the files back, start it again:

docker stop <container>
docker cp ~/nemomemo-backups/nemomemo-2026-08-22.db <container>:/app/data/nemomemo.db
docker cp ~/nemomemo-backups/uploads-2026-08-22/.   <container>:/app/data/uploads
docker start <container>

Backups only matter if restore is realistic — do one practice restore before you depend on it. Just keep swimming (with copies). 🐟