<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aryan Iyappan</title>
    <description>The latest articles on DEV Community by Aryan Iyappan (@aryaniyaps).</description>
    <link>https://dev.to/aryaniyaps</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F664689%2Ffe45d2e0-7876-4c54-8e03-3b77a771fb58.png</url>
      <title>DEV Community: Aryan Iyappan</title>
      <link>https://dev.to/aryaniyaps</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aryaniyaps"/>
    <language>en</language>
    <item>
      <title>How I Self-Hosted Postiz with Tailscale (and the Env Vars the Docs Don't Tell You About)</title>
      <dc:creator>Aryan Iyappan</dc:creator>
      <pubDate>Wed, 03 Jun 2026 08:22:47 +0000</pubDate>
      <link>https://dev.to/aryaniyaps/how-i-self-hosted-postiz-with-tailscale-and-the-env-vars-the-docs-dont-tell-you-about-4i8g</link>
      <guid>https://dev.to/aryaniyaps/how-i-self-hosted-postiz-with-tailscale-and-the-env-vars-the-docs-dont-tell-you-about-4i8g</guid>
      <description>&lt;h1&gt;
  
  
  How I Self-Hosted Postiz with Tailscale (and the Env Vars the Docs Don't Tell You About)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A step-by-step guide to running your own social media scheduler behind Tailscale — with the gotchas that cost me an afternoon.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;I wanted a social media scheduler that I control. Not another SaaS subscription. Not another dashboard I log into that owns my data. Postiz is the best open-source option out there — 28+ platform integrations, a CLI, and a clean UI — but self-hosting it has a learning curve.&lt;/p&gt;

&lt;p&gt;The docs will get you 80% of the way there. The other 20% — the env vars nobody mentions, the Tailscale networking trick, the Temporal stack — is what this post covers.&lt;/p&gt;

&lt;p&gt;By the end, you'll have Postiz running on your server, accessible from anywhere via Tailscale's MagicDNS, with automatic HTTPS. No port forwarding. No Cloudflare Tunnels. No domain registrar.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────┐
│                Your Server                     │
│                                                │
│  ┌──────────┐    ┌─────────────────────────┐  │
│  │ Tailscale │◄───│ network_mode: service    │  │
│  │ Container │    │ (Postiz rides Tailscale's │  │
│  │           │    │  network stack)           │  │
│  │ :443→5000 │    └─────────────────────────┘  │
│  └──────────┘              │                   │
│       │                    ▼                   │
│       │           ┌─────────────────┐          │
│       │           │   Postiz App    │          │
│       │           │   (port 5000)   │          │
│       │           └────────┬────────┘          │
│       │                    │                   │
│       ▼                    ▼                   │
│  MagicDNS HTTPS    ┌──────────────┐            │
│  postiz.tailXXXX   │  PostgreSQL  │            │
│  .ts.net           │  Redis       │            │
│                    │  Temporal    │            │
│                    └──────────────┘            │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: Postiz doesn't expose any ports to the host. Instead, it uses &lt;code&gt;network_mode: service:tailscale&lt;/code&gt; to piggyback on Tailscale's network stack. Tailscale handles DNS, HTTPS certificates, and the encrypted tunnel. Postiz just thinks it's serving HTTP on localhost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Linux server (Ubuntu 24.04 recommended) with at least &lt;strong&gt;2GB RAM&lt;/strong&gt; and &lt;strong&gt;2 vCPUs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Docker and Docker Compose installed&lt;/li&gt;
&lt;li&gt;A [Tailscale](undefined account (free tier is fine)&lt;/li&gt;
&lt;li&gt;Basic comfort with editing YAML files&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Get Your Tailscale Auth Key
&lt;/h2&gt;

&lt;p&gt;This is the key that lets your container join your Tailnet without manual authentication.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to [Tailscale Admin Console → Settings → Keys](undefined&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate auth key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set it to &lt;strong&gt;Reusable&lt;/strong&gt; and &lt;strong&gt;Ephemeral: Off&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add a tag like &lt;code&gt;tag:containers&lt;/code&gt; (you'll need this in your ACL later)&lt;/li&gt;
&lt;li&gt;Copy the key — it starts with &lt;code&gt;tskey-auth-&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The free tier gives you 3 users and 100 devices. One container = one device, so you've got plenty of room.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Clone the Postiz Docker Compose Repo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone undefined
&lt;span class="nb"&gt;cd &lt;/span&gt;postiz-docker-compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the base &lt;code&gt;docker-compose.yaml&lt;/code&gt; plus the Temporal dynamic config files. Don't run it yet — we're going to modify it significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: The Tailscale Service (This Is the Secret Sauce)
&lt;/h2&gt;

&lt;p&gt;Add this service to your &lt;code&gt;docker-compose.yaml&lt;/code&gt;. It sits &lt;strong&gt;above&lt;/strong&gt; Postiz in the network stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tailscale/tailscale:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz-tailscale&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxxxxxx&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TS_EXTRA_ARGS=--advertise-tags=tag:containers&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TS_SERVE_CONFIG=/config/ts.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TS_STATE_DIR=/var/lib/tailscale&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./tailscale-state:/var/lib/tailscale&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./tailscale-config:/config&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/dev/net/tun:/dev/net/tun&lt;/span&gt;
    &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_ADMIN&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_RAW&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tailscale&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--peers=false&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--json&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;grep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'Online.*true'"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postiz-network&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;temporal-network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TS_AUTHKEY&lt;/code&gt;&lt;/strong&gt; authenticates the container to your Tailnet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TS_EXTRA_ARGS&lt;/code&gt;&lt;/strong&gt; advertises the &lt;code&gt;tag:containers&lt;/code&gt; ACL tag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TS_SERVE_CONFIG&lt;/code&gt;&lt;/strong&gt; points to a JSON file that tells Tailscale how to route traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/dev/net/tun&lt;/code&gt;&lt;/strong&gt; is the kernel tunnel device — required for Tailscale to create its virtual network interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NET_ADMIN&lt;/code&gt;&lt;/strong&gt; and &lt;code&gt;NET_RAW&lt;/code&gt;** capabilities let Tailscale manipulate the network stack&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;healthcheck&lt;/strong&gt; waits for Tailscale to report &lt;code&gt;Online: true&lt;/code&gt; before Postiz starts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Tailscale Serve Config
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;tailscale-config/ts.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TCP"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"443"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"HTTPS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Web"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postiz.taila7d0df.ts.net:443"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Handlers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Proxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"undefined"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Tailscale:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Listen on port 443 (HTTPS)&lt;/li&gt;
&lt;li&gt;When a request comes in for &lt;code&gt;postiz.tailXXXX.ts.net&lt;/code&gt;, proxy it to &lt;code&gt;undefined&lt;/code&gt; (where Postiz listens)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Tailscale automatically provisions a Let's Encrypt certificate&lt;/strong&gt; for &lt;code&gt;*.ts.net&lt;/code&gt; domains. You get HTTPS with zero configuration.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Replace &lt;code&gt;postiz.taila7d0df.ts.net&lt;/code&gt; with your actual Tailscale MagicDNS name. You can find it in the Tailscale admin console under the machine's details, or run &lt;code&gt;tailscale status&lt;/code&gt; once the container is up.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: The Postiz Service — Env Vars That Actually Matter
&lt;/h2&gt;

&lt;p&gt;Here's the Postiz service, modified to work with Tailscale. This is where the docs fall short.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;postiz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/gitroomhq/postiz-app:v2.21.8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service:tailscale&lt;/span&gt;   &lt;span class="c1"&gt;# ← THIS IS THE KEY LINE&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# === URLs — MUST match your Tailscale MagicDNS name&lt;/span&gt;
      &lt;span class="na"&gt;MAIN_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;undefined'&lt;/span&gt;
      &lt;span class="na"&gt;FRONTEND_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;undefined'&lt;/span&gt;
      &lt;span class="na"&gt;NEXT_PUBLIC_BACKEND_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;undefined'&lt;/span&gt;

      &lt;span class="c1"&gt;# === Database &amp;amp; Redis&lt;/span&gt;
      &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-random-secret-string-here'&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postgresql://postiz-user:postiz-password@postiz-postgres:5432/postiz-db-local'&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis://postiz-redis:6379'&lt;/span&gt;

      &lt;span class="c1"&gt;# === Internal Communication (CRITICAL!)&lt;/span&gt;
      &lt;span class="na"&gt;BACKEND_INTERNAL_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000'&lt;/span&gt;
      &lt;span class="na"&gt;TEMPORAL_ADDRESS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temporal:7233'&lt;/span&gt;

      &lt;span class="c1"&gt;# === Feature Flags&lt;/span&gt;
      &lt;span class="na"&gt;IS_GENERAL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
      &lt;span class="na"&gt;DISABLE_REGISTRATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
      &lt;span class="na"&gt;RUN_CRON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
      &lt;span class="na"&gt;NOT_SECURED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;

      &lt;span class="c1"&gt;# === Storage (Cloudflare R2)&lt;/span&gt;
      &lt;span class="na"&gt;STORAGE_PROVIDER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cloudflare'&lt;/span&gt;
      &lt;span class="na"&gt;CLOUDFLARE_ACCOUNT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-account-id'&lt;/span&gt;
      &lt;span class="na"&gt;CLOUDFLARE_ACCESS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-access-key'&lt;/span&gt;
      &lt;span class="na"&gt;CLOUDFLARE_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-secret-key'&lt;/span&gt;
      &lt;span class="na"&gt;CLOUDFLARE_BUCKETNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-bucket-name'&lt;/span&gt;
      &lt;span class="na"&gt;CLOUDFLARE_BUCKET_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;undefined'&lt;/span&gt;
      &lt;span class="na"&gt;CLOUDFLARE_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auto'&lt;/span&gt;

      &lt;span class="c1"&gt;# === Platform API Keys (fill in the platforms you use)&lt;/span&gt;
      &lt;span class="na"&gt;X_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;X_API_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;LINKEDIN_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;LINKEDIN_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="c1"&gt;# ... (add keys for platforms you want to use)&lt;/span&gt;

      &lt;span class="c1"&gt;# === Misc&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;API_LIMIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;

    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postiz-config:/config/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postiz-uploads:/uploads/&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tailscale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;postiz-postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;postiz-redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Env Vars That Cost Me an Afternoon
&lt;/h3&gt;

&lt;p&gt;Here's what the official docs don't emphasize enough:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. &lt;code&gt;BACKEND_INTERNAL_URL: 'http://localhost:3000'&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;This is the most important env var you've never heard of.&lt;/strong&gt; Postiz's server-side code makes API calls to itself. If this is wrong, OAuth callbacks fail silently, image uploads break, and you get mysterious 500 errors with no useful logs.&lt;/p&gt;

&lt;p&gt;Always set this to &lt;code&gt;http://localhost:3000&lt;/code&gt; — &lt;strong&gt;not&lt;/strong&gt; your public URL. The app listens on port 3000 internally. The &lt;code&gt;network_mode: service:tailscale&lt;/code&gt; trick means Tailscale's port 5000 (or 443) proxies to this internal port. But the &lt;em&gt;app itself&lt;/em&gt; needs to reach itself at &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. &lt;code&gt;NEXT_PUBLIC_BACKEND_URL&lt;/code&gt; &lt;strong&gt;MUST&lt;/strong&gt; end with &lt;code&gt;/api&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This is the URL your browser uses to talk to the backend. If you forget the &lt;code&gt;/api&lt;/code&gt; suffix, the frontend loads but every API call returns a 404. You'll stare at a blank dashboard wondering why nothing works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ NEXT_PUBLIC_BACKEND_URL: 'undefined'
❌ NEXT_PUBLIC_BACKEND_URL: 'undefined'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/api&lt;/code&gt; suffix is how the Next.js frontend routes API requests. Without it, the frontend sends requests to &lt;code&gt;/graphql&lt;/code&gt; instead of &lt;code&gt;/api/graphql&lt;/code&gt;, and nothing resolves.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. &lt;code&gt;MAIN_URL&lt;/code&gt; and &lt;code&gt;FRONTEND_URL&lt;/code&gt; must be identical
&lt;/h4&gt;

&lt;p&gt;In most web apps, these can be different. In Postiz, they interact with OAuth redirects, cookie domains, and CORS in ways that are not fully documented. Set them both to your full HTTPS URL and save yourself the debugging.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. &lt;code&gt;RUN_CRON: 'true'&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Without this, your scheduled posts will never publish. Postiz uses an internal cron system (backed by Temporal) to fire posts at their scheduled time. If this is &lt;code&gt;false&lt;/code&gt; or missing, posts sit in the queue forever.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. &lt;code&gt;NOT_SECURED: 'false'&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;When running behind a reverse proxy (Tailscale, in our case), this tells Postiz "trust the upstream HTTPS, don't enforce your own." If you set it to &lt;code&gt;true&lt;/code&gt;, you'll get redirect loops and mixed content warnings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: The Supporting Cast (PostgreSQL, Redis, Temporal)
&lt;/h2&gt;

&lt;p&gt;Postiz needs Temporal for scheduling. The minimum stack looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;postiz-postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz-postgres&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz-password&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz-user&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz-db-local&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres-volume:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postiz-network&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg_isready -U postiz-user -d postiz-db-local&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;postiz-redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7.2&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz-redis&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-cli ping&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postiz-redis-data:/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postiz-network&lt;/span&gt;

  &lt;span class="c1"&gt;# Temporal stack (required for scheduled publishing)&lt;/span&gt;
  &lt;span class="na"&gt;temporal-postgresql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal-postgresql&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;temporal-network&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;temporal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7233:7233'&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporalio/auto-setup:1.28.1&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;temporal-postgresql&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB=postgres12&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_PORT=5432&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=temporal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PWD=temporal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_SEEDS=temporal-postgresql&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;temporal-network&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./dynamicconfig:/etc/temporal/config/dynamicconfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Temporal?&lt;/strong&gt; Postiz uses Temporal for its background job engine. When you schedule a post for "tomorrow at 2 PM," a Temporal workflow sleeps until the right time, then fires the publishing pipeline. Without Temporal running, cron jobs work but scheduled publishing silently fails.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Resource tip:&lt;/strong&gt; Temporal + its PostgreSQL can consume 500MB-1GB RAM. If you're on a tight VPS, consider running Temporal on a separate machine or scaling down to 1GB RAM (set &lt;code&gt;ES_JAVA_OPTS=-Xms100m -Xmx100m&lt;/code&gt; if using Elasticsearch).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 6: Networks and Volumes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres-volume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;postiz-redis-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;postiz-config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;postiz-uploads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postiz-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;temporal-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal-network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two separate networks keep Postiz app data and Temporal data isolated. The Tailscale container bridges both networks so Postiz (which rides on Tailscale) can reach Temporal at &lt;code&gt;temporal:7233&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Bring It Up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Make the directories Tailscale needs&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tailscale-state tailscale-config

&lt;span class="c"&gt;# Create the Tailscale serve config (edit the hostname!)&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tailscale-config/ts.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "TCP": {
    "443": {
      "HTTPS": true
    }
  },
  "Web": {
    "postiz.YOUR-TAILNET.ts.net:443": {
      "Handlers": {
        "/": {
          "Proxy": "undefined"
        }
      }
    }
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Start everything&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait about 60-90 seconds for everything to initialize. Tailscale needs to authenticate, provision certificates, and report healthy. Postgres and Redis need to accept connections. Temporal needs to set up its schemas.&lt;/p&gt;

&lt;p&gt;Watch the logs to see what's happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Watch all services&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Just Postiz&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; postiz

&lt;span class="c"&gt;# Just Tailscale (see if it authenticated)&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; tailscale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Verifying Everything Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Check Tailscale connectivity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;postiz-tailscale tailscale status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the container listed as &lt;code&gt;online&lt;/code&gt; with your MagicDNS name.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Check Postiz health
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; undefined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should return &lt;code&gt;HTTP/2 200&lt;/code&gt;. If it hangs, Tailscale isn't online yet — check the logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Open it in your browser
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;code&gt;undefined&lt;/code&gt;. You should see the Postiz login page. Register your admin account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; After registering, immediately add your social platform integrations from the dashboard. The platform API keys you put in &lt;code&gt;docker-compose.yaml&lt;/code&gt; enable backend communication, but you still need to connect each platform through the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Gotcha List
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gotcha&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;/api&lt;/code&gt; in &lt;code&gt;NEXT_PUBLIC_BACKEND_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dashboard loads but no data, 404s in console&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;/api&lt;/code&gt; suffix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong &lt;code&gt;BACKEND_INTERNAL_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;OAuth callbacks fail, uploads silently break&lt;/td&gt;
&lt;td&gt;Set to &lt;code&gt;http://localhost:3000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;RUN_CRON&lt;/code&gt; not set&lt;/td&gt;
&lt;td&gt;Scheduled posts never publish&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;RUN_CRON: 'true'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MAIN_URL&lt;/code&gt; ≠ &lt;code&gt;FRONTEND_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;CORS errors, cookie issues&lt;/td&gt;
&lt;td&gt;Make them identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale healthcheck too aggressive&lt;/td&gt;
&lt;td&gt;Postiz starts before Tailscale is ready&lt;/td&gt;
&lt;td&gt;Increase &lt;code&gt;start_period&lt;/code&gt; to 60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port 5000 exposed directly&lt;/td&gt;
&lt;td&gt;Bypasses Tailscale entirely&lt;/td&gt;
&lt;td&gt;Comment out/remove &lt;code&gt;ports&lt;/code&gt; from Postiz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal not running&lt;/td&gt;
&lt;td&gt;Scheduling silently fails&lt;/td&gt;
&lt;td&gt;Ensure &lt;code&gt;TEMPORAL_ADDRESS&lt;/code&gt; points to the right container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Why Go Through All This?
&lt;/h2&gt;

&lt;p&gt;You could use Postiz Cloud. It's great. But here's why I self-host:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data ownership.&lt;/strong&gt; Every API token, every scheduled post, every analytics data point lives on &lt;em&gt;my&lt;/em&gt; server. Not someone else's.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No monthly SaaS fee.&lt;/strong&gt; The only costs are my VPS and API usage. Postiz itself is free.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tailscale means zero firewall configuration.&lt;/strong&gt; I don't expose port 443 to the internet. I don't configure nginx. Tailscale's WireGuard tunnel handles all of it. My server could sit behind a NAT with no public IP and this would still work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Platform API key security.&lt;/strong&gt; All API keys live in environment variables on my server. They never touch Postiz's cloud. Given that these keys have write access to my social accounts, this matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Learning.&lt;/strong&gt; Setting this up taught me more about Docker networking, Tailscale Serve, and Next.js deployment patterns than any tutorial could.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Now that Postiz is running, here's what I'm building on top of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hermes Agent integration:&lt;/strong&gt; An AI agent that drafts posts using my voice, uploads them via the Postiz CLI, and schedules them. Zero manual posting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics pipeline:&lt;/strong&gt; Postiz's built-in analytics fed into a dashboard that tracks what's working across platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-user setup:&lt;/strong&gt; Adding collaborators through Postiz's team features, all behind the same Tailscale barrier.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;What about you?&lt;/strong&gt; Are you self-hosting your social media tooling, or do you trust the cloud providers? I'm genuinely curious — every self-hoster I've met has a story about the one time their config broke at 2 AM. Drop yours in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>opensource</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
