DEV Community

Cover image for How to Install Postiz on a Local Windows Machine with Only Docker Desktop
Mathias Ahlgren
Mathias Ahlgren

Posted on • Originally published at stackrater.io

How to Install Postiz on a Local Windows Machine with Only Docker Desktop

Postiz is the hugely popular open-source social media scheduler, a self-hosted alternative to Buffer or Hootsuite with over 33,000 GitHub stars. This guide walks through installing it on Windows with Docker Desktop.

If you are like me, and found that the official Postiz documentation is too vague, then this step by step is for you.

⚡ Read this first: the local-hosting caveat

One thing to know before you start:

Postiz only publishes posts while your machine is running.

Scheduling is handled by a background worker inside the Docker stack. If you schedule a post for 9:00 AM Tuesday and your PC is asleep, shut down, or Docker isn't running, the post does not go out. It fires late, when the stack next starts.

Windows sleep settings are the usual culprit. A machine that suspends
overnight will miss anything scheduled overnight, so check your power plan if posts land hours late.

That makes a local install great for:

  • Trying Postiz before committing to a server
  • Drafting and organising content
  • Development and testing

...and a poor fit for reliably hitting a posting schedule. If you need posts to land on time, put it on a VPS or use the hosted version. Everything below applies to a server too - only the "keep the machine on" problem goes away.

If scheduling is a dealbreaker then you can either pay for Postiz, or host it on a VPS. A cheap VPS is enough; I've written up which hosts are actually worth it here.

What you need

  • Windows 10 or 11
  • Docker Desktop - https://docker.com/products/docker-desktop
  • Git (for Git Bash) - https://git-scm.com/download/win
  • ~15 GB free disk space. The stack pulls about 9.5 GB of images, plus room for volumes and overhead. (Measured on one install - image sizes drift, so treat it as a ballpark.)
  • 8 GB RAM minimum; the stack idles around 3–4 GB. Note that Docker Desktop's WSL2 backend has its own memory ceiling on top of that, so 8 GB total leaves very little headroom. 16 GB is a lot more comfortable.

Times below assume a reasonably fast connection. The image pull dominates.

Step 1 - Install and start Docker Desktop

Install it, then launch it and wait for the whale icon in the system tray to stop animating. The Docker engine runs in a Linux VM that takes 30–60 seconds to boot. Every docker command fails until it's up.

Turn on Settings → General → Start Docker Desktop when you log in.
Postiz can't publish anything if Docker isn't running, so on a local install this is doing real work for you.

Verify in Git Bash:

docker --version
Enter fullscreen mode Exit fullscreen mode

Any version number means the CLI is reachable and the engine is up. If you get
bash: docker: command not found, see Pitfall 1.

Step 2 - Get the compose file

Pick wherever you keep projects. In Git Bash, your Windows user folder is
~, so this puts it in C:\Users\<you>\projects\postiz:

mkdir -p ~/projects && cd ~/projects
git clone https://github.com/gitroomhq/postiz-docker-compose postiz
cd postiz
Enter fullscreen mode Exit fullscreen mode

This is just the Docker Compose configuration - the application itself
arrives as prebuilt images.

The folder name matters a little: Compose prefixes its volumes and networks with it. Clone into postiz and you get postiz_postgres-volume; clone into my-postiz and it's my-postiz_postgres-volume. Commands below assume postiz.

Step 3 - Set a real JWT secret

docker-compose.yaml ships with a placeholder:

JWT_SECRET: 'random string that is unique to every install - just type random characters here!'
Enter fullscreen mode Exit fullscreen mode

That string signs your login tokens, and it's identical in every copy of the repo. Generate your own:

openssl rand -base64 32
Enter fullscreen mode Exit fullscreen mode

Paste the result in place of the placeholder. Do this before first launch - changing it later invalidates existing sessions.

Two other settings worth knowing, both near the top of the file:

Setting Default Meaning
DISABLE_REGISTRATION 'false' Signup is open. Needed for your first account - close it afterwards (Step 6).
MAIN_URL http://localhost:4007 Change only if you're not on localhost.

Step 4 - Start it

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

The first run pulls roughly 9.5 GB across 8 images - expect 5–15 minutes.
Subsequent starts take well under a minute.

-d runs it detached, in the background.

Step 5 - Wait for it to actually be ready

docker compose ps
Enter fullscreen mode Exit fullscreen mode

You want all services showing (healthy):

postiz                   Up (healthy)
postiz-postgres          Up (healthy)
postiz-redis             Up (healthy)
temporal                 Up (healthy)
temporal-postgresql      Up (healthy)
temporal-elasticsearch   Up (healthy)
temporal-ui              Up (healthy)
temporal-admin-tools     Up
Enter fullscreen mode Exit fullscreen mode

Services start in dependency order - Elasticsearch, then Temporal, then
Postiz - and Postiz runs database migrations on first boot. Seeing
(health: starting) for two or three minutes is normal.

Then open:

http://localhost:4007

Use http://, not https://. Nothing here terminates TLS, so the https
URL simply fails.

Register - the first account is the admin.

Step 6 - Close registration

Once your account exists, stop anyone else from creating one. In
docker-compose.yaml:

DISABLE_REGISTRATION: 'true'
Enter fullscreen mode Exit fullscreen mode

Apply it:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

That recreates only the changed container. Your account and data live in
named volumes and are untouched - you stay logged in.

Step 7 - Keep secrets out of version control

Optional, but worth doing if you'll ever commit this config or copy it to a server.

Docker Compose automatically reads a .env file sitting next to
docker-compose.yaml. Move the secrets there:

.env

JWT_SECRET=your-generated-secret
POSTGRES_USER=postiz-user
POSTGRES_PASSWORD=postiz-password
POSTGRES_DB=postiz-db-local
Enter fullscreen mode Exit fullscreen mode

docker-compose.yaml

JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
DATABASE_URL: 'postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postiz-postgres:5432/${POSTGRES_DB}'
Enter fullscreen mode Exit fullscreen mode

.gitignore

.env
Enter fullscreen mode Exit fullscreen mode

The :? syntax makes Compose fail with a clear message if the variable is
missing, rather than silently starting with an empty secret.

Check the substitution before restarting:

docker compose config | grep JWT_SECRET
Enter fullscreen mode Exit fullscreen mode

If .env is set up correctly, this prints the resolved value. If the variable is missing, docker compose config exits with an error rather than printing anything - the grep never runs. Either outcome tells you what you
need:

error while interpolating services.postiz.environment.JWT_SECRET:
required variable JWT_SECRET is missing a value: set JWT_SECRET in .env
Enter fullscreen mode Exit fullscreen mode

Back .env up somewhere outside the repo. It's git-ignored by design,
which also means it isn't backed up. Lose the JWT_SECRET and every session breaks.

Step 8 - Connect social channels

Postiz reads platform credentials from the container's environment, not from anything in the web UI. There's no dashboard field for these - you set them in config and restart.

Using the .env pattern from Step 7, take X as the example:

.env

X_API_KEY=your-key-here
X_API_SECRET=your-secret-here
Enter fullscreen mode Exit fullscreen mode

docker-compose.yaml

X_API_KEY: ${X_API_KEY:-}
X_API_SECRET: ${X_API_SECRET:-}
Enter fullscreen mode Exit fullscreen mode

The :- default keeps blank values valid, so platforms you haven't set up
stay unavailable instead of breaking startup.

Then docker compose up -d and connect the channel in the UI.

The full list of supported platforms

These are the variable names the compose file ships with. Wire up only the
ones you need - the pattern is identical for each: add the variables to
.env, switch the compose file to ${VAR:-}, restart.

Platform Variables
X (Twitter) X_API_KEY, X_API_SECRET
Facebook FACEBOOK_APP_ID, FACEBOOK_APP_SECRET
Instagram / Threads THREADS_APP_ID, THREADS_APP_SECRET
LinkedIn LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET
TikTok TIKTOK_CLIENT_ID, TIKTOK_CLIENT_SECRET
YouTube YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET
Pinterest PINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET
Reddit REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET
Mastodon MASTODON_CLIENT_ID, MASTODON_CLIENT_SECRET, MASTODON_URL
Discord DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN_ID
Slack SLACK_ID, SLACK_SECRET, SLACK_SIGNING_SECRET
Dribbble DRIBBBLE_CLIENT_ID, DRIBBBLE_CLIENT_SECRET
GitHub GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
Beehiiv BEEHIIVE_API_KEY, BEEHIIVE_PUBLICATION_ID

MASTODON_URL defaults to https://mastodon.social - change it if you're on a different instance. Note BEEHIIVE_* is spelled with the extra "E" in the compose file; copy it exactly.

Postiz supports more platforms than this in its UI. If one you want isn't
listed, check the
configuration reference for its variable names and add them the same way.

Each platform needs an app registered on its developer portal, and each needs a callback URL whitelisted - typically http://localhost:4007/integrations/social/<platform>. Check
docker compose logs postiz for the exact URL Postiz expects if a connection fails.

Start with an easy one

Platforms differ enormously in how much work they take, and it's worth knowing before you sink an afternoon into the wrong one:

  • Easy - Mastodon, Discord, Reddit, GitHub. Credentials issued immediately, no review.
  • Moderate - LinkedIn (needs a company page), Pinterest, Slack.
  • Hard - Facebook, Instagram/Threads, TikTok, YouTube. App review, sandbox restrictions, or business verification.
  • Paid - X requires a paid API tier for write access. The free tier is read-only, so posting won't work.

If you just want to prove the pipeline works end to end, Mastodon takes about two minutes: Preferences → Development → New application in any Mastodon instance.

These requirements change frequently - treat the grouping above as a starting point and check the current terms on each portal.

Everyday commands

docker compose ps             # status
docker compose logs -f postiz # follow logs, Ctrl+C to quit
docker compose stop           # stop, keeps all data
docker compose up -d          # start
docker compose restart postiz # restart just the app
Enter fullscreen mode Exit fullscreen mode

Data lives in Docker named volumes and survives stop, up -d, restart,
and down.

docker compose down -v deletes the volumes - that erases your account,
posts, and uploads.
It's the one command to be careful with.


Troubleshooting and common pitfalls

Pitfall 1: bash: docker: command not found

Docker Desktop is installed, but your shell can't find it.

First, check Docker Desktop is actually running. No amount of PATH fixing helps if the engine is down.

If it is running, check whether the CLI is on your PATH:

echo "$PATH" | tr ':' '\n' | grep -i docker
Enter fullscreen mode Exit fullscreen mode

No output means the directory is missing. Find the binary:

ls "/c/Program Files/Docker/Docker/resources/bin/docker.exe"
ls "$LOCALAPPDATA/Programs/DockerDesktop/resources/bin/docker.exe"
Enter fullscreen mode Exit fullscreen mode

Recent Docker Desktop versions may install per-user to
%LOCALAPPDATA%\Programs\DockerDesktop rather than C:\Program Files\Docker. Guides that assume Program Files won't match.

Add whichever path exists to ~/.bashrc. $HOME keeps your username out of it and stays in Unix-style path format, which is what PATH wants:

echo 'export PATH="$PATH:$HOME/AppData/Local/Programs/DockerDesktop/resources/bin"' >> ~/.bashrc
source ~/.bashrc
Enter fullscreen mode Exit fullscreen mode

(If yours is the Program Files install, use
/c/Program Files/Docker/Docker/resources/bin instead.)

Avoid $LOCALAPPDATA here. It expands to a Windows-style path with
backslashes and a drive letter (C:\Users\you\AppData\Local), and a C: in a colon-separated PATH is asking for trouble. $HOME is already
/c/Users/you in Git Bash.

A subtlety worth knowing: even when the directory is correctly registered in your Windows PATH, terminals inherit their environment from explorer.exe, which may hold a stale copy from before the install. Opening a new terminal doesn't always help - a reboot does. Editing ~/.bashrc sidesteps the whole problem.

Note that your project's location on disk is irrelevant here. PATH is a list of absolute directories; where you run the command from doesn't matter.

Pitfall 2: 502 Bad Gateway when logging in or registering

The page loads, but submitting the form returns nginx's 502.

This means the frontend is fine and the backend is dead.

The postiz container isn't one process - it runs several behind a single
nginx instance:

Process Port Role
nginx 5000 front door; routes everything
frontend (Next.js) 4200 the UI you see
backend (NestJS) 3000 the API - /api/* proxies here
workers / cron - publishing and scheduled jobs

nginx serves the UI happily whether or not the backend is alive. So the page renders, then every API call - login, registration - returns 502.

Check whether the backend is listening:

docker exec postiz ss -tln | grep 3000
Enter fullscreen mode Exit fullscreen mode

If ss isn't in the image, use Node, which is always present:

docker exec postiz node -e "require('net').connect(3000,'127.0.0.1').on('connect',()=>{console.log('backend UP');process.exit(0)}).on('error',()=>{console.log('backend DOWN');process.exit(1)})"
Enter fullscreen mode Exit fullscreen mode
  • A LISTEN line (or backend UP) → backend is fine; look elsewhere.
  • No output (or backend DOWN) → backend is down. Find out why:
docker compose logs postiz | grep -i "backend failed" -A 5
Enter fullscreen mode Exit fullscreen mode

Pitfall 3: "All services healthy" but the app is broken

This is the trap that makes Pitfall 2 confusing.

docker compose ps can report every service (healthy) while Postiz is
fundamentally broken. Look at what the compose file's health check actually tests - it fetches http://localhost:5000/, which is nginx. And nginx stays perfectly healthy serving the frontend even when the backend behind it has crashed.

So on this stack, (healthy) means "the web server is up," not "the
application works." When something misbehaves, the port 3000 check above is a far better signal than the status column.

Pitfall 4: Don't remove services to save RAM

The stack runs 8 containers and the Temporal portion looks like overkill for a personal install. Resist trimming it.

temporal-elasticsearch in particular looks optional and is not.
Elasticsearch is genuinely optional for Temporal in general - it powers
"advanced visibility," and Temporal falls back to PostgreSQL without it. But Postiz registers custom Temporal search attributes at startup, and the PostgreSQL visibility store caps Text-type attributes at three. Postiz needs more.

Remove Elasticsearch and the backend dies during initialisation with:

Unable to create search attributes: cannot have more than 3 search attribute of type Text.
Enter fullscreen mode Exit fullscreen mode

...which surfaces to you as a 502 on login, with every service reporting
healthy. The compose file ships these services for a reason.

If you already removed it, restore the original Elasticsearch service, then wipe Temporal's state so it re-initialises consistently:

docker compose down
docker volume rm postiz_temporal-postgres-data
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

That removes Temporal's database only. Your Postiz account and posts live in postiz_postgres-volume and are unaffected.

Check your volume names first - Compose prefixes them with the directory name, so they're postiz_* only if you cloned into a folder called postiz:

docker volume ls | grep temporal
Enter fullscreen mode Exit fullscreen mode

Pitfall 5: /api/copilot/chat timeouts in the logs

Harmless. That's the AI assistant feature failing because OPENAI_API_KEY is empty. Ignore it unless you want AI-generated post content, in which case supply a key.


Should you self-host locally at all?

Postiz is the best self-hosted Buffer alternative out there. Local hosting it is a genuinely good way to evaluate Postiz, learn the stack, and draft content without paying for anything. BUT, it is not a good way to hit a posting schedule.

If scheduling reliability matters, the same compose file runs on a small VPS - you'd change MAIN_URL, FRONTEND_URL, and NEXT_PUBLIC_BACKEND_URL to your domain and put a reverse proxy with TLS in front. Everything else transfers unchanged.

A cheap VPS is enough; I've written up which hosts are actually worth signing up for separately, along with the renewal-pricing traps to avoid.

Top comments (0)