How to Deploy Memos on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host Memos on the smallest VPS you can buy — one container, one SQLite file, a private note stream behind HTTPS, and a backup that takes one command.
- Any small VPS — 1 vCPU / 512 MB RAM is plenty
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server
- Docker Engine + Compose installed (see the base guide below)
What Memos is
Memos is a lightweight note stream: short Markdown notes, links and work-log entries, organised by a timeline, tags and search rather than folders. It feels closer to a private microblog than to a notebook app, which is exactly why people like it for quick capture — you open it, type, and the note lands at the top of the stream.
It is a Go server with a React front end, under the MIT licence, and it runs as a single container backed by SQLite by default. MySQL and PostgreSQL are supported if you already run one, but for one person or a small team SQLite is the right answer: no extra service, one file to back up.
If you want long-form, hierarchical notes instead, look at Trilium; for a team wiki, BookStack.
Server sizing
Memos is one of the smallest things you can self-host. The catalog lists 256 MB of RAM as the minimum, and on our test box — a GCP e2-standard-2 with Ubuntu 26.04 and Docker 29.8.1 — the container idled at about 16 MB of RAM, with about 100 MB of disk for the image and data.
So the constraint is not Memos; it's the reverse proxy and the OS next to it. Any entry-level VPS works, and Memos happily shares a box with other small services. Disk grows mainly with uploaded attachments.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, with a
ufw firewall. If not, start with
Docker & Compose on Ubuntu.
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Paid link — we earn a commission if you shop through it.
Install Memos (Docker Compose)
Upstream publishes neosmemo/memos with a few tag styles: stable is the
recommended production default, versioned tags such as 0.31.0 pin exactly, and
latest is development-oriented. The container stores everything under
/var/opt/memos, runs as a non-root user (UID 10001), and its entrypoint fixes
ownership of the mounted folder for you.
mkdir -p ~/memos/data && cd ~/memos
cat > docker-compose.yml <<'YAML'
services:
memos:
image: neosmemo/memos:stable
container_name: memos
restart: unless-stopped
ports:
# Loopback only: Caddy is the only way in from outside.
- "127.0.0.1:5230:5230"
volumes:
- ./data:/var/opt/memos
environment:
MEMOS_DRIVER: sqlite
YAML
docker compose up -d
Check that it answers:
for i in $(seq 1 30); do
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5230/)
[ "$code" = "200" ] && echo "Memos is up" && break
sleep 2
done
[ "$code" = "200" ]
A note on MEMOS_INSTANCE_URL
Memos has a MEMOS_INSTANCE_URL variable for its canonical external URL. Per the
upstream docs, it has a side effect on a fresh instance: leaving it out
initialises the instance with private access, while setting it initialises
public access (only when no access setting exists yet). This guide leaves it
out, so a new instance starts private. You can change the access policy later in
the instance settings; changing the URL of an existing instance does not change
the policy.
HTTPS + domain
Point an A record such as memos.example.com at the server and terminate TLS
in front of 127.0.0.1:5230, following
Automatic HTTPS with Caddy:
memos.example.com {
reverse_proxy 127.0.0.1:5230
}
If Caddy runs as a container, 127.0.0.1 is its own loopback; put it in the same
Compose project and use reverse_proxy memos:5230. Upstream also has a reverse
proxy page in its deployment docs.
First account and registration
Open https://memos.example.com and create an account: the first account you
register becomes the admin. Do that straight after the proxy goes live, before
anyone else can reach the page.
Then look at the instance settings as admin. Under general access controls you can turn off user registration, restrict password sign-in, and set the instance's access policy (public or private). For a personal note stream, turn registration off; add people later from the admin settings if you need to.
Getting notes in
The web UI is only one way in. Upstream documents several integrations, all optional:
- API access — personal access tokens let scripts and other tools create and read memos over HTTP.
- Webhooks — Memos can call a URL when memos change, which is how you push notes into other systems.
- Telegram bot — capture notes by messaging a bot, handy on a phone.
- MCP — an integration page covers connecting Memos to AI assistants through the Model Context Protocol.
Each is configured from the settings once you're logged in, and none of them needs changes to the container.
Securing it
- Register the admin account first and close registration afterwards.
- Keep port 5230 on loopback.
ss -ltnp | grep 5230should show127.0.0.1:5230. - Check the visibility of what you post. Memos lets individual notes be private or shared; get into the habit of checking before you post something sensitive on an instance with public access.
- Mind webhooks. Upstream blocks webhooks to private network addresses by
default; only allow specific hosts with
MEMOS_WEBHOOK_PRIVATE_NETWORK_ALLOWLISTif you need one.
Backups
With the default SQLite driver, everything is in ./data: the database
(memos_prod.db) and an assets/ folder for uploaded files kept on disk.
Upstream's safest offline method is to stop Memos and archive the directory,
which takes seconds for an app this size:
cd ~/memos
docker compose stop
sudo tar czf memos-backup-$(date +%F).tar.gz -C ~/memos data docker-compose.yml
docker compose start
sudo tar tzf memos-backup-$(date +%F).tar.gz | head
sudo is there because the container's user owns the files in ./data. For an
online backup without stopping, upstream documents SQLite's own .backup command
against memos_prod.db, run from a machine with the sqlite3 tool. Either way,
copy the archive off the server and schedule it with cron. Upstream's advice is
worth repeating: test a restore once before you depend on it, and note where your
attachments are stored (database, local disk or S3) alongside the backup.
Upgrades
cd ~/memos
docker compose pull
docker compose up -d
stable moves to each new release; data survives because it lives in ./data.
Back up first and read the release notes — upstream publishes upgrade notes for
specific versions (for example the move to 0.30). If you want to choose exactly
when upgrades happen, pin a versioned tag instead of stable, and roll back by
restoring the backup and the previous tag.
Troubleshooting
The page is blank or the container restarts. Read
docker compose logs memos. A permissions error on /var/opt/memos usually
means the folder was changed by hand; the entrypoint fixes ownership on start,
and MEMOS_UID / MEMOS_GID override the user if your host needs a different
one.
Anyone can see my notes. Check the instance access policy and each note's
visibility. Setting MEMOS_INSTANCE_URL on a fresh install initialises public
access.
Links in notifications or shared notes point at the wrong address. Set
MEMOS_INSTANCE_URL to your https:// URL (with the access-policy caveat above).
Verification + next steps
You're done when you can load https://memos.example.com over a valid
certificate, sign in as the admin you created, post a note and find it by tag or
search, confirm registration is closed from a private window, and have a backup
archive off the box.
From there, try the API, webhooks or the Telegram bot integration, all covered in upstream's integration docs. For hosts, see Best VPS for Self-Hosting.