MemosMemos
Deployment

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.

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.