What you are putting together
Memos is an open-source, self-hosted note-taking app that gathers short Markdown notes into a personal timeline, with tags, attachments and four visibility levels: only you, everyone signed in, the public, or the members of a Space. Running it yourself comes down to three pieces:
- the Memos container, reachable from nowhere but the server;
- Caddy in front of it, which handles the certificate and HTTPS;
- a copy of the data folder kept somewhere other than that server.
Will a small server keep up?
For one person's timeline, or a small team's, we expect so, but that is our judgement: the Memos docs publish no hardware minimums. What they do give are the database limits. SQLite takes one write at a time, serves any number of reads, and stays practical up to about 100 GB of data.
Memos also runs on MySQL or PostgreSQL. You switch by setting MEMOS_DRIVER to mysql or postgres and putting the connection string in MEMOS_DSN. The docs suggest that route if you want managed backups and tooling, have a database running already, or expect a far bigger collection. For most people the single SQLite file is simpler, and simpler is easier to back up.
The server that fits
Our dedicated servers are whole machines: every core, the memory and the disks are yours, you log in as root, and nobody meters the port. For Memos we would look at two kinds:
- A compact cartridge, when Memos is the one job the machine has. That is exactly what the cartridges are meant for.
- An everyday single-processor server, when Memos will sit beside a database and a few other apps of yours.
Skip the X-Gene ARM cartridge: its only systems are Ubuntu 16.04 and 18.04, and this guide expects a current Ubuntu. ARM as such is no problem, because the Memos image includes an arm64 build, so the Ampere Altra servers are fine.
If a firewall rule ever locks SSH out, rack servers, blades and two cartridge models (m510 and m710x) still give you the screen and the power button through a console that works out of band. A ready server is yours within 30 minutes of approval; a custom build takes hours rather than minutes. Pick the city nearest to you on the data centers page.
Step one: lock the doors, then start Memos
Begin with a fresh Ubuntu install (our Linux servers page lists the choices) and Docker Engine with Compose, set up from Docker's own guide. Then put the firewall in place before anything is listening:
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo ufw allow 80,443/tcp
sudo mkdir -p /srv/memos/data
The first line makes ufw add IPv6 copies of the rules that follow, which it only does for rules added after it is set. The --force flag skips the yes-or-no question that would otherwise swallow the next pasted line. Next, save this as /srv/memos/compose.yaml:
services:
memos:
image: neosmemo/memos:0.31.0
restart: unless-stopped
ports:
- "127.0.0.1:5230:5230"
volumes:
- /srv/memos/data:/var/opt/memos
Bring it up with sudo docker compose -f /srv/memos/compose.yaml up -d, and check that sudo docker compose -f /srv/memos/compose.yaml logs shows no errors.
Why 127.0.0.1 sits in front of the port: the docs' examples write a bare 5230:5230, and Docker then listens on all of the server's addresses, IPv4 (0.0.0.0) and IPv6 ([::]) alike. Docker's own rules let IPv4 traffic to a published port skip ufw, and IPv6 traffic too when Docker's IPv6 networking is switched on. With 127.0.0.1 in place (and a Docker Engine from version 28 on), nothing outside the server gets in, and Caddy talks to Memos from the inside.
Step two: claim the admin account in private
Whoever creates the first account becomes the admin of your Memos. The docs say so, and the sign-up code in the project's repository (server/api/user_service.go) hands out the admin role when no users exist yet. Go through an SSH tunnel so nobody can get there before you:
- On your own computer, run
ssh -N -L 8230:127.0.0.1:5230 you@your-serverand leave it open. - Browse to http://localhost:8230 and sign up with a strong password. That account is your admin.
- In the admin settings, switch on Disallow user registration. That is the wording in the project's own English interface text; the docs' configuration files call it
disallowUserRegistration. - Make sure the instance access is set to private. If you never want public notes, turn off PUBLIC visibility as well (the docs' FAQ confirms that switch exists).
Why sign-ups matter even on a private instance: the docs list registration, while it is allowed, among the few things an anonymous visitor can still reach. Share links are the other exception; each one opens its single note until you revoke it or it expires.
Step three: a proper address with HTTPS
The docs want Memos behind a reverse proxy that serves HTTPS and passes on the Host and X-Forwarded headers. Caddy, from Ubuntu's own package, forwards those headers by default and fetches its certificate by itself. Work in this order:
- Give your chosen name, say notes.example.com, an A record for its IPv4 address and, if you use IPv6, an AAAA record as well. Wait until the name resolves, because the certificate request fails without it.
- Run
sudo apt install -y caddy, replace the contents of /etc/caddy/Caddyfile with the lines below, thensudo systemctl reload caddy. Caddy gets the certificate for that name before it serves the site. - Add an
environment:section to the memos service in compose.yaml withMEMOS_INSTANCE_URL: https://notes.example.com. Recreate the container so it takes effect:sudo docker compose -f /srv/memos/compose.yaml up -d. - Sign in at the new address and check that access still says private.
notes.example.com {
reverse_proxy 127.0.0.1:5230 {
flush_interval -1
}
}
flush_interval -1 is our addition, not the docs': Caddy already streams server-sent events as they come, and the line simply makes sure it never buffers the live-update stream the docs ask proxies to pass straight through. And why the address waits until now: on a brand-new instance, a configured URL sets access to public according to the docs, while an empty one sets it to private. Adding the URL afterwards leaves that choice alone.
Keep a copy you have actually restored
Backups are yours to run: on our dedicated servers, you manage the server; we keep the hardware and the network running. Everything Memos stores is in /srv/memos/data, the database file memos_prod.db plus an assets folder when attachments are kept on local disk. Those files belong to the container's user, UID 10001, so the commands need sudo.
- Once:
sudo apt install -y sqlite3, and a private folder,sudo mkdir -p -m 700 /srv/memos-backups. - Copy while Memos runs, with the backup command built into SQLite, which the docs recommend because it handles WAL mode:
sudo sqlite3 /srv/memos/data/memos_prod.db ".backup '/srv/memos-backups/notes-$(date +%F).db'". - That command copies the database only. If your attachments sit on local disk, archive their folder too:
sudo tar -czf /srv/memos-backups/assets-$(date +%F).tar.gz -C /srv/memos/data assets. - Or use the docs' safest, offline method, which takes everything at once: stop with
sudo docker compose -f /srv/memos/compose.yaml stop, archive withsudo tar -czf /srv/memos-backups/folder-$(date +%F).tar.gz -C /srv/memos data, start again withsudo docker compose -f /srv/memos/compose.yaml start, and make sure the archive reads back:sudo tar -tzf /srv/memos-backups/folder-YYYY-MM-DD.tar.gz > /dev/null && echo archive ok. - Check a database copy with
sudo test -s /srv/memos-backups/notes-YYYY-MM-DD.db && sudo sqlite3 /srv/memos-backups/notes-YYYY-MM-DD.db "PRAGMA integrity_check;"(put in the copy's date). It should print ok, which tells you the file is a sound database and nothing more. - Now prove Memos can use it: copy it as memos_prod.db into an empty folder, start a spare Memos container (same image tag) with that folder mounted at /var/opt/memos and the port published as
127.0.0.1:5231:5230, tunnel to 5231, sign in and open a few notes. Stop it and delete the folder afterwards. - Move the copies off the server to a machine or storage you control.
To take the online copies every night, add one line to /etc/cron.d/memos-backup: `20 2 * * * root umask 077; mkdir -p /srv/memos-backups && sqlite3 /srv/memos/data/memos_prod.db ".backup '/srv/memos-backups/notes-$(date +\%F).db'" && if [ -d /srv/memos/data/assets ]; then tar -czf /srv/memos-backups/assets-$(date +\%F).tar.gz -C /srv/memos/data assets; fi. In a cron file a plain % ends the command, so it is written \%`. Clear out old copies now and then.
If you ever need your notes back
These steps put a backup onto your running Memos, whether something broke or an update went wrong. Keep the current folder until you have seen the restored one working.
- Choose the copy and check it again: set
b=/srv/memos-backups/notes-YYYY-MM-DD.db, thensudo test -s "$b" && sudo sqlite3 "$b" "PRAGMA integrity_check;". Carry on only if you see ok. - Stop Memos:
sudo docker compose -f /srv/memos/compose.yaml stop. - Set the current folder aside rather than deleting it:
sudo mv /srv/memos/data /srv/memos/data.old, and make a fresh one withsudo install -d /srv/memos/data. - Put the copy in under the name Memos expects,
sudo cp "$b" /srv/memos/data/memos_prod.db, and unpack your attachments archive into the same folder if you keep one:sudo tar -xzf /srv/memos-backups/assets-YYYY-MM-DD.tar.gz -C /srv/memos/data. Because the folder started empty, no leftover -wal or -shm file can trip up the restored database. - Hand ownership to UID 10001, the user Memos runs as inside its container:
sudo chown -R 10001:10001 /srv/memos/data. - Make sure compose.yaml names the version the backup came from (after a failed update, the previous one), start Memos with
sudo docker compose -f /srv/memos/compose.yaml up -d, then look around once you have signed in. When everything is there, remove /srv/memos/data.old.
Updating without surprises
Memos updates its own database the first time a new version starts. The docs are plain about it: you cannot downgrade afterwards, and restoring your backup, with the old version, is the way back (the steps are above). That is why the compose file names an exact version instead of stable, a tag that would upgrade you whenever you pull.
- Take a backup.
- Go through the release notes of each version you are jumping over. The docs warn that both the older 0.x minor releases and the new calendar releases can break things.
- Edit the image tag, fetch it with
sudo docker compose -f /srv/memos/compose.yaml pull, and letsudo docker compose -f /srv/memos/compose.yaml up -dswitch over while you keep an eye onsudo docker compose -f /srv/memos/compose.yaml logs. - Sign in, open an older note, write a new one and open an attachment.
A heads-up on the next big step: 26.10, a release candidate when we wrote this, has to be reached through 0.31.0 if you are on 0.22 to 0.30, and through 0.25.3 and then 0.31.0 from anything older. It also renames the API paths from /api/v1/ to /api/, so scripts that talk to Memos need updating.
Questions people ask
- Is Memos free to run on my own server?
- Yes. The LICENSE file in the repository and the docs' FAQ both name the MIT License. Read generally, and not as legal advice: MIT lets you use, change and share Memos, and the duty to keep its copyright and permission notice attached falls on whoever hands copies on. Using it privately hands nothing on.
- Which port do I need to open for Memos?
- None for Memos directly. The container answers on 5230, but here that port is bound to 127.0.0.1 and only Caddy talks to it. The ports you open are SSH, plus 80 and 443 for Caddy.
- Can other people use my Memos?
- Yes, when you want them to. Memos has two instance roles, Admin and User, and as admin you can create accounts for people while public sign-up stays closed. Bear in mind that the docs say admins can read and manage every memo, private ones included, so keep that role for people you trust.