HTTPS Setup
Configure SSL/TLS certificates and secure your Memos instance with HTTPS using reverse proxies.
HTTPS Setup
Securing your Memos instance with HTTPS is essential for protecting user data and maintaining trust. This guide covers SSL/TLS certificate setup using popular reverse proxies and automated certificate management.
Why HTTPS is Important
Security Notice: Never run Memos in production without HTTPS. Unencrypted connections expose user credentials, session tokens, and sensitive data to network attacks.
HTTPS provides:
- Data encryption in transit
- Authentication of your server
- Data integrity protection
- SEO benefits and browser trust indicators
- Required for modern web features (PWA, secure cookies)
Architecture Overview
Internet → Reverse Proxy (HTTPS) → Memos (HTTP)
↓
SSL Termination
Certificate Management
Security Headers
Recommended setup: Use a reverse proxy (Nginx, Apache, or Caddy) to handle SSL termination and proxy requests to Memos running on HTTP internally.
Option 1: Nginx + Let's Encrypt (Recommended)
This is the most popular and reliable setup for production deployments.
Prerequisites
# Ubuntu/Debian
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
# CentOS/RHEL
sudo yum install nginx certbot python3-certbot-nginx
Basic Nginx Configuration
Create the configuration file:
sudo nano /etc/nginx/sites-available/memos
server {
listen 80;
server_name memos.example.com;
# Redirect all HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name memos.example.com;
# SSL Configuration (will be added by certbot)
# ssl_certificate /path/to/certificate;
# ssl_certificate_key /path/to/private_key;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# Reverse proxy to Memos
location / {
proxy_pass http://127.0.0.1:5230;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# Static file optimization
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
proxy_pass http://127.0.0.1:5230;
proxy_cache_valid 200 1d;
add_header Cache-Control "public, max-age=86400";
}
}
Enable Configuration
# Enable the site
sudo ln -s /etc/nginx/sites-available/memos /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# Start/reload Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl reload nginx
SSL Certificate with Let's Encrypt
# Obtain certificate
sudo certbot --nginx -d memos.example.com
# Test automatic renewal
sudo certbot renew --dry-run
# Set up automatic renewal
sudo crontab -e
# Add: 0 12 * * * /usr/bin/certbot renew --quiet
Advanced Nginx Configuration
For production environments with additional security and performance:
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
server {
listen 443 ssl http2;
server_name memos.example.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/memos.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/memos.example.com/privkey.pem;
# SSL Security
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/memos.example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# Rate limiting
location /api/v1/auth/ {
limit_req zone=login burst=5 nodelay;
proxy_pass http://127.0.0.1:5230;
# ... other proxy settings
}
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:5230;
# ... other proxy settings
}
# Main location block
location / {
proxy_pass http://127.0.0.1:5230;
# ... proxy settings from basic config
}
}
Option 2: Apache + Let's Encrypt
Prerequisites
# Ubuntu/Debian
sudo apt install apache2 certbot python3-certbot-apache
sudo a2enmod ssl proxy proxy_http headers rewrite
# CentOS/RHEL
sudo yum install httpd mod_ssl certbot python3-certbot-apache
Apache Virtual Host Configuration
sudo nano /etc/apache2/sites-available/memos.conf
<VirtualHost *:80>
ServerName memos.example.com
Redirect permanent / https://memos.example.com/
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName memos.example.com
# SSL Configuration (managed by certbot)
SSLEngine on
# SSLCertificateFile /path/to/certificate
# SSLCertificateKeyFile /path/to/private_key
# Security Headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy strict-origin-when-cross-origin
# Reverse Proxy
ProxyPreserveHost On
ProxyRequests Off
ProxyPass / http://127.0.0.1:5230/
ProxyPassReverse / http://127.0.0.1:5230/
# WebSocket Support
RewriteEngine on
RewriteCond %{HTTP:UPGRADE} websocket [NC]
RewriteCond %{HTTP:CONNECTION} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:5230/$1" [P,L]
# Logging
ErrorLog ${APACHE_LOG_DIR}/memos_error.log
CustomLog ${APACHE_LOG_DIR}/memos_access.log combined
</VirtualHost>
</IfModule>
Enable Site and SSL
# Enable site and modules
sudo a2ensite memos.conf
sudo a2enmod ssl headers rewrite proxy proxy_http
# Test configuration
sudo apache2ctl configtest
# Reload Apache
sudo systemctl reload apache2
# Get SSL certificate
sudo certbot --apache -d memos.example.com
Option 3: Caddy (Automatic HTTPS)
Caddy provides automatic HTTPS with zero configuration - perfect for simple deployments.
Installation
# Ubuntu/Debian
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
echo 'deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Caddyfile Configuration
sudo nano /etc/caddy/Caddyfile
memos.example.com {
reverse_proxy 127.0.0.1:5230
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
}
# Rate limiting
rate_limit {
zone static {
key {remote_host}
events 100
window 1m
}
}
# Logging
log {
output file /var/log/caddy/memos.log
format single_field common_log
}
}
Start Caddy
# Start and enable Caddy
sudo systemctl start caddy
sudo systemctl enable caddy
# Check status
sudo systemctl status caddy
# View logs
sudo journalctl -u caddy -f
Caddy Magic: Caddy automatically obtains and renews SSL certificates from Let's Encrypt with zero configuration. Just point your domain to the server and it works!
Option 4: Docker with Traefik
For containerized deployments with automatic SSL.
Docker Compose with Traefik
version: '3.8'
services:
traefik:
image: traefik:v2.10
command:
- --api.dashboard=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=admin@example.com
- --certificatesresolvers.letsencrypt.acme.storage=/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik_acme:/acme.json
labels:
- traefik.enable=true
- traefik.http.routers.traefik.rule=Host(`traefik.example.com`)
- traefik.http.routers.traefik.tls.certresolver=letsencrypt
- traefik.http.routers.traefik.service=api@internal
memos:
image: neosmemo/memos:stable
volumes:
- memos_data:/var/opt/memos
labels:
- traefik.enable=true
- traefik.http.routers.memos.rule=Host(`memos.example.com`)
- traefik.http.routers.memos.entrypoints=websecure
- traefik.http.routers.memos.tls.certresolver=letsencrypt
- traefik.http.services.memos.loadbalancer.server.port=5230
volumes:
traefik_acme:
memos_data:
DNS Configuration
Ensure your domain points to your server:
# Check DNS resolution
nslookup memos.example.com
dig memos.example.com A
# Example DNS records:
# A memos.example.com → 192.168.1.100
# AAAA memos.example.com → 2001:db8::1 (if using IPv6)
Firewall Configuration
Open necessary ports:
# UFW (Ubuntu)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
# iptables
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Firewalld (CentOS/RHEL)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Testing HTTPS Setup
SSL Labs Test
Test your SSL configuration:
# Online test
# Visit: https://www.ssllabs.com/ssltest/
# Command line test
curl -I https://memos.example.com
Certificate Verification
# Check certificate details
openssl s_client -connect memos.example.com:443 -servername memos.example.com
# Check certificate expiry
echo | openssl s_client -connect memos.example.com:443 2>/dev/null | openssl x509 -noout -dates
Security Headers Test
# Test security headers
curl -I https://memos.example.com
# Expected headers:
# Strict-Transport-Security: max-age=31536000; includeSubDomains
# X-Frame-Options: SAMEORIGIN
# X-Content-Type-Options: nosniff
# X-XSS-Protection: 1; mode=block
Troubleshooting
Common Issues
Certificate Not Working
# Check Nginx/Apache error logs
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/apache2/error.log
# Verify certificate files exist
sudo ls -la /etc/letsencrypt/live/memos.example.com/
# Test certificate renewal
sudo certbot renew --dry-run
Mixed Content Warnings
Ensure Memos knows it's behind HTTPS:
# Set environment variable
export MEMOS_PUBLIC_URL="https://memos.example.com"
# Or in Docker
docker run -e MEMOS_PUBLIC_URL="https://memos.example.com" neosmemo/memos:stable
WebSocket Connection Issues
# Check proxy configuration supports WebSocket upgrade
# Nginx: proxy_set_header Upgrade $http_upgrade;
# Apache: RewriteRule for websocket connections
Monitoring and Maintenance
Certificate Expiry Monitoring
#!/bin/bash
# cert-monitor.sh
DOMAIN="memos.example.com"
EXPIRY_DATE=$(echo | openssl s_client -connect $DOMAIN:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 30 ]; then
echo "WARNING: Certificate for $DOMAIN expires in $DAYS_LEFT days!"
# Send alert (email, Slack, etc.)
fi
Log Rotation
# Nginx log rotation
sudo nano /etc/logrotate.d/memos-nginx
/var/log/nginx/memos*.log {
weekly
missingok
rotate 4
compress
notifempty
create 0644 www-data www-data
postrotate
systemctl reload nginx
endscript
}
Performance Optimization
HTTP/2 and Compression
# Nginx HTTP/2 and compression
server {
listen 443 ssl http2;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;
# Brotli compression (if module available)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
Caching
# Static asset caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
proxy_pass http://127.0.0.1:5230;
}
# API response caching (careful with dynamic content)
location /api/v1/system/info {
proxy_cache_valid 200 5m;
proxy_cache_key $scheme$request_method$host$request_uri;
proxy_pass http://127.0.0.1:5230;
}
Security Enhancements
Additional Security Headers
# Advanced security headers
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
Rate Limiting
# Rate limiting configuration
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=5r/s;
location /api/v1/auth/ {
limit_req zone=login burst=3 nodelay;
# ... proxy configuration
}
Production Ready: Following this guide gives you a production-ready HTTPS setup with automatic certificate renewal, security headers, and performance optimizations.
Next Steps
Need help with HTTPS setup? Check the troubleshooting guide or ask in our community discussions.