Self-Hosted MPC Co-signer Deployment Guide

Self-Hosted MPC Co-signer Deployment Guide

This guide covers deploying the MPC co-signer container on your own Linux server or on-premise infrastructure using Docker and Caddy for automatic TLS termination.


Prerequisites

Before starting, ensure you have:

  • A Linux server (Ubuntu 22.04/24.04/26.04, Debian 12, RHEL 8/9, or similar)
  • A domain name pointing to your server's public IP address
  • Root or sudo access to the server
  • Ports 80 and 443 open in your firewall (required for Let's Encrypt)
  • Port 8000 or 8001 available internally for the container

Credentials You'll Need

Obtain these from the Vaultody portal before deployment:

CredentialDescriptionHow to Obtain
API KeyAuthenticates your node with VaultodyVaultody portal → MPC Node Configuration
API SecretSecret for API authenticationGenerated alongside API Key
Private KeyYour node's ECDSA private key (Base64)Generate using OpenSSL (see below)
AMQP Server URLMessage queue connection stringProvided by Vaultody

System Requirements

Minimum Hardware

ComponentMinimumRecommended
CPU2 cores4 cores
RAM2 GB4 GB
Storage20 GB SSD50 GB SSD
Network100 Mbps1 Gbps

Supported Operating Systems

  • Ubuntu 22.04 LTS / 24.04 LTS / 26.04 LTS
  • Debian 11 / 12
  • RHEL 8 / 9
  • Rocky Linux 8 / 9
  • AlmaLinux 8 / 9

Automated Deployment (Quick Start)

For a fast and automated deployment on Ubuntu (22.04 / 24.04 / 26.04 LTS), you can use the provided automated script.

This script performs the following tasks:

  1. Installs all required system packages (openssl, curl, jq, ufw).
  2. Installs and configures Docker Engine.
  3. Sets up the directory structure at /opt/mpc-co-signer.
  4. Generates the ECDSA key pair and database password.
  5. Configures config.yaml, .env, Caddyfile, and docker-compose.yaml.
  6. Configures the UFW firewall (allows SSH, HTTP, and HTTPS).
  7. Pulls the Docker images and starts the containers.
  8. Configures a daily database backup cron job at /opt/mpc-backups/.
  9. Sets up a Systemd service to start the containers on system boot.

Running the Script

  1. Download the mpc-co-sign-deployer-selfhosted.sh script attached to the bottom of this article, then transfer it to your server:

    # From your local machine
    scp mpc-co-sign-deployer-selfhosted.sh user@your-server:/root/

    (Or open the attached script, copy its contents into a new file named mpc-co-sign-deployer-selfhosted.sh on the server.)

  2. Make the script executable:

    chmod +x mpc-co-sign-deployer-selfhosted.sh
  3. Run the script as root or with sudo:

    sudo ./mpc-co-sign-deployer-selfhosted.sh
  4. The script will prompt you for:

    • Your domain (e.g., mpc-node.yourdomain.com)
    • Your email address (for Let's Encrypt notifications)
    • API Key (from the Vaultody Portal)
    • API Secret (from the Vaultody Portal)
    • Node Index (1, 2, or 3, default 2)
  5. Once completed, the script will output your Public Key (Base64). Make sure to copy it and save it, then upload/enter it in the Vaultody Portal to complete your co-signer setup.


Manual Installation

Step 1: Update System Packages

Ubuntu/Debian:

sudo apt update && sudo apt upgrade -y

RHEL/Rocky/Alma:

sudo dnf update -y

Step 2: Install Docker

Ubuntu/Debian:

# Install prerequisites
sudo apt install -y ca-certificates curl gnupg lsb-release

# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add the repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "
$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

RHEL/Rocky/Alma:

# Install prerequisites
sudo dnf install -y yum-utils

# Add Docker repository
sudo yum-config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo

# Install Docker
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Start and enable Docker
sudo systemctl start docker
sudo systemctl enable docker

Step 3: Configure Docker (All Distributions)

# Add your user to the docker group (avoids needing sudo)
sudo usermod -aG docker $USER

# Apply group changes (or log out and back in)
newgrp docker

# Verify Docker installation
docker --version
docker compose version

Step 4: Install Additional Tools

# Ubuntu/Debian
sudo apt install -y openssl curl jq

# RHEL/Rocky/Alma
sudo dnf install -y openssl curl jq

Directory Structure

Create the deployment directory structure:

# Create base directory
sudo mkdir -p /opt/mpc-co-signer
cd /opt/mpc-co-signer

# Create subdirectories
sudo mkdir -p config data certs

# Set ownership (replace 'your-user' with your username)
sudo chown -R $USER:$USER /opt/mpc-co-signer

# Set permissions
chmod 700 config data
chmod 755 certs

Final structure:

/opt/mpc-co-signer/
├── config/
│ └── config.yaml # Application configuration
├── data/
│ └── mpc_node.db # SQLite database (created automatically)
├── certs/ # TLS certificates (if using self-signed)
├── .env # Environment variables/secrets
├── docker-compose.yaml # Docker Compose configuration
└── Caddyfile # Caddy reverse proxy configuration

Configuration

Step 1: Generate Cryptographic Keys

Generate your node's ECDSA key pair:

cd /opt/mpc-co-signer

# Generate private key in DER format
openssl ecparam -name prime256v1 -genkey -noout -out private_key.der -outform DER

# Extract public key
openssl ec -in private_key.der -inform DER -pubout -out public_key.der -outform DER

# Convert private key to Base64 (you'll need this for .env file)
echo "Your Private Key (Base64):"
openssl base64 -A -in private_key.der
echo ""

# Convert public key to Base64 (upload this to Vaultody portal)
echo "Your Public Key (Base64) - Upload to Vaultody Portal:"
openssl base64 -A -in public_key.der
echo ""

# Securely delete the key files after copying the values
shred -vfz -n 10 private_key.der public_key.der 2>/dev/null || rm -f private_key.der public_key.der

Important: Save the Base64-encoded private key securely. You'll need it for the .env file.

Step 2: Create Configuration File

Create the application configuration:

cat > /opt/mpc-co-signer/config/config.yaml << 'EOF'
api:
port: 8000
log:
level: "info"
path: "/dev/stdout"
db:
driver: "sqlite"
source: "/data/mpc_node.db"
node:
index: 2
threshold: 2
playersCount: 3
sessionTimeoutMilliseconds: 20000
EOF

Configuration Options:

ParameterDescriptionDefault
api.portPort the container listens on8000
log.levelLogging verbosity: debug, info, warn, errorinfo
log.pathLog output path (/dev/stdout for container logs)/dev/stdout
db.driverDatabase driver: sqlite or gcssqlite
db.sourceDatabase file path (inside container)/data/mpc_node.db
node.indexYour node's index in the MPC cluster (1, 2, or 3)-
node.thresholdMinimum nodes required for signing2
node.playersCountTotal nodes in MPC cluster3
node.sessionTimeoutMillisecondsMPC session timeout20000

Note: The node.index should be unique for each co-signer in the MPC cluster. Coordinate with Vaultody to determine your assigned index.

Step 3: Create Environment File

Create the secrets file:

cat > /opt/mpc-co-signer/.env << 'EOF'
# Database encryption password (minimum 12 characters, mixed case + numbers)
DB_MASTER_PASSWORD=YourSecurePassword123!

# Your node's private key (Base64 encoded)
PRIVATE_KEY=your-base64-encoded-private-key-here

# API credentials from Vaultody portal
API_KEY=your-api-key-from-vaultody
API_SECRET=your-api-secret-from-vaultody

# AMQP connection string (provided by Vaultody)
AMQP_SERVER_URL=amqps://username:password@hostname/vhost

# Config file path inside container
CONFIG_FILE=/config/config.yaml
EOF

Secure the environment file:

chmod 600 /opt/mpc-co-signer/.env

Step 4: Generate Strong Database Password

If you need to generate a secure password:

# Generate a 24-character random password
openssl rand -base64 24

TLS Setup with Caddy

Caddy is recommended because it automatically obtains and renews Let's Encrypt certificates with zero configuration.

Why Caddy?

FeatureCaddyNginx + Certbot
Automatic HTTPS✅ Built-in❌ Requires certbot
Certificate Renewal✅ Automatic⚠️ Requires cron job
ConfigurationSimpleComplex
Setup Time~5 minutes~30 minutes

Step 1: Create Caddyfile

cat > /opt/mpc-co-signer/Caddyfile << 'EOF'
{
# Global options
email your-email@example.com
}

# Replace with your actual domain
mpc-node.yourdomain.com {
# Reverse proxy to MPC co-signer container
reverse_proxy mpc-co-signer:8000 {
# Health check configuration
health_uri /health/liveness
health_interval 30s
health_timeout 10s
}

# Enable compression
encode gzip

# Security headers
header {
# Prevent clickjacking
X-Frame-Options "DENY"
# XSS protection
X-Content-Type-Options "nosniff"
# Referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
# Remove server header
-Server
}

# Logging
log {
output file /var/log/caddy/access.log {
roll_size 100mb
roll_keep 5
}
}
}
EOF

Replace:

  • your-email@example.com with your email (for Let's Encrypt notifications)
  • mpc-node.yourdomain.com with your actual domain

Docker Compose Deployment

Create Docker Compose File

cat > /opt/mpc-co-signer/docker-compose.yaml << 'EOF'
version: "3.8"

services:
mpc-co-signer:
image: europe-west1-docker.pkg.dev/vaultody/vaultody-public/mpc-co-signer:latest
container_name: mpc-co-signer
restart: unless-stopped
env_file:
- .env
volumes:
- ./config:/config:ro
- ./data:/data
networks:
- mpc-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/liveness"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Internal only - Caddy handles external traffic
expose:
- "8000"

caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
- caddy_logs:/var/log/caddy
networks:
- mpc-network
depends_on:
- mpc-co-signer

networks:
mpc-network:
driver: bridge

volumes:
caddy_data:
caddy_config:
caddy_logs:
EOF

Configuration Breakdown

MPC Co-signer Service:

  • Uses the official Vaultody container image
  • Mounts configuration read-only
  • Mounts data directory for SQLite persistence
  • Only exposed internally to Caddy (not to the host)
  • Includes health check for automatic restart on failure

Caddy Service:

  • Handles TLS termination with automatic Let's Encrypt
  • Exposes ports 80 (HTTP redirect) and 443 (HTTPS)
  • Persists certificates in Docker volumes
  • Reverse proxies to the MPC container

Running the Service

Step 1: Configure Firewall

Ubuntu/Debian (UFW):

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

RHEL/Rocky/Alma (firewalld):

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Step 2: Verify DNS Configuration

Before starting, ensure your domain points to your server:

# Check DNS resolution
dig +short mpc-node.yourdomain.com

# Should return your server's public IP

Step 3: Pull the Container Image

cd /opt/mpc-co-signer
docker compose pull

Step 4: Start the Services

# Start in detached mode
docker compose up -d

# View logs
docker compose logs -f

# View only MPC co-signer logs
docker compose logs -f mpc-co-signer

# View only Caddy logs
docker compose logs -f caddy

Step 5: Verify Services Are Running

# Check container status
docker compose ps

# Expected output:
# NAME STATUS PORTS
# caddy Up (healthy) 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
# mpc-co-signer Up (healthy) 8000/tcp

Verification and Testing

Step 1: Check Certificate Status

# View Caddy logs for certificate provisioning
docker compose logs caddy | grep -i "certificate"

# Test HTTPS connection
curl -I https://mpc-node.yourdomain.com/health/liveness

Step 2: Test Health Endpoints

# Liveness check (container is running)
curl https://mpc-node.yourdomain.com/health/liveness

# Readiness check (service is operational)
curl https://mpc-node.yourdomain.com/health/readiness

# Expected response:
# {"status":"ok"}

Step 3: Check Container Health

# View container health status
docker inspect mpc-co-signer --format='{{.State.Health.Status}}'

# Should return: healthy

Step 4: Verify Database Creation

# Check if SQLite database was created
ls -la /opt/mpc-co-signer/data/

# Expected output:
# -rw-r--r-- 1 ... mpc_node.db

Maintenance and Operations

Viewing Logs

# Real-time logs (all services)
docker compose logs -f

# Last 100 lines of MPC container
docker compose logs --tail=100 mpc-co-signer

# Export logs to file
docker compose logs > /tmp/mpc-logs.txt

Restarting Services

# Restart all services
docker compose restart

# Restart only MPC container
docker compose restart mpc-co-signer

# Stop all services
docker compose down

# Start all services
docker compose up -d

Updating the Container

cd /opt/mpc-co-signer

# Pull latest image
docker compose pull

# Recreate container with new image
docker compose up -d

# Verify new version is running
docker compose ps

Backing Up Data

# Create backup directory
mkdir -p /opt/mpc-backups

# Backup SQLite database
cp /opt/mpc-co-signer/data/mpc_node.db /opt/mpc-backups/mpc_node_$(date +%Y%m%d_%H%M%S).db

# Backup configuration
tar -czvf /opt/mpc-backups/config_$(date +%Y%m%d).tar.gz \
/opt/mpc-co-signer/config \
/opt/mpc-co-signer/.env \
/opt/mpc-co-signer/Caddyfile \
/opt/mpc-co-signer/docker-compose.yaml

Automated Backups (Cron)

# Add to crontab
crontab -e

# Add this line for daily backups at 2 AM
0 2 * * * cp /opt/mpc-co-signer/data/mpc_node.db /opt/mpc-backups/mpc_node_$(date +\%Y\%m\%d).db && find /opt/mpc-backups -name "mpc_node_*.db" -mtime +30 -delete

Setting Up as Systemd Service

For automatic start on boot:

cat | sudo tee /etc/systemd/system/mpc-co-signer.service << 'EOF'
[Unit]
Description=MPC Co-signer Docker Compose Service
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/mpc-co-signer
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target
EOF

# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable mpc-co-signer
sudo systemctl start mpc-co-signer

# Check status
sudo systemctl status mpc-co-signer

Security Hardening

1. Restrict File Permissions

# Secure sensitive files
chmod 600 /opt/mpc-co-signer/.env
chmod 600 /opt/mpc-co-signer/config/config.yaml
chmod 700 /opt/mpc-co-signer/data

# Verify permissions
ls -la /opt/mpc-co-signer/

2. Enable Automatic Security Updates

Ubuntu/Debian:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

RHEL/Rocky/Alma:

sudo dnf install -y dnf-automatic
sudo systemctl enable --now dnf-automatic.timer

3. Configure Fail2ban (Optional)

# Install fail2ban
sudo apt install -y fail2ban # Ubuntu/Debian
sudo dnf install -y fail2ban # RHEL/Rocky/Alma

# Create jail configuration for Caddy
cat | sudo tee /etc/fail2ban/jail.d/caddy.conf << 'EOF'
[caddy]
enabled = true
port = http,https
filter = caddy
logpath = /var/lib/docker/volumes/mpc-co-signer_caddy_logs/_data/access.log
maxretry = 5
bantime = 3600
EOF

# Start fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

4. Network Security

Only allow necessary ports:

# UFW example - restrict to specific IPs if possible
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

5. Docker Security

# Ensure Docker socket is not exposed
ls -la /var/run/docker.sock

# Should show:
# srw-rw---- 1 root docker ... /var/run/docker.sock

Troubleshooting

Container Won't Start

# Check container logs
docker compose logs mpc-co-signer

# Check if config file is valid
cat /opt/mpc-co-signer/config/config.yaml

# Verify environment file
cat /opt/mpc-co-signer/.env

# Check for port conflicts
sudo netstat -tlnp | grep -E ':(80|443|8000)'

Certificate Not Provisioning

# Check Caddy logs
docker compose logs caddy | grep -i "tls\|certificate\|error"

# Verify DNS is correct
dig +short mpc-node.yourdomain.com

# Ensure ports 80 and 443 are open
sudo netstat -tlnp | grep -E ':(80|443)'

# Test port accessibility from outside
# From another machine:
nc -zv your-server-ip 80
nc -zv your-server-ip 443

Health Checks Failing

# Test health endpoint directly from container
docker exec mpc-co-signer curl -s http://localhost:8000/health/liveness

# Check if application is listening
docker exec mpc-co-signer netstat -tlnp

# View detailed container logs
docker compose logs --tail=200 mpc-co-signer

Database Issues

# Check database file
ls -la /opt/mpc-co-signer/data/

# Check permissions
stat /opt/mpc-co-signer/data/

# Verify SQLite file integrity
docker exec mpc-co-signer sqlite3 /data/mpc_node.db "PRAGMA integrity_check;"

Connection Refused

# Verify container is running
docker compose ps

# Check internal network
docker network inspect mpc-co-signer_mpc-network

# Test internal connectivity
docker exec caddy ping -c 3 mpc-co-signer

Common Error Messages

ErrorCauseSolution
dial tcp: connection refusedContainer not runningCheck logs, restart container
certificate not validDNS not propagatedWait for DNS, verify A record
permission deniedFile permissionsCheck .env and config permissions
database is lockedConcurrent accessRestart container
invalid API keyWrong credentialsVerify API_KEY and API_SECRET

Alternative: Nginx + Certbot

If you prefer Nginx over Caddy, here's the setup:

Step 1: Install Nginx and Certbot

Ubuntu/Debian:

sudo apt install -y nginx certbot python3-certbot-nginx

RHEL/Rocky/Alma:

sudo dnf install -y nginx certbot python3-certbot-nginx

Step 2: Create Nginx Configuration

cat | sudo tee /etc/nginx/sites-available/mpc-co-signer << 'EOF'
server {
listen 80;
server_name mpc-node.yourdomain.com;

location / {
return 301 https://$server_name$request_uri;
}
}

server {
listen 443 ssl http2;
server_name mpc-node.yourdomain.com;

# SSL certificates (will be added by Certbot)
ssl_certificate /etc/letsencrypt/live/mpc-node.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mpc-node.yourdomain.com/privkey.pem;

# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;

# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;

location / {
proxy_pass http://127.0.0.1:8000;
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;
}

location /health/ {
proxy_pass http://127.0.0.1:8000;
proxy_connect_timeout 5s;
proxy_read_timeout 10s;
}
}
EOF

# Enable the site
sudo ln -sf /etc/nginx/sites-available/mpc-co-signer /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default

Step 3: Obtain Certificate

# Get certificate (Certbot will modify nginx config)
sudo certbot --nginx -d mpc-node.yourdomain.com

# Test auto-renewal
sudo certbot renew --dry-run

Step 4: Modified Docker Compose (No Caddy)

cat > /opt/mpc-co-signer/docker-compose.yaml << 'EOF'
version: "3.8"

services:
mpc-co-signer:
image: europe-west1-docker.pkg.dev/vaultody/vaultody-public/mpc-co-signer:latest
container_name: mpc-co-signer
restart: unless-stopped
env_file:
- .env
volumes:
- ./config:/config:ro
- ./data:/data
ports:
- "127.0.0.1:8000:8000" # Only bind to localhost
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/liveness"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
EOF

Step 5: Start Services

# Start Nginx
sudo systemctl enable nginx
sudo systemctl start nginx

# Start Docker container
cd /opt/mpc-co-signer
docker compose up -d

Quick Reference Commands

# Start services
cd /opt/mpc-co-signer && docker compose up -d

# Stop services
docker compose down

# View logs
docker compose logs -f

# Restart MPC container
docker compose restart mpc-co-signer

# Update container
docker compose pull && docker compose up -d

# Check health
curl https://mpc-node.yourdomain.com/health/readiness

# Backup database
cp /opt/mpc-co-signer/data/mpc_node.db ~/mpc_backup_$(date +%Y%m%d).db

# Check certificate expiry (Caddy)
docker exec caddy caddy list-certificates

Version: 1.0.0