Introduction: why Memos + Podman Quadlet is a practical production stack
If your team writes architecture notes, incident timelines, meeting decisions, and runbook fragments every day, a lightweight internal knowledge feed becomes mission-critical surprisingly fast. Memos is one of the cleanest open-source options for this use case: fast UI, markdown-friendly notes, and a simple deployment model. In many organizations, though, the usual Docker Compose stack introduces friction around lifecycle management, auto-start behavior after reboot, and service observability under systemd.
This guide shows a production-grade deployment of Memos on Podman Quadlet (rootless, systemd-managed containers) with PostgreSQL, Caddy TLS termination, backup automation, verification checks, and troubleshooting playbooks. The goal is to keep the stack operationally boring: easy to audit, easy to restart, and easy to recover.
Architecture and flow overview
- Memos app container runs rootless under the user systemd manager.
- PostgreSQL container runs rootless on loopback only (not public).
- Caddy terminates HTTPS and reverse-proxies to local Memos port.
- Quadlet defines lifecycle in
~/.config/containers/systemd/*.container. - Backups combine DB dumps and file-asset archives for full restore.
Operationally, this pattern gives you systemd-native service controls (enable, status, journalctl), plus rootless container isolation and straightforward updates using Podmanβs auto-update path.
Prerequisites
- Ubuntu 22.04/24.04 VM or bare-metal host with sudo access
- DNS record (example:
notes.sysbrix.com) pointing to your server - Open ports 80/443 at firewall/security-group layer
- A dedicated Linux user for the app (recommended)
- Basic familiarity with systemd and reverse proxies
Step-by-step deployment with full commands
1) Install Podman and baseline dependencies
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release uidmap dbus-user-session
# Podman from Ubuntu repo (stable baseline)
sudo apt install -y podman podman-compose
# Verify versions
podman --version
systemctl --version2) Prepare user-level service runtime and storage layout
# Allow user services without active login session
sudo loginctl enable-linger $USER
# Prepare directories
mkdir -p ~/.config/containers/systemd
mkdir -p ~/srv/memos/{data,backups}
# Optional: rootless network health check
podman info --format '{{.Host.Security.Rootless}}'Enabling linger is essential. Without it, user services stop when the user logs out, which is unacceptable for production workloads.
3) Create environment secrets and runtime config
# Generate strong secrets
openssl rand -hex 32 # MEMOS_SECRET
openssl rand -hex 24 # POSTGRES_PASSWORD
cat > ~/srv/memos/.env <<'EOF'
MEMOS_SECRET=replace_with_hex_32
POSTGRES_USER=memos
POSTGRES_PASSWORD=replace_with_hex_24
POSTGRES_DB=memos
MEMOS_PORT=5230
EOF
chmod 600 ~/srv/memos/.envKeep this file owner-readable only. In audits, loose secret file permissions are one of the top self-hosting failures.
4) Define PostgreSQL with Quadlet
Create ~/.config/containers/systemd/memos-db.container:
[Unit]
Description=PostgreSQL for Memos (rootless Podman)
Wants=network-online.target
After=network-online.target
[Container]
ContainerName=memos-db
Image=docker.io/library/postgres:16-alpine
EnvironmentFile=%h/srv/memos/.env
Environment=POSTGRES_INITDB_ARGS=--data-checksums
Volume=%h/srv/memos/data/pg:/var/lib/postgresql/data:Z
PublishPort=127.0.0.1:55432:5432
HealthCmd=pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
HealthInterval=10s
HealthRetries=5
AutoUpdate=registry
[Service]
Restart=always
RestartSec=5
[Install]
WantedBy=default.target5) Define Memos app with Quadlet
Create ~/.config/containers/systemd/memos.container:
[Unit]
Description=Memos app (rootless Podman)
Wants=network-online.target memos-db.service
After=network-online.target memos-db.service
[Container]
ContainerName=memos
Image=ghcr.io/usememos/memos:stable
EnvironmentFile=%h/srv/memos/.env
Environment=MEMOS_MODE=prod
Environment=MEMOS_DRIVER=postgres
Environment=MEMOS_DSN=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:55432/${POSTGRES_DB}?sslmode=disable
Environment=MEMOS_PORT=${MEMOS_PORT}
Volume=%h/srv/memos/data/memos:/var/opt/memos:Z
PublishPort=127.0.0.1:5230:5230
HealthCmd=wget -qO- http://127.0.0.1:5230/ >/dev/null || exit 1
HealthInterval=15s
HealthRetries=5
AutoUpdate=registry
[Service]
Restart=always
RestartSec=5
[Install]
WantedBy=default.target6) Enable and launch user services
systemctl --user daemon-reload
systemctl --user enable --now memos-db.service
systemctl --user enable --now memos.service
# Inspect runtime
systemctl --user status memos-db.service --no-pager
systemctl --user status memos.service --no-pager
podman ps --format 'table {{.Names}} {{.Status}} {{.Ports}}'7) Configure Caddy as secure edge proxy
sudo apt install -y caddy
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
notes.sysbrix.com {
encode zstd gzip
reverse_proxy 127.0.0.1:5230
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /var/log/caddy/memos-access.log
format json
}
}
EOF
sudo systemctl enable --now caddy
sudo systemctl reload caddyCaddy automatically provisions and renews TLS certificates, reducing certificate automation complexity while preserving excellent default security posture.
Configuration and secret-handling best practices
- Bind app/db to loopback only and expose externally through Caddy.
- Use separate backup path from live data directories.
- Rotate secrets quarterly (or immediately after role changes).
- Keep image tags intentional and test updates in staging first.
- Lock file permissions on env files, backups, and service units.
For teams with stricter controls, move secrets to a managed vault and render environment files at deploy time instead of storing static values on disk.
Verification checklist (production readiness)
# App health endpoint through local bind
curl -I http://127.0.0.1:5230/
# Public edge health
curl -I https://notes.sysbrix.com/
# Service readiness
systemctl --user is-active memos-db.service
systemctl --user is-active memos.service
systemctl is-active caddy- Create an admin account and log in over HTTPS.
- Create test memo entries with markdown and attachment upload.
- Restart host and confirm auto-start for all three services.
- Validate
journalctlshows no recurring crash loops. - Confirm cert validity window and successful TLS renewal later.
Backup, restore, and disaster recovery runbook
# Backup PostgreSQL and attachments
source ~/srv/memos/.env
mkdir -p ~/srv/memos/backups/$(date +%F)
pg_dump "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:55432/${POSTGRES_DB}" | gzip > ~/srv/memos/backups/$(date +%F)/memos.sql.gz
tar -czf ~/srv/memos/backups/$(date +%F)/memos-files.tar.gz -C ~/srv/memos/data memos
# Restore example
# gunzip -c memos.sql.gz | psql "postgresql://memos:@127.0.0.1:55432/memos For real disaster recovery, run restore drills quarterly on a throwaway host. A backup you have never restored is not an operational guarantee.
Common issues and fixes
Issue: Memos cannot connect to PostgreSQL
Likely causes: wrong DSN, DB service not healthy, or env mismatch.
Fix: verify MEMOS_DSN, check systemctl --user status memos-db.service, and confirm DB credentials in the env file.
Issue: HTTPS not issuing certificates
Likely causes: DNS mismatch or port 80 blocked.
Fix: check DNS A/AAAA records, firewall rules, and Caddy logs under /var/log/caddy.
Issue: Services stop after logout
Likely cause: linger not enabled for the service user.
Fix: run sudo loginctl enable-linger <user> and reboot to validate persistence.
Issue: Upgrade caused startup regression
Fix strategy: pin previous image tag, restore DB snapshot, and re-apply in staging before retrying production rollout.
# Journal + logs triage
journalctl --user -u memos.service -n 120 --no-pager
journalctl --user -u memos-db.service -n 120 --no-pager
sudo journalctl -u caddy -n 120 --no-pager
# Container image updates (safe rolling)
podman auto-update
systemctl --user restart memos.serviceFAQ
1) Why use Podman Quadlet instead of Docker Compose for this stack?
Quadlet integrates directly with systemd, which gives you cleaner service lifecycle behavior, native logs, and deterministic boot startup. For teams already operating Linux services through systemd, this reduces cognitive overhead and incident response time.
2) Is rootless Podman secure enough for production?
For many internal workloads, yes. Rootless mode removes an entire class of daemon-level privilege concerns. You still need patch discipline, strong secrets, and network hardening, but rootless is a meaningful defense-in-depth baseline.
3) Should I run PostgreSQL inside a container for Memos?
It is acceptable for small/medium deployments when you have backup and monitoring controls. For larger environments, managed PostgreSQL or dedicated DB hosts may simplify scaling and resilience.
4) Can I place this behind Cloudflare or another CDN/WAF?
Yes. Keep origin access restricted, preserve HTTPS between edge and origin when possible, and validate header forwarding behavior for client IP logs.
5) How do I handle zero-downtime updates?
Use a staging host for image validation, back up first, then roll updates with quick health checks and rollback criteria. For strict SLOs, move to multi-node active/standby patterns.
6) What retention policy should I use for memo data and backups?
A practical baseline is 30 daily, 12 monthly, and 4 quarterly restore points, with encrypted offsite replication. Align final policy with legal/compliance requirements for your organization.
Related internal guides
- Headscale Production Guide
- Deploy Uptime Kuma with Docker Compose and Caddy
- Deploy Gitea with Docker Compose and Caddy
Talk to us
If you want this Memos platform deployed with SSO, policy-as-code guardrails, encrypted offsite backups, and documented recovery objectives, talk to our team. We can help you design a right-sized rollout path: single-node pilot, staged production, and then HA expansion when usage grows.
Share your expected user count, auth provider, and backup/RPO targets, and weβll map that to a concrete architecture plan and implementation timeline.
Clipboard helper: If your Odoo theme allows inline scripts, add the following once per page template. If scripts are blocked, users can still manually copy from each code block.
document.addEventListener('click', async (e) => {
if (!e.target.classList.contains('copy-code')) return;
const code = e.target.parentElement.querySelector('code')?.innerText || '';
try {
await navigator.clipboard.writeText(code);
e.target.textContent = 'β
Copied';
setTimeout(() => e.target.textContent = 'π Copy', 1500);
} catch {
e.target.textContent = 'Select text manually';
}
});