Pluck runs as one Docker image (the web app, and the same image again as the worker), MongoDB, and an S3-compatible blob store. docker-compose.yml at the root of the repo brings all of it up.
git clone https://github.com/codu-code/pluck && cd pluck
docker compose up -dOpen http://localhost:3000 and sign in. Sign-in is by email link; until you set up real mail, the links land in the bundled Mailpit at http://localhost:8025.
That is all it takes on one machine. The first start builds the image (a few minutes), makes the blob bucket, starts a single-node MongoDB replica set, and generates a secret for sessions, which it keeps in a volume.
Make an .env next to docker-compose.yml (Compose reads it), put a reverse proxy with HTTPS in front, and set:
| Variable | What | Default |
|---|---|---|
PLUCK_URL |
The public address, no trailing slash. Becomes the sign-in link host, the OAuth issuer and the connector URL (<PLUCK_URL>/mcp) |
http://localhost:3000 |
PLUCK_PORT |
Host port for the web app, on 127.0.0.1 only, for your proxy | 3000 |
MINIO_PORT, MAILPIT_PORT |
Host ports for MinIO and Mailpit's inbox, on 127.0.0.1 only | 9000, 8025 |
BETTER_AUTH_SECRET |
32+ random bytes. Leave unset to have one generated and kept in the secrets volume |
generated |
DIGEST_SECRET |
Optional: a separate key for the Monday email's one-tap links, so you can revoke them without signing everyone out | BETTER_AUTH_SECRET |
SMTP_URL |
Your mail relay, e.g. smtps://user:pass@smtp.example.com:465 |
the bundled Mailpit |
EMAIL_FROM |
The From line for sign-in links and the Monday email | Pluck <pluck@localhost> |
DIGEST_FROM, EMAIL_REPLY_TO |
Optional: a separate From line for the Monday email (a subdomain keeps spam reports on it away from sign-in links), and where replies go if your From address takes no mail | EMAIL_FROM, none |
S3_PUBLIC_ENDPOINT |
Where browsers and phones reach the blob store (uploads, covers, exports). Publish MinIO behind your proxy and put its public address here | http://localhost:9000 |
S3_ACCESS_KEY, S3_SECRET_KEY |
MinIO's credentials, used by the app too. Set the secret to a long random value (openssl rand -hex 32): the default is public, so Pluck will not start with it unless PLUCK_URL and S3_PUBLIC_ENDPOINT are both localhost |
pluck, pluck-change-me |
S3_ENDPOINT, S3_BUCKET, AWS_REGION |
To use real S3 (or R2, B2, …) instead of the bundled MinIO | MinIO |
ADMIN_EMAILS |
Who can open /admin (reports, take-downs), comma separated |
none |
ABUSE_EMAIL |
Where reports of public lists are emailed | none |
APPLE_TEAM_ID |
Only if you build the iPhone app for your own server: your Apple team ID, so links to your server open the app (universal links). The app needs applinks:<your host> too |
off |
NEXT_PUBLIC_POSTHOG_KEY, POSTHOG_HOST |
Your own PostHog project, if you want analytics and error tracking. The key is read when the image is built (docker compose build with it in the environment); unset, there is no cookie banner and nothing is sent anywhere. The host defaults to PostHog's EU cloud |
off |
WELCOME_FROM |
A From line for a welcome note sent 27 minutes after each sign-up, signed by the person who runs getpluck.io. Leave it unset on your own server | off |
REVIEW_EMAIL, REVIEW_PASSWORD |
Only if you submit your own build of the iPhone app to App Review: the one address that signs in with a password instead of an email link or code (it gets no email). Leave both unset | off |
EMAIL_IN_DOMAIN |
For email-in: the domain whose mail lands in the bucket under inbound/ (needs an inbound mail service that writes raw messages there, like SES receipt rules) |
off |
Never set ALLOW_PRIVATE_FETCH or AUTH_RATE_LIMIT on a server: they exist for local development and tests only. Without them Pluck refuses to fetch private addresses and rate-limits sign-in.
Rate limits key on the caller's address, which Pluck reads from X-Forwarded-For. Your proxy must set that header to the one address it saw, not pass on whatever the client sent. Caddy's reverse_proxy does this by default. In nginx, use proxy_set_header X-Forwarded-For $remote_addr;, not $proxy_add_x_forwarded_for.
Update with git pull && docker compose up -d --build. Back up the mongo and minio volumes.
- Browser extension: the store extension only reaches getpluck.io (one host permission keeps its review short), so build one for your server:
PLUCK_SERVER=<PLUCK_URL> pnpm --filter @pluck/extension build, thenchrome://extensions, Developer mode, Load unpacked,apps/extension/.output/chrome-mv3. It signs in through your server, or takes an API key (Settings, API keys). - iPhone: open
pluck://signin?server=<PLUCK_URL>on the phone (from a note or your server's Settings) and the sign-in screen opens on "Use your own server" with your address filled in. Store builds hide that choice otherwise; a build withPLUCK_SHOW_SERVER=1always shows it. To build and sign the app yourself, change our identifiers to your own first, since Apple lets one team hold each: the bundle IDio.getpluck.app(and.share), the App Groupgroup.io.getpluck.appand the Keychain groupio.getpluck.shared, inapps/ios/app.config.ts,apps/ios/targets/share/PluckClient.swift,apps/ios/lib/data.tsandapps/web/app/.well-known/apple-app-site-association/route.ts. - AI tools: the connector is
<PLUCK_URL>/mcp, with OAuth like the hosted service. For a tool that only speaks stdio, or a server without public HTTPS:npx @getpluck/mcp --server <PLUCK_URL> --key pk_live_…. Seedocs/mcp.md.
web:next starton port 3000. Health at/api/health.worker: the job queue (reading pages, imports, exports, shared files, email-in, welcome notes and the Monday email, together at 8 a second) and the schedule (the nightly move to the Archive, the Monday email, emptying the bin). On Vercel a cron tick does the same; here it is a loop.mongo: MongoDB 8.2, a single-node replica set.minioandminio-init: the blob store and a one-off that makes the bucket.mailpit: catches mail untilSMTP_URLpoints elsewhere. Remove it once you do.
Minimum: 1 vCPU and 1 GB of RAM for all of it.