How to Install Postiz on a Local Windows Machine
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, t

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. 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 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. 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. 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. Verify in Git Bash: docker --version Any version number means the CLI is reachable and the engine is up. If you get bash: docker: command not found, see Pitfall 1. 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 This is just the Docker Compose configuration - the application itself 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. docker-compose.yaml ships with a placeholder: JWT_SECRET: 'random string that is unique to every install - just type random characters here!' That string signs your login tokens, and it's identical in every copy of the repo. Generate your own: openssl rand -base64 32 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. docker compose up -d The first run pulls roughly 9.5 GB across 8 images - expect 5–15 minutes. -d runs it detached, in the background. docker compose ps 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 Services start in dependency order - Elasticsearch, then Temporal, then (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 Register - the first account is the admin. Once your account exists, stop anyone else from creating one. In docker-compose.yaml: DISABLE_REGISTRATION: 'true' Apply it: docker compose up -d That recreates only the changed container. Your account and data live in 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 docker-compose.yaml JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env} DATABASE_URL: 'postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postiz-postgres:5432/${POSTGRES_DB}' .gitignore .env The :? syntax makes Compose fail with a clear message if the variable is Check the substitution before restarting: docker compose config | grep JWT_SECRET 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 error while interpolating services.postiz.environment.JWT_SECRET: required variable JWT_SECRET is missing a value: set JWT_SECRET in .env Back .env up somewhere outside the repo. It's git-ignored by design, JWT_SECRET and every session breaks. 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 docker-compose.yaml X_API_KEY: ${X_API_KEY:-} X_API_SECRET: ${X_API_SECRET:-} The :- default keeps blank values valid, so platforms you haven't set up Then docker compose up -d and connect the channel in the UI. These are the variable names the compose file ships with. Wire up only the .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 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. 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. 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 Data lives in Docker named volumes and survives stop, up -d, restart, down. docker compose down -v deletes the volumes - that erases your account, It's the one command to be careful with. 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 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" 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 (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 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. 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 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 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)})" 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 This is the trap that makes Pitfall 2 confusing. docker compose ps can report every service (healthy) while Postiz is 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 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. 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. ...which surfaces to you as a 502 on login, with every service reporting 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 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 /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. 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.
Key Takeaways
- •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 story was reported by Dev.to, covering developments in the dev space.
- •AI advancements continue to reshape industries — read the full article on Dev.to for complete coverage.
📖 Continue reading the full article:
Read Full Article on Dev.to →


