I self-hosted Postiz — it kept redirecting to localhost until MAIN_URL was HTTPS

I self-hosted Postiz — it kept redirecting to localhost until MAIN_URL was HTTPS

I wanted a private social calendar instead of another scheduling SaaS. Postiz on Ubuntu 24.04 using the official Compose repo came up, then every redirect still pointed at localhost until MAIN_URL, FRONTEND_URL, and NEXT_PUBLIC_BACKEND_URL matched the public hostname.

 Postiz self-hosted social scheduling workspace

Caption: Drafts and provider tokens on my VPS. Temporal actually fires the schedule. Nginx is the only public port.

Why I wanted this on my server

Campaign calendars and unreleased copy do not belong in a random SaaS seat. I wanted Postiz on a domain I control: connect one network, schedule a boring post, keep Postgres and uploads in backups I own.

Social APIs change. This is an app with a Temporal stack, not a static HTML form. I treat upgrades like production.

What I actually installed

Official gitroomhq/postiz-docker-compose under /opt/postiz. App, PostgreSQL, Redis, Temporal (and Temporal’s Postgres/Elasticsearch). Nginx to 127.0.0.1:4007. Floor: 2 vCPU / 4 GB RAM; more if media and many schedules. SSD. The compose file is the source of truth — I clone it, I do not invent a three-service stack from memory.

Where it broke

On a fresh Ubuntu 24.04 box this install is famous for sending you back to localhost.

The containers were up. curl -I http://127.0.0.1:4007/ looked fine. The browser on https://postiz.example.com bounced to localhost URLs because the upstream compose still had sample MAIN_URL / FRONTEND_URL / NEXT_PUBLIC_BACKEND_URL.

I set:

  • MAIN_URL and FRONTEND_URL to https://postiz.example.com
  • NEXT_PUBLIC_BACKEND_URL to https://postiz.example.com/api

Then recreated containers (docker compose down without -v, then up -d). Env in an already-created Next app is not a restart away.

The other footgun: DISABLE_REGISTRATION=true before any user exists. I flipped it off from a trusted network, created admin, flipped it back.

 Postiz Docker Compose stack

Caption: App, Postgres, Redis, Temporal behind Nginx. First boot is slow on purpose.

Prerequisites

Docker + Compose v2, Git, Nginx, Certbot, provider developer apps only after the base stack is healthy. Hex passwords in DATABASE_URL so I do not fight URL encoding.

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl git gnupg openssl ufw nginx certbot python3-certbot-nginx python3

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

The working install

1. Install Docker Engine

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version

2. Clone the official Compose repository

sudo mkdir -p /opt/postiz
sudo chown -R "$USER":"$USER" /opt/postiz
chmod 750 /opt/postiz

git clone https://github.com/gitroomhq/postiz-docker-compose /opt/postiz
cd /opt/postiz

git status --short
git log -1 --oneline

3. Generate deployment secrets

cd /opt/postiz

export POSTIZ_DOMAIN="postiz.example.com"
export POSTIZ_DB_PASSWORD="$(openssl rand -hex 24)"
export POSTIZ_JWT_SECRET="$(openssl rand -hex 32)"

printf 'Postiz domain: %s\n' "$POSTIZ_DOMAIN"
printf 'Postiz database password: %s\n' "$POSTIZ_DB_PASSWORD"
printf 'Postiz JWT secret: %s\n' "$POSTIZ_JWT_SECRET"

JWT stays stable unless I want every session dead.

4. Harden docker-compose.yaml

cd /opt/postiz
cp docker-compose.yaml docker-compose.yaml.original

python3 - <<'PY'
from pathlib import Path
import os
import re

path = Path("docker-compose.yaml")
text = path.read_text()
domain = os.environ["POSTIZ_DOMAIN"]
db_password = os.environ["POSTIZ_DB_PASSWORD"]
jwt_secret = os.environ["POSTIZ_JWT_SECRET"]

replacements = {
    r"MAIN_URL:\s*'[^']*'": f"MAIN_URL: 'https://{domain}'",
    r"FRONTEND_URL:\s*'[^']*'": f"FRONTEND_URL: 'https://{domain}'",
    r"NEXT_PUBLIC_BACKEND_URL:\s*'[^']*'": f"NEXT_PUBLIC_BACKEND_URL: 'https://{domain}/api'",
    r"JWT_SECRET:\s*'[^']*'": f"JWT_SECRET: '{jwt_secret}'",
    r"DATABASE_URL:\s*'[^']*'": f"DATABASE_URL: 'postgresql://postiz-user:{db_password}@postiz-postgres:5432/postiz-db-local'",
    r"DISABLE_REGISTRATION:\s*'[^']*'": "DISABLE_REGISTRATION: 'true'",
    r"POSTGRES_PASSWORD:\s*[^\n]+": f"POSTGRES_PASSWORD: {db_password}",
}

for pattern, value in replacements.items():
    text = re.sub(pattern, value, text)

text = text.replace('- "4007:5000"', '- "127.0.0.1:4007:5000"')
path.write_text(text)
PY

git diff -- docker-compose.yaml

Bind is 127.0.0.1:4007:5000. DATABASE_URL password must match POSTGRES_PASSWORD.

5. Start Postiz

cd /opt/postiz

docker compose config
docker compose pull
docker compose up -d
docker compose ps

First boot can take several minutes. Postgres, Redis, Temporal Postgres, Elasticsearch, Temporal, then the app. I wait. I do not -v wipe volumes to “try again.”

docker compose logs --since=10m postiz postiz-postgres postiz-redis temporal temporal-postgresql temporal-elasticsearch
docker compose ps
curl -I http://127.0.0.1:4007/

Unhealthy Temporal often means RAM or disk, and Elasticsearch complains first.

6. Configure Nginx and HTTPS

Create /etc/nginx/sites-available/postiz.example.com:

server {
    listen 80;
    listen [::]:80;
    server_name postiz.example.com;

    client_max_body_size 100m;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300;
        proxy_pass http://127.0.0.1:4007;
    }
}
sudo ln -s /etc/nginx/sites-available/postiz.example.com /etc/nginx/sites-enabled/postiz.example.com
sudo nginx -t
sudo systemctl reload nginx

sudo certbot --nginx -d postiz.example.com
sudo systemctl reload nginx

Important tips

Scheduled posts are Temporal’s job. If a draft sits forever, I check RUN_CRON, Temporal health, clock, and whether the provider token is still valid — not only the Postiz UI.

STORAGE_PROVIDER: 'local'
UPLOAD_DIRECTORY: '/uploads'
NEXT_PUBLIC_UPLOAD_DIRECTORY: '/uploads'

Local uploads are easy to tar. They also fill the disk. client_max_body_size in Nginx has to match what I upload.

Provider keys go in the same env block, one network at a time:

X_API_KEY=
X_API_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
cd /opt/postiz
docker compose down
docker compose up -d
docker compose logs --since=5m postiz

Never down -v for a config tweak.

 Postiz provider credential flow

Caption: One provider, one callback, one harmless scheduled post. Then the next provider.

First publish I actually ran

Admin login, public URL is HTTPS, small image upload, one provider, a post a few minutes out. I watch docker compose logs -f postiz temporal through that window. Then registration stays off.

Backup, expose, next step

cd /opt/postiz
mkdir -p backups

docker compose exec -T postiz-postgres pg_dump -U postiz-user postiz-db-local > "backups/postiz-db-$(date +%F).sql"
docker run --rm -v postiz_postiz-uploads:/uploads:ro -v "$PWD/backups:/backups" alpine \
  tar -czf "/backups/postiz-uploads-$(date +%F).tar.gz" -C / uploads

tar -czf "backups/postiz-config-$(date +%F).tar.gz" docker-compose.yaml dynamicconfig

If the upload volume name differs, docker volume ls. Compose revision + secrets travel with the dump.

cd /opt/postiz
git fetch origin main
git diff HEAD..origin/main -- docker-compose.yaml

 Postiz backup and restore flow

Caption: Compose repo revision, Postgres, Redis, uploads, Temporal data, secret list. One set.

What I have running now is Postiz on HTTPS with registration closed, localhost bind, URLs pointing at the real domain. Next I connect one low-risk account, survive one scheduled publish, then copy backups off-box.

Did you hit the same wall?

I got stuck on Postiz redirecting to localhost until MAIN_URL, FRONTEND_URL, and NEXT_PUBLIC_BACKEND_URL were the public HTTPS URLs and I recreated the containers. Did you hit the same thing, or a different one? Tell me in the comments. I read them.

Need this done on your server?

I deploy and harden Laravel/CodeCanyon apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.