Self-hosting
Run Postline on your own server with Docker Compose, for free behind a Cloudflare Tunnel, or on Cloudflare Containers. Required settings and a checklist.
On this page
OptionsDocker ComposeFree: one server and a Cloudflare TunnelCloudflare ContainersRequired settingsCommon optional settingsSign-in with ClerkConnecting networksProduction checklistHealth checks and logsOptions
| Option | What runs where | Cost |
|---|---|---|
| Docker Compose | MongoDB, the app, a publishing worker and Caddy (HTTPS) on one server | Your server |
| Free: one server + Cloudflare Tunnel | The Docker stack on a free server, with Cloudflare handling your domain and HTTPS | $0 a month |
| Cloudflare Containers | One container behind a Worker, media in R2, database on MongoDB Atlas | About $12–13 a month |
Step-by-step guides are on GitHub: deploy guides.
A self-hosted server without Dodo Payments keys has no billing and no limits. Every feature is unlocked.
Docker Compose
git clone https://github.com/shekhar9837/postline.git && cd postline
cp .env.example .env
# Set DOMAIN, APP_URL (https://DOMAIN), ENCRYPTION_KEY, and keys for the networks you want.
docker compose up -d --build
This starts MongoDB, the app (web and API), a separate publishing worker, and Caddy, which gets an HTTPS certificate for DOMAIN automatically.
Without a domain, for example behind your own proxy, skip Caddy:
docker compose up -d --build mongo app worker
Free: one server and a Cloudflare Tunnel
The whole stack runs on one free server. Cloudflare's free plan handles your domain and HTTPS. You don't open any ports on the server. Nothing goes to sleep, so scheduled posts go out on time.
You need:
- A domain on Cloudflare's free plan.
- A free server with about 2 GB of memory or more, such as Oracle Cloud Always Free. A 1 GB server (such as Google Cloud's free e2-micro) works if you use MongoDB Atlas's free tier instead of running MongoDB on the server.
- Ubuntu 24.04.
Steps:
Install Docker on the server and clone the repository.
In the Cloudflare dashboard, go to Networking → Tunnels → Create a tunnel, name it
postline, and choose Docker. Copy only the token. Add a published application with hostnameapp.yourdomain.comand serviceHTTP→app:4000.Copy
.env.exampleto.envand set at least:APP_URL=https://app.yourdomain.com ENCRYPTION_KEY= # node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" TUNNEL_TOKEN= # from step 2 ENABLE_SANDBOX_PROVIDER=false MAX_VIDEO_MB=95 # Cloudflare's free plan caps uploads at 100 MBStart it:
docker compose -f docker-compose.yml -f docker-compose.tunnel.yml up -d --buildBack up MongoDB every night. The guide has a ready-made cron line. Also keep a copy of your
.env.
Full guide: deploy/free.
Cloudflare Containers
Postline runs as one container behind a Worker. Uploads go to R2 and the database is MongoDB Atlas. A cron wakes the container every minute so scheduled posts aren't delayed.
You need a Cloudflare account on the Workers Paid plan, your domain on Cloudflare, and a MongoDB Atlas cluster. You set APP_URL in wrangler.jsonc, load secrets with npx wrangler secret bulk, and deploy with npx wrangler deploy or a GitHub Action.
Uploads through a Worker are capped at 100 MB, so MAX_VIDEO_MB is set to 95.
Full guide: deploy/cloudflare.
Required settings
Every setting is described in .env.example.
| Variable | What it does |
|---|---|
APP_URL | The public address people open. OAuth callbacks and media URLs are built from it. Use https in production |
ENCRYPTION_KEY | Encrypts channel tokens. Required in production. Never change it after launch |
MONGODB_URI | The database. Docker Compose sets it for you |
DOMAIN | Your domain, for Caddy's automatic HTTPS (Docker Compose only) |
TUNNEL_TOKEN | The Cloudflare Tunnel token (free setup only) |
Common optional settings
| Variable | What it does |
|---|---|
RESEND_API_KEY, MAIL_FROM | Send email through Resend. Verify your sending domain and use an address on it |
SMTP_URL | Send email through any SMTP server instead, for example smtps://user:password@smtp.example.com:465 |
REQUIRE_EMAIL_VERIFICATION | Require a confirmed email before connecting channels or scheduling posts |
ALLOW_SIGNUP | Set to false to allow sign-up by invite only |
SUPERADMIN_EMAILS | Emails that can open the admin page |
ANTHROPIC_API_KEY | Turns on AI writing and the assistant |
OPENAI_API_KEY | Turns on AI image generation |
UNSPLASH_ACCESS_KEY, PEXELS_API_KEY | Turn on free stock photo search in the composer |
SUPPORT_EMAIL | Where messages from the in-app support chat are emailed (default: SUPERADMIN_EMAILS). Answer them on the admin page |
AFFILIATE_PROGRAM, AFFILIATE_RATE, AFFILIATE_MONTHS, AFFILIATE_HOLD_DAYS | The affiliate program (needs billing). Off until AFFILIATE_PROGRAM=true; then 30% of each payment, for 12 months, payable after 30 days, unless you change them |
S3_* | Store media in S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, MinIO) instead of local disk |
SHORT_LINK_BASE | A short domain for tracked links |
METRICS_INTERVAL_HOURS | How often analytics refresh (default 6) |
FEED_INTERVAL_MINUTES | How often RSS feeds are checked (default 15, minimum 5) |
PLUG_INTERVAL_MINUTES | How often plugs check likes (default 30, minimum 10) |
DODO_PAYMENTS_*, DODO_PRODUCT_*, TRIAL_DAYS | Turn on billing. Leave DODO_PAYMENTS_API_KEY empty for no limits |
SENTRY_DSN | Error reports |
Without email set up, invite links are shown in the app so you can share them yourself.
Sign-in with Clerk
By default, Postline has its own sign-in: email and password, Google, GitHub and two-factor. To use Clerk instead, set CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY. Clerk then handles sign-up, sign-in, social logins, two-factor, email verification and password resets, and Postline's own password sign-in turns off.
- People with an existing Postline account are linked the first time they sign in with the same verified email.
- New people get their own workspace, unless an invite is waiting for their email.
ALLOW_SIGNUP=falsestill blocks sign-ups without an invite. Restrict sign-ups in Clerk too.- Optional: point a Clerk webhook at
{APP_URL}/api/clerk/webhookforuser.updatedanduser.deleted, and setCLERK_WEBHOOK_SIGNING_SECRET.
Connecting networks
Some networks need no server setup: people connect them with their own credentials (for example Bluesky, Mastodon, Discord, Telegram and Slack). Others need an app on the network and keys in .env.
Register this redirect URL with every network that uses sign-in:
{APP_URL}/api/channels/oauth/{network}/callback
Media must be reachable from the internet at APP_URL/uploads/... or your S3 public URL, because Instagram, Threads, TikTok and others download it from there.
Production checklist
-
APP_URLis your public https URL -
ENCRYPTION_KEYis a long random value and is backed up. Changing it disconnects every channel -
ENABLE_SANDBOX_PROVIDER=false - MongoDB has authentication and backups
- Media storage is persistent (the uploads volume or S3)
- Email is set up so verification, invites and notifications are sent. Consider
REQUIRE_EMAIL_VERIFICATION=true - The
LEGAL_*values are filled in, and the terms and privacy pages are reviewed by a lawyer - If you charge customers, Dodo Payments is set up and a test purchase, plan switch and cancellation work before you set
DODO_PAYMENTS_ENVIRONMENT=live_mode -
SENTRY_DSNis set if you want error reports - Every network you offer has been tested with a real account
Health checks and logs
GET /api/healthanswers when the app is running.GET /api/readyanswers when the app is connected to the database.- With Docker Compose, read logs with
docker compose logs -f app worker.