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 pageOptionsDocker ComposeFree: one server and a Cloudflare TunnelCloudflare ContainersRequired settingsCommon optional settingsSign-in with ClerkConnecting networksProduction checklistHealth checks and logs

Options

OptionWhat runs whereCost
Docker ComposeMongoDB, the app, a publishing worker and Caddy (HTTPS) on one serverYour server
Free: one server + Cloudflare TunnelThe Docker stack on a free server, with Cloudflare handling your domain and HTTPS$0 a month
Cloudflare ContainersOne container behind a Worker, media in R2, database on MongoDB AtlasAbout $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:

  1. Install Docker on the server and clone the repository.

  2. 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 hostname app.yourdomain.com and service HTTP → app:4000.

  3. Copy .env.example to .env and 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 MB
    
  4. Start it:

    docker compose -f docker-compose.yml -f docker-compose.tunnel.yml up -d --build
    
  5. Back 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.

VariableWhat it does
APP_URLThe public address people open. OAuth callbacks and media URLs are built from it. Use https in production
ENCRYPTION_KEYEncrypts channel tokens. Required in production. Never change it after launch
MONGODB_URIThe database. Docker Compose sets it for you
DOMAINYour domain, for Caddy's automatic HTTPS (Docker Compose only)
TUNNEL_TOKENThe Cloudflare Tunnel token (free setup only)

Common optional settings

VariableWhat it does
RESEND_API_KEY, MAIL_FROMSend email through Resend. Verify your sending domain and use an address on it
SMTP_URLSend email through any SMTP server instead, for example smtps://user:password@smtp.example.com:465
REQUIRE_EMAIL_VERIFICATIONRequire a confirmed email before connecting channels or scheduling posts
ALLOW_SIGNUPSet to false to allow sign-up by invite only
SUPERADMIN_EMAILSEmails that can open the admin page
ANTHROPIC_API_KEYTurns on AI writing and the assistant
OPENAI_API_KEYTurns on AI image generation
UNSPLASH_ACCESS_KEY, PEXELS_API_KEYTurn on free stock photo search in the composer
SUPPORT_EMAILWhere 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_DAYSThe 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_BASEA short domain for tracked links
METRICS_INTERVAL_HOURSHow often analytics refresh (default 6)
FEED_INTERVAL_MINUTESHow often RSS feeds are checked (default 15, minimum 5)
PLUG_INTERVAL_MINUTESHow often plugs check likes (default 30, minimum 10)
DODO_PAYMENTS_*, DODO_PRODUCT_*, TRIAL_DAYSTurn on billing. Leave DODO_PAYMENTS_API_KEY empty for no limits
SENTRY_DSNError 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=false still blocks sign-ups without an invite. Restrict sign-ups in Clerk too.
  • Optional: point a Clerk webhook at {APP_URL}/api/clerk/webhook for user.updated and user.deleted, and set CLERK_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_URL is your public https URL
  • ENCRYPTION_KEY is 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_DSN is set if you want error reports
  • Every network you offer has been tested with a real account

Health checks and logs

  • GET /api/health answers when the app is running.
  • GET /api/ready answers when the app is connected to the database.
  • With Docker Compose, read logs with docker compose logs -f app worker.