Privacy beginner ⏱ 20 minutes

PrivateBin: Self-Hosted Encrypted Pastebin

Zero-knowledge encrypted pastebin. Share text and files securely with expiring links.

By homelabbing.io • 2025-12-17
#privatebin#pastebin#encryption#privacy#docker

Introduction

PrivateBin is a minimalist, open-source online pastebin where the server has zero knowledge of pasted data. All encryption and decryption happens in the browser using 256-bit AES, making it impossible for the server operator to read the content. Perfect for securely sharing passwords, code snippets, configuration files, or sensitive information with automatic expiration and burn-after-reading features.

By the end of this guide, you’ll have a production-ready PrivateBin installation that:

  • Encrypts all data client-side before sending to the server
  • Supports text pastes and file attachments up to 10MB
  • Offers burn-after-reading (self-destruct after first view)
  • Provides configurable expiration times
  • Includes discussion threads for collaborative notes
  • Features syntax highlighting for code
  • Maintains complete privacy with zero-knowledge architecture

Prerequisites

Before starting, ensure you have:

  • A Linux server (VM, VPS, or bare metal) with at least 256MB RAM
  • Docker and Docker Compose installed
  • A domain name (optional but recommended for HTTPS)
  • Port 80 and 443 available (or alternative ports)
  • Root or sudo access
  • At least 5GB free disk space
  • Basic understanding of Docker

Why Use PrivateBin?

Zero-Knowledge Architecture

Client-Side Encryption:

  • All encryption happens in your browser before data leaves your device
  • Server only stores encrypted blobs
  • Even server administrators cannot read content
  • Decryption key never sent to server

Privacy by Design:

  • No logs or tracking
  • No cookies required
  • No JavaScript analytics
  • Open source and auditable

Use Cases

Secure Sharing:

  • Passwords and API keys
  • Configuration files with secrets
  • Private code snippets
  • Personal notes and documents
  • Incident reports

Collaboration:

  • Bug reports with sensitive data
  • Security vulnerability reports
  • Team credentials sharing
  • Temporary file exchange

Ephemeral Data:

  • One-time secret sharing
  • Time-limited access to information
  • Self-destructing messages

Architecture Overview

Our deployment consists of:

  1. PrivateBin Container: Core pastebin application
  2. Web Server: Nginx for reverse proxy and HTTPS
  3. Storage: File-based or Redis for paste storage
  4. Optional: Let’s Encrypt for automatic SSL

Data flow:

  • User creates paste in browser
  • Data encrypted with AES-256 in JavaScript
  • Encrypted blob sent to server with metadata
  • Server stores blob without decryption key
  • Share link includes decryption key in URL fragment
  • Recipient’s browser decrypts content

Directory Structure Setup

Create the necessary directory structure:

mkdir -p ~/privatebin/{data,nginx,scripts,backups}
cd ~/privatebin

Create subdirectories:

mkdir -p data/uploads
mkdir -p nginx/conf.d

Docker Compose Configuration

Create a docker-compose.yml file:

version: '3.8'

services:
  privatebin:
    image: privatebin/nginx-fpm-alpine:latest
    container_name: privatebin
    restart: unless-stopped
    read_only: true
    volumes:
      - ./data:/srv/data
      - ./conf.php:/srv/cfg/conf.php:ro
    ports:
      - "8080:8080"
    environment:
      TZ: America/New_York
      PHP_TZ: America/New_York
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=64M
      - /var/tmp:rw,noexec,nosuid,size=64M
    networks:
      - privatebin_net

  nginx:
    image: nginx:alpine
    container_name: privatebin_nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    networks:
      - privatebin_net
    depends_on:
      - privatebin

networks:
  privatebin_net:
    driver: bridge

PrivateBin Configuration

Create a conf.php file for PrivateBin configuration:

<?php
// Configuration file for PrivateBin

[main]
; Name displayed in the title bar
name = "PrivateBin"

; Discussion forum feature
discussion = true

; Allow users to set password protection
password = true

; Allow file uploads
fileupload = true

; Burn after reading feature
burnafterreadingselected = false

; Default expiration time (5min, 10min, 1hour, 1day, 1week, 1month, 1year, never)
default = "1week"

; Show clone button
clone = true

; QR code generation
qrcode = true

; Email notification when paste is read
email = true

; Show visitor counter
zerobincompatibility = false

; Template to use (bootstrap, page)
template = "bootstrap"

; Syntax highlighting
syntaxhighlightingtheme = "sons-of-obsidian"

; Size limit for attachments (10 MiB)
sizelimit = 10485760

; Compression type (zlib, none)
compression = "zlib"

[expire]
; Expiration options available
default = "1week"

[expire_options]
; Seconds, minutes, hours, days, weeks, months, years
5min = 300
10min = 600
1hour = 3600
1day = 86400
1week = 604800
1month = 2592000
1year = 31536000
never = 0

[traffic]
; Time limit between calls from the same IP
limit = 10

; Traffic directory
dir = PATH "data"

[purge]
; Minimum time between purge runs
limit = 300

; Path to purge script
dir = PATH "data"

[model]
; Storage backend (Filesystem, Database, S3, GoogleCloudStorage)
class = Filesystem

[model_options]
; Directory for file-based storage
dir = PATH "data"

Nginx Reverse Proxy Configuration

Create nginx/conf.d/privatebin.conf:

server {
    listen 80;
    server_name paste.yourdomain.com;

    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name paste.yourdomain.com;

    # SSL Configuration
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "no-referrer" always;
    add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;

    # Max upload size
    client_max_body_size 10M;

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

    # Logging
    access_log /var/log/nginx/privatebin_access.log;
    error_log /var/log/nginx/privatebin_error.log;
}

SSL Certificate Setup

Option 1: Self-Signed Certificate (Testing)

Generate a self-signed certificate:

mkdir -p nginx/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048     -keyout nginx/ssl/key.pem     -out nginx/ssl/cert.pem     -subj "/C=US/ST=State/L=City/O=Organization/CN=paste.yourdomain.com"

Option 2: Let’s Encrypt (Production)

Install certbot:

sudo apt update
sudo apt install certbot

Obtain certificate:

sudo certbot certonly --standalone -d paste.yourdomain.com

Copy certificates:

sudo cp /etc/letsencrypt/live/paste.yourdomain.com/fullchain.pem nginx/ssl/cert.pem
sudo cp /etc/letsencrypt/live/paste.yourdomain.com/privkey.pem nginx/ssl/key.pem
sudo chown $USER:$USER nginx/ssl/*.pem

Set up auto-renewal:

sudo crontab -e

Add:

0 3 * * * certbot renew --quiet && cp /etc/letsencrypt/live/paste.yourdomain.com/fullchain.pem /path/to/privatebin/nginx/ssl/cert.pem && cp /etc/letsencrypt/live/paste.yourdomain.com/privkey.pem /path/to/privatebin/nginx/ssl/key.pem && docker-compose restart nginx

Starting PrivateBin

Start all services:

docker-compose up -d

Verify containers are running:

docker-compose ps

Check logs:

docker-compose logs -f privatebin
docker-compose logs -f nginx

Access your PrivateBin instance:

https://paste.yourdomain.com

Using PrivateBin

Creating a Paste

Basic Text Paste:

  1. Navigate to your PrivateBin URL
  2. Enter or paste text in the main editor
  3. Select expiration time (5 min to never)
  4. Optional: Enable “Burn after reading”
  5. Optional: Add password protection
  6. Optional: Enable “Open discussion”
  7. Click “Send”
  8. Copy the generated URL

The URL Structure:

https://paste.yourdomain.com/?a1b2c3d4e5f6g7h8#i9j0k1l2m3n4o5p6
                             |_______________|  |_______________|
                             Paste ID (server)  Encryption key (client)

The key after the hash (#) never reaches the server.

Attaching Files

Upload a File:

  1. Click “Attach a file” tab
  2. Select file (max 10MB by default)
  3. File is encrypted client-side
  4. Set expiration and options
  5. Click “Send”
  6. Share URL with recipient

File Types:

  • Documents (PDF, DOCX, TXT)
  • Images (PNG, JPG, GIF)
  • Archives (ZIP, TAR, GZ)
  • Code files
  • Configuration files

Password Protection

Add Password to Paste:

  1. Create your paste
  2. Check “Password (recommended)”
  3. Enter password
  4. Click “Send”
  5. Share URL and password separately via different channels

Best Practice:

  • Use strong, unique passwords
  • Share URL and password via different communication methods
  • Consider using password + burn after reading for maximum security

Burn After Reading

Self-Destructing Pastes:

  1. Create paste
  2. Enable “Burn after reading”
  3. Click “Send”
  4. Paste will be permanently deleted after first view
  5. Viewer gets one-time access only

Use Cases:

  • One-time password sharing
  • Temporary API keys
  • Sensitive documents
  • Emergency access codes

Discussion Feature

Open Discussion:

  1. Create paste
  2. Enable “Open discussion”
  3. Click “Send”
  4. Recipients can reply with comments
  5. All comments are encrypted
  6. Discussion thread preserved until expiration

Collaboration:

  • Bug reports with Q&A
  • Shared troubleshooting
  • Temporary project notes
  • Team coordination

Syntax Highlighting

PrivateBin automatically detects and highlights code:

Supported Languages:

  • Python, JavaScript, PHP, Ruby, Go
  • Java, C, C++, C#, Rust
  • HTML, CSS, SQL, Bash
  • JSON, YAML, XML, Markdown
  • And 150+ more languages

Manual Selection:

  1. Paste code
  2. Click “Format” button
  3. Select language from dropdown
  4. Syntax highlighting applied automatically

Advanced Configuration

Redis Backend for Better Performance

For high-traffic instances, use Redis instead of filesystem:

Add to docker-compose.yml:

  redis:
    image: redis:alpine
    container_name: privatebin_redis
    restart: unless-stopped
    volumes:
      - ./redis:/data
    networks:
      - privatebin_net

Update conf.php:

[model]
class = Database

[model_options]
dsn = "mysql:host=mysql;dbname=privatebin;charset=UTF8"
tbl = "privatebin_"
usr = "privatebin"
pwd = "SECURE_PASSWORD"
opt[12] = true

Or for Redis:

[model]
class = Filesystem

[model_options]
dir = PATH "data"

S3 Storage Backend

For distributed deployments:

[model]
class = S3

[model_options]
region = "us-east-1"
version = "latest"
endpoint = "https://s3.amazonaws.com"
bucket = "my-privatebin-bucket"
prefix = "pastes/"

Set AWS credentials in docker-compose.yml:

    environment:
      AWS_ACCESS_KEY_ID: "your-key-id"
      AWS_SECRET_ACCESS_KEY: "your-secret-key"

Custom Expiration Times

Modify conf.php to add custom expiration options:

[expire_options]
30min = 1800
6hours = 21600
3days = 259200
2weeks = 1209600
3months = 7776000

Rate Limiting

Prevent abuse with traffic limiting:

[traffic]
; Minimum seconds between requests from same IP
limit = 10

; Enable traffic limiter
exempted = "127.0.0.1,192.168.1.0/24"

Size Limits

Adjust maximum paste size:

[main]
; 50 MiB limit
sizelimit = 52428800

; Or disable limits entirely (not recommended)
sizelimit = 0

Update nginx configuration to match:

client_max_body_size 50M;

Security Best Practices

Enable HTTPS Only

Always use HTTPS in production:

  1. Obtain valid SSL certificate (Let’s Encrypt recommended)
  2. Configure strong SSL/TLS settings
  3. Enable HTTP Strict Transport Security (HSTS)
  4. Force HTTPS redirects

Content Security Policy

The provided nginx config includes strict CSP headers:

Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';

This prevents:

  • XSS attacks
  • Data injection
  • Clickjacking
  • Unauthorized resource loading

Firewall Configuration

Restrict access to PrivateBin:

# Allow HTTPS
sudo ufw allow 443/tcp

# Allow HTTP (for Let's Encrypt validation)
sudo ufw allow 80/tcp

# Block direct access to container port
sudo ufw deny 8080/tcp

# Enable firewall
sudo ufw enable

Regular Updates

Keep PrivateBin updated:

# Pull latest image
docker-compose pull

# Recreate containers
docker-compose up -d

Disable Unnecessary Features

If you don’t need certain features, disable them in conf.php:

[main]
; Disable file uploads
fileupload = false

; Disable discussions
discussion = false

; Disable email notifications
email = false

; Disable QR codes
qrcode = false

Backup Procedures

Automated Backup Script

Create scripts/backup-privatebin.sh:

#!/bin/bash

BACKUP_DIR="./backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="privatebin_backup_$TIMESTAMP"

# Create backup directory
mkdir -p "$BACKUP_DIR"

echo "Starting PrivateBin backup..."

# Backup data directory
echo "Backing up pastes and data..."
tar -czf "$BACKUP_DIR/$BACKUP_NAME.tar.gz" data/ conf.php

# Backup nginx configuration
tar -czf "$BACKUP_DIR/nginx_config_$TIMESTAMP.tar.gz" nginx/

# Keep only last 30 backups
echo "Cleaning old backups (keeping last 30)..."
cd "$BACKUP_DIR"
ls -t privatebin_backup_*.tar.gz | tail -n +31 | xargs -r rm

echo "Backup completed: $BACKUP_DIR/$BACKUP_NAME.tar.gz"

Make executable:

chmod +x scripts/backup-privatebin.sh

Automated Backups with Cron

crontab -e

Add daily backup at 3 AM:

0 3 * * * cd /path/to/privatebin && ./scripts/backup-privatebin.sh >> ./backups/backup.log 2>&1

Restore Procedure

Create scripts/restore-privatebin.sh:

#!/bin/bash

if [ -z "$1" ]; then
    echo "Usage: $0 backup_file.tar.gz"
    exit 1
fi

BACKUP_FILE="$1"

echo "Restoring from $BACKUP_FILE..."

# Stop containers
docker-compose down

# Backup current state
mv data data.bak_$(date +%Y%m%d_%H%M%S)

# Extract backup
tar -xzf "$BACKUP_FILE"

# Start containers
docker-compose up -d

echo "Restore completed successfully!"

Make executable:

chmod +x scripts/restore-privatebin.sh

Monitoring and Maintenance

Health Check Script

Create scripts/healthcheck.sh:

#!/bin/bash

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

echo "=== PrivateBin Health Check ==="
echo ""

# Check containers
echo -n "PrivateBin container: "
if docker ps | grep -q privatebin; then
    echo -e "${GREEN}Running${NC}"
else
    echo -e "${RED}Not running${NC}"
fi

echo -n "Nginx container: "
if docker ps | grep -q privatebin_nginx; then
    echo -e "${GREEN}Running${NC}"
else
    echo -e "${RED}Not running${NC}"
fi

# Test HTTPS
echo -n "HTTPS connectivity: "
if curl -sk https://paste.yourdomain.com | grep -q "PrivateBin"; then
    echo -e "${GREEN}Working${NC}"
else
    echo -e "${RED}Failed${NC}"
fi

# Check disk space
echo ""
echo "Disk usage:"
df -h | grep -E "Filesystem|/mnt|/$"

# Check paste storage
echo ""
echo "Paste storage:"
du -sh data/

echo ""
echo "Number of pastes:"
find data/ -name "*.php" 2>/dev/null | wc -l

Make executable:

chmod +x scripts/healthcheck.sh

Purge Expired Pastes

PrivateBin automatically purges expired pastes, but you can manually trigger:

docker exec privatebin php /srv/bin/purge

Schedule regular purge:

crontab -e

Add:

0 */6 * * * docker exec privatebin php /srv/bin/purge >> /path/to/privatebin/logs/purge.log 2>&1

Log Management

Monitor logs for issues:

# View recent logs
docker-compose logs --tail=50 privatebin

# Follow logs in real-time
docker-compose logs -f privatebin

# Search for errors
docker-compose logs privatebin | grep -i error

Rotate logs:

# Add to cron
0 0 * * 0 docker-compose logs --no-color > /path/to/privatebin/logs/privatebin_$(date +%Y%m%d).log 2>&1

Mobile Device Configuration

iOS (iPhone/iPad)

Using PrivateBin:

  1. Open Safari or your preferred browser
  2. Navigate to your PrivateBin URL
  3. Bookmark for easy access
  4. Add to Home Screen:
    • Share Add to Home Screen
    • Icon appears like native app

Sharing from iOS:

  1. Copy text you want to share
  2. Open PrivateBin in browser
  3. Paste content
  4. Set expiration and options
  5. Send and share URL via Messages, Email, etc.

Android

Using PrivateBin:

  1. Open Chrome or your preferred browser
  2. Navigate to your PrivateBin URL
  3. Bookmark for easy access
  4. Add to Home Screen:
    • Menu Add to Home screen
    • Icon appears like native app

Browser Extensions:

  • Install “PrivateBin Share” (if available)
  • Enables sharing directly from Share menu

Performance Optimization

Nginx Caching

Add to nginx configuration:

# Cache static assets
location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# Gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;

PHP-FPM Tuning

For high-traffic sites, optimize PHP-FPM.

Create php-fpm.conf:

[www]
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500

Mount in docker-compose.yml:

    volumes:
      - ./php-fpm.conf:/etc/php8/php-fpm.d/www.conf:ro

Resource Monitoring

Monitor container resources:

# Real-time stats
docker stats privatebin

# Memory usage
docker exec privatebin free -h

# Disk usage
docker exec privatebin df -h

Troubleshooting

Common Issues

Issue: Cannot access PrivateBin

# Check container status
docker-compose ps

# Check logs
docker-compose logs privatebin
docker-compose logs nginx

# Verify nginx configuration
docker exec privatebin_nginx nginx -t

# Test direct container access
curl http://localhost:8080

Issue: SSL certificate errors

# Verify certificate files exist
ls -la nginx/ssl/

# Check certificate validity
openssl x509 -in nginx/ssl/cert.pem -text -noout

# Verify nginx can read certificates
docker exec privatebin_nginx ls -la /etc/nginx/ssl/

Issue: File uploads failing

  1. Check sizelimit in conf.php
  2. Verify client_max_body_size in nginx config
  3. Check disk space:
    df -h
  4. Review logs:
    docker-compose logs privatebin | grep -i upload

Issue: Pastes not expiring

  1. Verify purge is running:
    docker exec privatebin php /srv/bin/purge
  2. Check purge configuration in conf.php
  3. Set up cron job for automatic purging

Debugging

Enable debug mode in conf.php:

[main]
; Enable error display (disable in production)
debug = true

View detailed logs:

docker-compose logs -f privatebin

Integration and Automation

Browser Bookmarklet

Create a bookmarklet for quick paste creation:

javascript:(function(){var%20url='https://paste.yourdomain.com/?';var%20text=window.getSelection().toString()||prompt('Enter text to paste:');if(text){window.open(url+'text='+encodeURIComponent(text))}})();

Save as bookmark, click to paste selected text.

Command-Line Integration

Create a shell function for CLI usage:

# Add to ~/.bashrc or ~/.zshrc
paste() {
    if [ -z "$1" ]; then
        echo "Usage: paste filename-or-text"
        return 1
    fi

    if [ -f "$1" ]; then
        curl -X POST https://paste.yourdomain.com/api/             -F "file=@$1"             -F "expire=1week"
    else
        echo "$1" | curl -X POST https://paste.yourdomain.com/api/             -F "text=<-"             -F "expire=1week"
    fi
}

Usage:

# Paste file
paste myfile.txt

# Paste text
paste "This is my secret text"

# Paste command output
cat config.yaml | paste -

API Usage

PrivateBin has a simple API:

Create paste via API:

# Text paste
curl -X POST https://paste.yourdomain.com/api/     -H "Content-Type: application/json"     -d '{
        "paste": "My secret text",
        "expire": "1day",
        "burnafterreading": 0
    }'

# File upload
curl -X POST https://paste.yourdomain.com/api/     -F "[email protected]"     -F "expire=1week"

Response:

{
    "status": 0,
    "id": "abc123",
    "url": "https://paste.yourdomain.com/?abc123",
    "deletetoken": "def456"
}

Conclusion

You now have a fully functional self-hosted PrivateBin installation with:

  • Client-side end-to-end encryption for maximum privacy
  • Zero-knowledge architecture where server cannot read content
  • Support for text pastes and file attachments
  • Burn-after-reading for self-destructing messages
  • Configurable expiration times
  • Password protection for extra security
  • Discussion threads for collaboration
  • Syntax highlighting for code
  • HTTPS with automatic certificate management

PrivateBin provides a secure, private alternative to public pastebin services while maintaining complete control over your data.

Next Steps

Consider these enhancements:

  • Set up high availability with load balancing
  • Implement Redis cluster for distributed storage
  • Configure S3 backend for unlimited storage
  • Add custom branding and themes
  • Implement rate limiting with fail2ban
  • Set up monitoring with Prometheus and Grafana
  • Create custom API integrations
  • Implement backup automation to cloud storage

For support and updates, visit: