Self-host Memos for notes
Follow this on your own server. Start free, no card needed
Most note-taking apps worth using eventually ask you to pay a monthly fee to keep your own notes available to yourself, or they lock your data into a format only their app can read. Memos is a small, open-source alternative: a single Go binary with an embedded SQLite database, built for quick notes, lists and a personal knowledge base, that you can run on hardware you control. By the end of this guide you’ll have your own Memos instance running on a $6/month server, reachable over HTTPS at your own domain, with a backup routine and an upgrade path that won’t surprise you.
What you’ll need
- A server. DigitalOcean’s Basic Droplet at $6.00/month gets you 1 vCPU, 1 GB RAM and 25 GB SSD, which is plenty for Memos and a reverse proxy running together on a single box. Memos is a lightweight Go binary backed by SQLite, not a stack of services, so this is more headroom than it actually needs. Any Ubuntu 24.04 VPS from another provider works the same way.
- A domain name (or a subdomain of one you already own), with access to its DNS settings.
- Docker and the Docker Compose plugin installed on the server (covered in Step 2).
- A terminal and SSH access to the server.
Step 1: Create the server and point a domain at it
Spin up an Ubuntu 24.04 droplet on DigitalOcean’s $6/month Basic plan (1 vCPU, 1 GB RAM, 25 GB SSD). Note its public IP address.
In your domain’s DNS settings, add an A record pointing a subdomain, for example memos.yourdomain.com, at that IP:
Type: A
Name: memos
Value: <your server's IP>
TTL: 300
DNS propagation can take a few minutes to a few hours. You should see dig memos.yourdomain.com +short return your server’s IP before moving on, since the next steps depend on it.
Step 2: Install Docker
SSH into the server and install Docker Engine plus the Compose plugin:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Log out and back in for the group change to take effect, then check both pieces are present:
docker --version
docker compose version
You should see a Docker version and a Compose version printed, with no “command not found” errors.
Step 3: Lay out the project directory
Memos doesn’t need an encryption key or any secrets generated up front, unlike some self-hosted apps. The only thing worth keeping outside the compose file is the domain, so Caddy can pick it up:
mkdir -p ~/memos && cd ~/memos
Create .env with your domain:
# .env
DOMAIN=memos.yourdomain.com
That’s the entire secrets story for this setup.
Step 4: Write the Docker Compose file
Memos ships with an embedded SQLite database by default, which is the documented, supported way to run it for personal or small-team use, not a fallback mode you’re expected to outgrow. There’s no separate database container to run or keep in sync here.
Create docker-compose.yml:
services:
memos:
image: neosmemo/memos:0.31.0
restart: unless-stopped
volumes:
- memos_data:/var/opt/memos
networks:
- memos-net
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- DOMAIN=${DOMAIN}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- memos
networks:
- memos-net
networks:
memos-net:
volumes:
memos_data:
caddy_data:
caddy_config:
A few choices worth explaining:
- The image is pinned to
neosmemo/memos:0.31.0rather than the floatingstabletag. Memos tags its releasesYY.MM.N(release candidates asYY.MM.N-rc.K), andstablealways points at whichever of those is newest, which is convenient but means a plaindocker compose pullcould move you to a different release without you choosing to.0.31.0is the exact releasestablepoints at today; pinning it means you upgrade deliberately, on your own schedule, covered in Step 8. /var/opt/memosis the one path inside the container where Memos keeps everything: its SQLite database and any files you’ve uploaded through it. One named volume mounted there is the entire data story for this app, which is also why the backup step later is so short.- Only Caddy publishes ports to the host. Memos itself is reachable only on the internal Docker network, never directly from the internet.
Validate the file parses before moving on:
docker compose config
This should print the fully resolved configuration with no errors.
Step 5: Write the Caddyfile and start everything
Caddy handles the TLS certificate from Let’s Encrypt automatically, as long as DNS is already pointing at the server and ports 80 and 443 are reachable from the internet. Create Caddyfile in the same directory:
{$DOMAIN} {
reverse_proxy memos:5230
}
Start the stack:
docker compose up -d
Watch it come up and check Caddy actually got a certificate:
docker compose logs -f caddy
You should see a line mentioning a certificate obtained for your domain, then visiting https://memos.yourdomain.com in a browser should load Memos with a valid padlock, no certificate warning.
Step 6: First-run setup
Memos doesn’t ask you to sign up anywhere or enter a license key. The first time you visit your instance, it shows a form to create the first account directly, and that account becomes the instance’s host, with admin rights over it. Create it now, with a real password, since anyone who reaches the instance before you do could claim that first account instead.
Step 7: Back up the data volume
Everything Memos keeps, its database and any uploaded files, lives under the single /var/opt/memos path inside the container, which is the memos_data named volume. Backing up that one volume is the entire backup story for this app:
docker run --rm -v memos_memos_data:/data -v $(pwd):/backup alpine \
tar czf /backup/memos-data-backup-$(date +%F).tar.gz -C /data .
Put that in a cron job and copy the result somewhere other than this server, an object storage bucket, another machine, anywhere that isn’t this droplet. A backup that lives only on the server it’s backing up doesn’t survive the failure mode that actually destroys a server.
Step 8: Upgrade Memos
Check the version you’re currently running, read Memos’ release notes for anything between that version and the one you’re moving to, then bump the tag in docker-compose.yml once you’ve decided to move, for example:
image: neosmemo/memos:<new-version>
Pull and restart just that service:
docker compose pull memos
docker compose up -d memos
Check the logs for a clean startup:
docker compose logs -f memos
You should see Memos start up with no error, and your existing notes should still be there when you log back in. Your volume backup from Step 7 is the way back if something looks wrong.
The shortcut: do it with Railyard
Everything above, the single Memos container with a persistent data volume and HTTPS already wired up, is also a one-click template:
https://app.railyard.run/deploy?template=memos
This is the same single-container shape as the manual setup in this guide: no Postgres add-on, no separate database service, Memos’ own embedded SQLite backend doing the same job it does here. The persistent volume and HTTPS are already configured for you.
If you’d rather start from Railyard generally before deciding on Memos specifically, the quickstart covers the basics, and databases and backups explains how backups work across every template, not just this one. Deploy button covers the ?template= link format itself if you want to embed one elsewhere.
Railyard’s Free plan covers one server plus a database server and 3 apps, no card required. Pro is $19/month per team, and Team is $49/month per team, for larger limits and more collaborators.
Troubleshooting
Caddy never gets a certificate, or the browser shows a certificate warning. This is almost always DNS, not Caddy. Run dig memos.yourdomain.com +short from your own machine and confirm it returns the server’s IP, not an old one, and not nothing. Let’s Encrypt also needs port 80 reachable from the internet for the initial validation, so check your cloud provider’s firewall or security group allows inbound 80 and 443, not just 443.
docker compose up fails with “port is already allocated.” Something else on the box is already bound to 80 or 443, commonly a default Nginx or Apache install that ships with some base images. Find it with sudo ss -tlnp | grep -E ':80|:443' and stop that service.
Notes seem to disappear after a restart. This usually means a host directory was bind-mounted instead of a named Docker volume for /var/opt/memos, and something about that path, ownership or an unrelated container reusing it, left the data somewhere other than where you think it is. Named volumes, as used in the compose file above, avoid this because Docker manages them consistently regardless of what’s on the host filesystem.
The container starts then immediately exits. Check docker compose logs memos for the actual error first. A wrong or missing mount for /var/opt/memos, or a volume with permission issues from an earlier manual change, is the usual cause. Recreating the volume cleanly and restoring from a backup if you have real data on it is the fastest fix.
FAQ
Do I need Postgres or MySQL instead of SQLite for a bigger team? No. Memos’ embedded SQLite backend is the documented default and it’s what this guide uses. It’s built for personal use or a small team, and nothing in this setup changes if your team grows within that range. If you’re running something large enough that SQLite’s single-writer model becomes a real bottleneck, that’s a different scale of deployment than this guide covers.
Can I run this without a domain, just the server’s IP? Caddy’s automatic HTTPS needs a real domain name; Let’s Encrypt doesn’t issue certificates for bare IP addresses. You can reach Memos over plain HTTP on an IP for quick testing by pointing a browser straight at port 5230 on the server, but don’t rely on that for anything you intend to keep using.
How much does this actually cost to run? The server is $6/month. Memos itself is free, open-source software, so you’re paying for the box it runs on, nothing else.
What exactly is backed up in that one volume? Everything. Memos’ SQLite database, which holds every note, tag and setting, and any files you’ve uploaded through the app, all live under /var/opt/memos. There’s no second data store to remember, which is the entire reason Step 7 is as short as it is.
Is Railyard’s template the same architecture as this manual setup? Yes. Both run a single Memos container against its own embedded SQLite database, with no Postgres or MySQL add-on, and both persist that one volume. Railyard’s template uses the floating stable tag rather than a pinned version, since a managed deployment handles the upgrade path for you.
Either path gets you the same thing: a notes app with no subscription attached, your data in one place you control, and a server bill that doesn’t change no matter how many notes you write.
Why people pick Railyard for this
- Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
- One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
- One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
- Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
- Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.