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

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
    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

Env varDefaultPurpose
NEMOMEMO_PORT (or PORT)5230HTTP port
NEMOMEMO_DATA./dataData directory
DORY_TTL_SECONDS86400How long Dory remembers
NEMOMEMO_WEB_DISTPath to the built SPA (source installs)
NEMOMEMO_SMTP_HOSTSMTP server for outbound email (optional — see below)
NEMOMEMO_SMTP_PORT587SMTP port (465 switches to implicit TLS)
NEMOMEMO_SMTP_USERSMTP login
NEMOMEMO_SMTP_PASSSMTP password / API key
NEMOMEMO_SMTP_FROMFrom address, e.g. "NemoMemo <[email protected]>"

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 R2Create 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). 🐟

On this page