Guide · 12 min read

Setting up a self-hosted clipboard sync server.

The case for self-hosting a clipboard is straightforward: clipboards contain sensitive stuff. Passwords. Tokens. Emails. Running it on infrastructure you control means no third party has your clips — ever. Here's the full walkthrough.

Why self-host a clipboard?

Clipboards contain everything you copy, briefly, including things you never intended to share with a third party. A password from 1Password you're pasting into a terminal. An API key from Stripe. The contents of an internal doc. A credit card number.

Using a cloud clipboard service means trusting that company's security, data-handling practices, and future business decisions with all of that. For most people, that's fine — companies like MoveMyWork don't read your clips and have no incentive to. But if you're a journalist, security researcher, or just someone who takes privacy seriously, self-hosting removes the need to trust anyone.

The good news: MoveMyWork is designed to self-host from day one. No Docker complexity. No Kubernetes. Just PHP and MySQL on any Linux box with a public IP.

What you need

Total setup time is about 30 minutes if you're familiar with Linux, an hour if you're not.

1. Install the LAMP/LEMP basics

On a fresh Ubuntu 24.04 server:

sudo apt update
sudo apt install -y nginx mysql-server \
    php8.3-fpm php8.3-mysql php8.3-curl \
    php8.3-mbstring php8.3-xml certbot python3-certbot-nginx

Start MySQL and create a database:

sudo mysql_secure_installation
sudo mysql <<'EOF'
CREATE DATABASE movemywork CHARACTER SET utf8mb4;
CREATE USER 'movemywork'@'localhost'
    IDENTIFIED BY 'pick-a-strong-password';
GRANT SELECT, INSERT, UPDATE, DELETE
    ON movemywork.* TO 'movemywork'@'localhost';
FLUSH PRIVILEGES;
EOF

2. Get the code

Grab the latest release from the downloads page or clone it:

sudo mkdir -p /var/www/movemywork
sudo chown "$USER":www-data /var/www/movemywork
cd /var/www/movemywork
# extract the zip here — you should end up with bin/, src/, public/, etc.

3. Run the migrations

cd /var/www/movemywork
mysql -u movemywork -p movemywork < database/schema.sql
mysql -u movemywork -p movemywork < database/migrations/001_registration.sql
mysql -u movemywork -p movemywork < database/migrations/002_shares.sql
mysql -u movemywork -p movemywork < database/migrations/003_waitlist.sql

4. Configure

cp config/config.example.php config/config.php
cp config/secrets.example.php config/secrets.php
chmod 600 config/secrets.php
php bin/gen_secret.php   # copy output into secrets.php as app_secret

Edit config/config.php to set your app_url. Edit config/secrets.php to fill in DB credentials (if you use the secrets.php db block rather than config.php) and your Mailjet API keys.

5. Set up Mailjet

Mailjet handles 2FA and password-reset emails. Free tier is enough for personal use (200 emails/day).

  1. Sign up at mailjet.com.
  2. Add your sending domain. Mailjet gives you SPF and DKIM records; add them to your domain's DNS.
  3. Wait 15–60 minutes for DNS to propagate, then re-check in Mailjet. Both records should go green.
  4. Add a sending address (e.g. support@yourdomain.com) and click the verification link Mailjet emails.
  5. Go to Account Settings → REST API → copy the API Key and Secret Key into config/secrets.php.

Test delivery:

php bin/send_test_email.php you@yourdomain.com

6. Nginx configuration

# /etc/nginx/sites-available/movemywork
server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;
    root /var/www/movemywork/public;
    index index.php;

    ssl_certificate     /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    client_max_body_size 120M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Short share URLs
    location ~ ^/s/([a-f0-9]{32})/?$ {
        try_files $uri /s.php?t=$1;
    }

    location = /sitemap.xml { try_files $uri /sitemap.xml.php; }
}

Enable it and get TLS:

sudo ln -s /etc/nginx/sites-available/movemywork /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d your-domain.com

7. Storage directory

mkdir -p /var/www/movemywork/storage/files
sudo chown -R www-data:www-data /var/www/movemywork/storage
chmod 750 /var/www/movemywork/storage /var/www/movemywork/storage/files

8. Create the first admin user

For your very first account, skip the signup flow — use the CLI tool instead, which bypasses email verification:

php bin/create_user.php you@yourdomain.com

It prompts for a password, creates the account pre-verified, and prints an API token. Save the token — the desktop client needs it.

9. Set up the periodic prune

# crontab -e
*/10 * * * * php /var/www/movemywork/bin/prune.php >/dev/null 2>&1

This cleans up expired 2FA tokens, old rate-limit buckets, stale share rows, and prunes clips past the history limit.

10. Test end-to-end

  1. Visit https://your-domain.com in a browser. You should see the marketing homepage.
  2. Click Sign in, log in with the credentials you just created.
  3. You'll land on the dashboard. Paste something, click Copy, and it appears at the top.
  4. Click Settings, grab the API token, install the desktop client on your laptop, paste the token in.
  5. Copy something on the laptop. Check it appears on the web dashboard.
At this point you have your own private clipboard sync service. No third party touches your data. Your hardware, your rules.

Going further

Desktop clients for other users in your household

Each user you want to give access to needs their own account. Use bin/create_user.php again, or just have them sign up through the public signup page — they'll receive a normal verification email via your Mailjet.

Backing up

Nightly cron:

0 3 * * * mysqldump -u movemywork -pPASS movemywork \
  | gzip > /var/backups/mmw-$(date +\%F).sql.gz
0 3 * * * tar czf /var/backups/mmw-files-$(date +\%F).tgz \
  /var/www/movemywork/storage

Rotate old backups with your preferred method — rsnapshot, restic, or just find -mtime +30 -delete.

Behind a VPN only?

If you want MoveMyWork accessible only when you're on your Tailscale/WireGuard network, bind nginx to the VPN interface only and skip the Let's Encrypt step. The desktop client only needs to reach the server — it doesn't care whether that's via public internet or a private network.

Security hardening

FAQ

Can I run this behind Cloudflare?

Yes. Turn off Cloudflare's email obfuscation for your login paths (it breaks verification codes). Everything else works.

How much RAM does it actually use?

Idle: ~30 MB (PHP-FPM pool size dependent). Under load for a single user, negligible. A 1 GB VPS handles a household without breaking a sweat.

What about Docker?

No official Docker image. Could one exist? Sure, and community members have built them. But the stack is simple enough that running it directly is less work than maintaining a container.

Will you keep supporting the self-hosted version?

Yes — it's the project's foundation. Every feature lands in self-host first. Our own hosted service runs the same codebase.


Or just use our hosted version