$linuxjunkies
>

Self-Host CryptPad (Encrypted Office Suite)

Deploy CryptPad, the zero-knowledge encrypted office suite, with Docker Compose, nginx TLS, a custom domain pair, and configurable registration policies.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • Two DNS A records pointing to your server: one for the main domain and one for the sandbox domain
  • Docker Engine 24+ and the Docker Compose v2 plugin installed
  • Ports 80 and 443 open and not bound by another process
  • A non-root user with sudo privileges and membership in the docker group

CryptPad is a zero-knowledge, end-to-end encrypted office suite covering documents, spreadsheets, kanban boards, and more. Nothing on the server can read your content — encryption and decryption happen entirely in the browser. Self-hosting gives you full control over who can register, how much storage to allocate, and which domain serves the application. This guide walks through a production-ready Docker Compose deployment behind a reverse proxy with TLS, a custom domain, and a locked-down registration policy.

Architecture Overview

The setup uses three components:

  • CryptPad container — the Node.js application, running as an unprivileged user.
  • Nginx reverse proxy — terminates TLS and forwards traffic. We use a separate nginx container with Certbot, or you can use Caddy if you prefer.
  • Persistent volumes — blob storage, datastore, logs, and configuration survive container restarts.

CryptPad requires two domains: a main domain (e.g. pad.example.com) and a sandboxed domain (e.g. sandbox.example.com). The sandbox domain isolates untrusted document content in a separate origin to prevent XSS. Both must have valid TLS certificates. Point both DNS A records to the same server IP before proceeding.

Prerequisites and Server Sizing

A minimum of 2 GB RAM and 2 vCPUs is workable for a small team; 4 GB RAM is comfortable for 20–50 concurrent users. Docker Engine 24+ and Docker Compose v2 (the docker compose plugin, not the legacy docker-compose binary) are required.

Install Docker on Debian/Ubuntu

sudo apt update && sudo apt install -y ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER

Install Docker on Fedora/RHEL family

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER

Install Docker on Arch

sudo pacman -S docker docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker $USER

Step 1 — Create the Project Directory and Volumes

sudo mkdir -p /opt/cryptpad/{data/blob,data/datastore,data/logs,data/pins,data/tasks,data/block,nginx/conf.d,nginx/certs}
sudo chown -R 4001:4001 /opt/cryptpad/data
cd /opt/cryptpad

UID/GID 4001 matches the cryptpad user inside the official container image. Getting this right prevents permission errors on startup.

Step 2 — Write the Docker Compose File

cat > /opt/cryptpad/docker-compose.yml <<'EOF'
services:
  cryptpad:
    image: cryptpad/cryptpad:latest
    container_name: cryptpad
    restart: unless-stopped
    environment:
      - CPAD_MAIN_DOMAIN=pad.example.com
      - CPAD_SANDBOX_DOMAIN=sandbox.example.com
      - CPAD_TRUSTED_PROXY=nginx
    volumes:
      - ./data/blob:/cryptpad/blob
      - ./data/datastore:/cryptpad/datastore
      - ./data/logs:/cryptpad/log
      - ./data/pins:/cryptpad/pins
      - ./data/tasks:/cryptpad/tasks
      - ./data/block:/cryptpad/block
      - ./cryptpad_config.js:/cryptpad/config/config.js:ro
    expose:
      - "3000"
    networks:
      - internal

  nginx:
    image: nginx:stable-alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - cryptpad
    networks:
      - internal

networks:
  internal:
EOF

Step 3 — Configure CryptPad

CryptPad reads a JavaScript config file. The environment variables set in Compose cover the domains, but storage quotas, registration policy, and admin keys live here.

cat > /opt/cryptpad/cryptpad_config.js <<'EOF'
module.exports = {
  httpUnsafeOrigin: 'https://pad.example.com',
  httpSafeOrigin: 'https://sandbox.example.com',

  /* Set to false to disable open registration entirely.
     Set to true for invite-only (users need an admin-generated link). */
  allowSubscriptions: false,
  registrationLimit: 50,

  /* Paste your admin public key from the /settings page after first login. */
  // adminKeys: ['your-admin-public-key'],

  /* Storage limits per user in MB (0 = use default 1 GB). */
  defaultStorageLimit: 500 * 1024 * 1024,  // 500 MB

  /* Log verbosity: 'info', 'warn', 'error' */
  verbose: false,
  logFeedback: false,
  logLevel: 'warn',

  /* Enforce HTTPS. Never set to false in production. */
  httpAddress: '0.0.0.0',
  httpPort: 3000,

  /* Websocket keeps long-lived connections for real-time collaboration. */
  websocketPath: '/cryptpad_websocket',
};
EOF

Registration Policy Explained

  • Open registration: Remove registrationLimit entirely and leave allowSubscriptions unset.
  • Capped open registration: Set registrationLimit to an integer — new signups are blocked once that count is reached.
  • Invite-only: Set allowSubscriptions: true — only users with a link generated from the admin panel can register.
  • Closed: Set allowSubscriptions: false and registrationLimit: 0 — no new accounts at all.

Step 4 — Obtain TLS Certificates

Certbot standalone mode works before nginx is running. Run this on the host (not in a container) if port 80 is free, then copy certs into the nginx volume path.

sudo apt install -y certbot   # or: sudo dnf install certbot / sudo pacman -S certbot
sudo certbot certonly --standalone \
  -d pad.example.com \
  -d sandbox.example.com \
  --agree-tos --email [email protected] --non-interactive

sudo cp /etc/letsencrypt/live/pad.example.com/fullchain.pem /opt/cryptpad/nginx/certs/
sudo cp /etc/letsencrypt/live/pad.example.com/privkey.pem   /opt/cryptpad/nginx/certs/
sudo chown root:root /opt/cryptpad/nginx/certs/*.pem
sudo chmod 640 /opt/cryptpad/nginx/certs/*.pem

Set up automatic renewal. Since nginx will hold port 80 after initial setup, use the webroot or DNS-01 challenge for renewals.

# Add to crontab or a systemd timer:
# 0 3 * * * certbot renew --quiet --deploy-hook "docker exec nginx nginx -s reload"

Step 5 — Write the Nginx Configuration

cat > /opt/cryptpad/nginx/conf.d/cryptpad.conf <<'EOF'
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name pad.example.com sandbox.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name pad.example.com sandbox.example.com;

    ssl_certificate     /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # Required security headers for CryptPad
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header Permissions-Policy interest-cohort=();

    location / {
        proxy_pass         http://cryptpad:3000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection $connection_upgrade;
        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;
        client_max_body_size 150m;
    }
}
EOF

Step 6 — Start and Verify

cd /opt/cryptpad
docker compose up -d
docker compose logs -f cryptpad

Wait for the line CryptPad server started in the logs (output will vary). Then open https://pad.example.com in a browser. Register your first account — this will become the admin account once you add its public key to the config.

Set the Admin Key

  1. Log in, go to Settings → Account, copy your Public Signing Key.
  2. Edit cryptpad_config.js, uncomment and populate adminKeys.
  3. Restart the container: docker compose restart cryptpad
  4. The Admin panel will now appear in your user menu.

Firewall Rules

Only ports 80 and 443 need to be reachable from the internet. Apply rules appropriate to your system:

# ufw (Ubuntu/Debian)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
# firewalld (Fedora/RHEL)
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload

Troubleshooting

  • Permission denied on data volumes: Confirm all ./data subdirectories are owned by UID 4001. Run sudo chown -R 4001:4001 /opt/cryptpad/data and recreate the container.
  • WebSocket errors / real-time editing broken: Nginx must pass the Upgrade header. Verify the map block and proxy_set_header Upgrade lines are present and nginx reloaded after any config change.
  • Sandbox domain shows a blank page: The sandbox domain must be a genuinely different origin — a subdomain of the main domain. Verify both DNS records resolve and the TLS cert covers both. Check httpSafeOrigin in cryptpad_config.js matches exactly.
  • Large file uploads fail: Increase client_max_body_size in the nginx config and set maxUploadSize in cryptpad_config.js to a matching byte value.
  • Container restarts in a loop: Run docker compose logs cryptpad and look for JavaScript syntax errors in your config file — a missing comma or unmatched brace is the most common cause.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Why does CryptPad need two domains instead of one?
The sandbox domain isolates untrusted document content in a separate browser origin. This prevents a malicious document from accessing cookies or local storage belonging to your main session — it is a core security boundary, not optional.
Can I use a single wildcard certificate for both domains?
Yes. A certificate covering *.example.com is valid for both pad.example.com and sandbox.example.com. You can request a wildcard certificate with Certbot using the DNS-01 challenge via your DNS provider's API.
How do I update CryptPad to a new version?
Pull the latest image and recreate the container: run 'docker compose pull && docker compose up -d'. CryptPad may run database migrations on first start after an upgrade; watch the logs and do not interrupt the process.
Is data actually private from the server operator?
Document content is encrypted before it leaves the browser using keys derived from the URL fragment, which is never sent to the server. The server stores only ciphertext. However, metadata such as account existence, timestamps, and IP addresses in logs may still be visible to the operator.
How do I back up CryptPad data?
Stop or pause the cryptpad container, then archive the entire /opt/cryptpad/data directory. The datastore, blob, and pins subdirectories contain all document data and user accounts. Restore by replacing those directories before restarting.

Related guides