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
- A Linux VPS or home server — 1 GB RAM is fine, 2 GB comfortable. Tested on Ubuntu 22.04 and 24.04, Debian 12, and Rocky Linux 9.
- A domain name (or subdomain) you control, pointed at the server.
- PHP 8.1 or newer with
pdo_mysql,curl,fileinfo, andsessionextensions. - MySQL 8+ or MariaDB 10.6+.
- Nginx or Apache with TLS. Let's Encrypt is fine.
- A Mailjet account (free tier — 200 emails/day) for 2FA codes.
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).
- Sign up at mailjet.com.
- Add your sending domain. Mailjet gives you SPF and DKIM records; add them to your domain's DNS.
- Wait 15–60 minutes for DNS to propagate, then re-check in Mailjet. Both records should go green.
- Add a sending address (e.g.
support@yourdomain.com) and click the verification link Mailjet emails. - 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
- Visit
https://your-domain.comin a browser. You should see the marketing homepage. - Click Sign in, log in with the credentials you just created.
- You'll land on the dashboard. Paste something, click Copy, and it appears at the top.
- Click Settings, grab the API token, install the desktop client on your laptop, paste the token in.
- 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
- Fail2ban on SSH and the app's login endpoint to block brute-force attackers.
- Disable signup if it's a personal instance:
'registration_open' => falsein config. - Database user with minimum privileges — no CREATE, DROP, ALTER rights.
- File permissions — 600 for secrets.php, 640 for config.php, 750 for storage/.
- Keep PHP up to date — apt autoupdate is fine for minor versions.
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.