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.
Before starting, ensure you have:
Obtain these from the Vaultody portal before deployment:
| Credential | Description | How to Obtain |
|---|---|---|
| API Key | Authenticates your node with Vaultody | Vaultody portal → MPC Node Configuration |
| API Secret | Secret for API authentication | Generated alongside API Key |
| Private Key | Your node's ECDSA private key (Base64) | Generate using OpenSSL (see below) |
| AMQP Server URL | Message queue connection string | Provided by Vaultody |
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4 cores |
| RAM | 2 GB | 4 GB |
| Storage | 20 GB SSD | 50 GB SSD |
| Network | 100 Mbps | 1 Gbps |
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:
openssl, curl, jq, ufw)./opt/mpc-co-signer.config.yaml, .env, Caddyfile, and docker-compose.yaml./opt/mpc-backups/.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.)
Make the script executable:
chmod +x mpc-co-sign-deployer-selfhosted.sh
Run the script as root or with sudo:
sudo ./mpc-co-sign-deployer-selfhosted.sh
The script will prompt you for:
mpc-node.yourdomain.com)2)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.
Ubuntu/Debian:
sudo apt update && sudo apt upgrade -y
RHEL/Rocky/Alma:
sudo dnf update -y
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
# 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
# Ubuntu/Debian
sudo apt install -y openssl curl jq
# RHEL/Rocky/Alma
sudo dnf install -y openssl curl jq
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
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.
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:
| Parameter | Description | Default |
|---|---|---|
api.port | Port the container listens on | 8000 |
log.level | Logging verbosity: debug, info, warn, error | info |
log.path | Log output path (/dev/stdout for container logs) | /dev/stdout |
db.driver | Database driver: sqlite or gcs | sqlite |
db.source | Database file path (inside container) | /data/mpc_node.db |
node.index | Your node's index in the MPC cluster (1, 2, or 3) | - |
node.threshold | Minimum nodes required for signing | 2 |
node.playersCount | Total nodes in MPC cluster | 3 |
node.sessionTimeoutMilliseconds | MPC session timeout | 20000 |
Note: The node.index should be unique for each co-signer in the MPC cluster. Coordinate with Vaultody to determine your assigned index.
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
If you need to generate a secure password:
# Generate a 24-character random password
openssl rand -base64 24
Caddy is recommended because it automatically obtains and renews Let's Encrypt certificates with zero configuration.
| Feature | Caddy | Nginx + Certbot |
|---|---|---|
| Automatic HTTPS | ✅ Built-in | ❌ Requires certbot |
| Certificate Renewal | ✅ Automatic | ⚠️ Requires cron job |
| Configuration | Simple | Complex |
| Setup Time | ~5 minutes | ~30 minutes |
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 domaincat > /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
MPC Co-signer Service:
Caddy Service:
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
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
cd /opt/mpc-co-signer
docker compose pull
# 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
# 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
# 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
# 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"}
# View container health status
docker inspect mpc-co-signer --format='{{.State.Health.Status}}'
# Should return: healthy
# Check if SQLite database was created
ls -la /opt/mpc-co-signer/data/
# Expected output:
# -rw-r--r-- 1 ... mpc_node.db
# 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
# 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
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
# 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
# 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
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
# 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/
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
# 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
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
# Ensure Docker socket is not exposed
ls -la /var/run/docker.sock
# Should show:
# srw-rw---- 1 root docker ... /var/run/docker.sock
# 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)'
# 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
# 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
# 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;"
# 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
| Error | Cause | Solution |
|---|---|---|
dial tcp: connection refused | Container not running | Check logs, restart container |
certificate not valid | DNS not propagated | Wait for DNS, verify A record |
permission denied | File permissions | Check .env and config permissions |
database is locked | Concurrent access | Restart container |
invalid API key | Wrong credentials | Verify API_KEY and API_SECRET |
If you prefer Nginx over Caddy, here's the setup:
Ubuntu/Debian:
sudo apt install -y nginx certbot python3-certbot-nginx
RHEL/Rocky/Alma:
sudo dnf install -y nginx certbot python3-certbot-nginx
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
# Get certificate (Certbot will modify nginx config)
sudo certbot --nginx -d mpc-node.yourdomain.com
# Test auto-renewal
sudo certbot renew --dry-run
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
# Start Nginx
sudo systemctl enable nginx
sudo systemctl start nginx
# Start Docker container
cd /opt/mpc-co-signer
docker compose up -d
# 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