Self-Host LightPost, a One-Dashboard Social Media Scheduler, with Docker Compose

LightPost lets you write a post once and send it to Bluesky, Mastodon, X, Threads, LinkedIn and more from your own server. Here is the recorded Docker Compose install of LightPost v0.2.0, running locally with no login and no Dockerfile.

Self-Host LightPost, a One-Dashboard Social Media Scheduler, with Docker Compose

LightPost is a small, self-hosted social media scheduler. You connect your accounts, write a post once, and publish it now or schedule it for later from a single dashboard. It is one Python process with SQLite and plain HTML/CSS/JavaScript, so it needs no external database and no frontend build step. It is MIT-licensed.

Supported platforms in v0.2.0 are Bluesky, Mastodon, X (Twitter), Instagram, Threads, Discord, LinkedIn, Telegram and Facebook Pages. TikTok, Pinterest and Google Business Profile are listed upstream as under development, so don't plan on them yet. You bring your own API credentials for each platform. LightPost encrypts them and keeps them on your server.

This guide follows the exact path we recorded and checked: LightPost v0.2.0 (commit 1698095) on Docker 29.9.2 with Compose 5.6.1, rootful Docker on Linux. The repo ships no Dockerfile, so we run the README's own Python install inside a throwaway python:3.12-slim container. Nothing gets installed on your host besides that image, two Docker volumes and one container.

Source: github.com/brainstormity/LightPost

LightPost landing page with the

Prerequisites

  • Rootful Docker Engine on Linux with the Compose v2 plugin (docker compose version works). The primary path uses network_mode: host. On rootless Docker or Docker Desktop, host networking isn't your real loopback, so use the bridge variant in the troubleshooting section.
  • git, curl and ss (iproute2)
  • A free TCP port 3000 on 127.0.0.1
  • No platform credentials are needed to install and explore the app. You only need them when you connect a real account.

Install

Run every block in the same shell, because the variables from step 1 are used throughout.

1. Get the code and set paths

git clone https://github.com/brainstormity/LightPost.git ~/LightPost
export REPO=~/LightPost
export DEPLOY=~/lightpost-deploy
export PROJ=lightpost

The compose file and its .env go in $DEPLOY, outside the clone. The repo gets mounted read-only, so the clone stays clean.

2. Preflight

docker info >/dev/null 2>&1 && echo docker_ok
# expect: docker_ok
docker compose version
# expect: Docker Compose version v2.x or newer
test -f $REPO/requirements.txt && test -f $REPO/lightpost/__main__.py && echo repo_ok
# expect: repo_ok
docker info --format '{{.SecurityOptions}}' | grep -q rootless && echo ROOTLESS || echo rootful
# expect: rootful   (ROOTLESS means: use the bridge variant under Troubleshooting)

3. Pick a free port

LightPost defaults to 3000. This loop takes the first free port from a short list:

LP_PORT=
for p in 3000 3010 3020 3030 3040; do
  ss -ltn | awk '{print $4}' | grep -qE "[:.]$p\$" || { LP_PORT=$p; break; }
done
echo "LP_PORT=$LP_PORT"
# expect: LP_PORT=3000

4. Write the compose file

The container uses host networking and binds 127.0.0.1. That keeps LightPost in its documented loopback mode, where no password is needed and nothing is exposed to your LAN. The venv and the SQLite database live in named volumes.

mkdir -p $DEPLOY
cat > $DEPLOY/.env <<EOF
LP_PORT=$LP_PORT
REPO=$REPO
EOF
cat > $DEPLOY/compose.yml <<'EOF'
services:
  lightpost:
    image: python:3.12-slim
    network_mode: host
    working_dir: /src
    volumes:
      - ${REPO}:/src:ro
      - lightpost-data:/data
      - lightpost-venv:/opt/venv
    environment:
      HOST: 127.0.0.1
      PORT: ${LP_PORT}
      DATABASE_PATH: /data/lightpost.sqlite
      PYTHONUNBUFFERED: "1"
      PIP_DISABLE_PIP_VERSION_CHECK: "1"
    command:
      - sh
      - -c
      - |
        set -e
        test -x /opt/venv/bin/python || python -m venv /opt/venv
        /opt/venv/bin/pip install -q -r requirements.txt
        exec /opt/venv/bin/python -m lightpost
    healthcheck:
      test: ["CMD", "python", "-c", "import os,urllib.request;urllib.request.urlopen('http://127.0.0.1:%s/api/session'%os.environ['PORT'],timeout=3)"]
      interval: 5s
      timeout: 5s
      retries: 30
      start_period: 60s
    restart: "no"
volumes:
  lightpost-data:
  lightpost-venv:
EOF
docker compose -p $PROJ --env-file $DEPLOY/.env -f $DEPLOY/compose.yml config >/dev/null && echo compose_ok
# expect: compose_ok

restart: "no" suits a test run. If you keep LightPost, change it to unless-stopped, because scheduled posts only go out while the app is running.

5. Start it

docker compose -p $PROJ --env-file $DEPLOY/.env -f $DEPLOY/compose.yml up -d
docker compose -p $PROJ --env-file $DEPLOY/.env -f $DEPLOY/compose.yml logs lightpost | tail -1
# expect: lightpost-1  | LightPost: http://127.0.0.1:3000

The first start installs the four Python dependencies (aiohttp, aiosqlite, cryptography, regex) into the venv volume. On our run, with the image already pulled, the container was healthy about 12 seconds after up -d. On a cold machine, give it up to a minute or two. If the log line isn't there yet, wait a few seconds and run the logs command again.

Browser setup

Open http://127.0.0.1:3000/ (use your LP_PORT). Use 127.0.0.1 or localhost only. In loopback mode, LightPost rejects API calls that arrive with any other hostname, such as a LAN IP.

  1. The landing page shows the hero and platform logos. Click Start creating → to open /app. No signup or login is needed.
  2. "Opening your workspace…" gives way to the dashboard. The sidebar has New post, Posts, Accounts, Settings and Setup Guide.

Empty composer with the

  1. New post: type into the text box. The Post preview on the right mirrors your text as you type, and the character counter updates. "Publish to" shows Connect account until you add one. Saving, scheduling or publishing a post requires at least one connected account.

Composer with typed text and the live preview showing 109 characters

  1. Setup Guide: a card for each platform. Open one, for example Bluesky, to see step-by-step credential instructions and a Connect Bluesky button. ← All platforms takes you back.

Setup Guide platform grid

Bluesky setup guide with the Connect Bluesky button

  1. Accounts: starts empty. Connect your first account opens a modal where you pick a platform and enter its credentials. For Bluesky, that's your handle, home server and an app password. Submitting the form makes a real call to the platform, so only do it with credentials you intend to use.

Accounts empty state

Connect an account modal with Bluesky handle, home server and app-password fields

  1. Settings: change Name to your workspace name and click Save settings. The top-bar title updates and a "Settings saved." toast appears. This writes to the SQLite database in the lightpost-data volume.

Settings saved with the workspace renamed to

  1. Theme: the Dark theme switch in the top bar flips between light and dark.

LightPost settings page in the dark theme

  1. Posts: shows "Your first post starts here." until you create one.

Posts empty state

In our recorded run we did not connect any account or publish anything. Everything above works with no credentials.

Verify

for i in $(seq 1 30); do
  s=$(docker inspect -f '{{.State.Health.Status}}' $(docker compose -p $PROJ --env-file $DEPLOY/.env -f $DEPLOY/compose.yml ps -q lightpost))
  [ "$s" = healthy ] && break; sleep 3
done; echo "health=$s"
# expect: health=healthy

curl -s http://127.0.0.1:$LP_PORT/api/session; echo
# expect: {"authenticated": true, "passwordRequired": false}

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:$LP_PORT/app
# expect: 200

curl -s http://127.0.0.1:$LP_PORT/api/state | head -c 300; echo
# expect: JSON starting with {"accounts": [], "posts": [], ...  on a new workspace

A working install also means:

  • /app shows the sidebar and composer with no "Unable to open workspace" error card.
  • The workspace name you saved in Settings is still there after a page reload.
  • docker compose ... logs lightpost shows no Python tracebacks.

Tear down

docker compose -p $PROJ --env-file $DEPLOY/.env -f $DEPLOY/compose.yml down -v --remove-orphans
docker volume ls --filter name=$PROJ -q     # expect: no output
rm -rf $DEPLOY                              # optional: removes the compose file and .env

down -v deletes the data volume, which holds the database, uploaded media and the generated encryption key. If you want to keep your workspace, run down without -v. For a backup, the README says to stop the app and copy the whole data directory, here the lightpost-data volume, together with any .env. Post-history exports leave out credentials and images, so they are not full backups.

Troubleshooting

SymptomCauseFix
Logs say Set ADMIN_PASSWORD ... and the container exitsHOST isn't a loopback address. LightPost refuses to bind beyond loopback without a password of 16+ charactersKeep HOST: 127.0.0.1 with host networking, or use the bridge variant below
Address already in use in the logsPort was taken between step 3 and step 5Re-run steps 3–5 to pick another port
/api/* returns 403 with "Use a loopback hostname…"You opened the app via a LAN IP or another hostnameBrowse to http://127.0.0.1:<port>/app or http://localhost:<port>/app
curl to 127.0.0.1 gets connection refused, but the container runsRootless Docker or Docker Desktop: network_mode: host isn't your host's loopbackUse the bridge variant below
No LightPost: http://... log line yetpip is still installing on first startWait and re-run the logs command. Later starts reuse the lightpost-venv volume
Save, Schedule or Publish shows "Choose at least one connected account."No account is connectedConnect an account under Accounts, using that platform's Setup Guide
Settings and posts disappeared after a restartYou ran down -v, which removes the data volumeUse down without -v to keep data

Bridge variant (rootless Docker / Docker Desktop)

We did not run this variant in the recorded install. It comes from the install plan and the README's ADMIN_PASSWORD rule.

In bridge mode the app must bind 0.0.0.0, so LightPost requires an ADMIN_PASSWORD of at least 16 characters and shows a sign-in form at /app.

grep -q ADMIN_PASSWORD $DEPLOY/.env || echo "ADMIN_PASSWORD=lightpost-$(openssl rand -hex 6)" >> $DEPLOY/.env
grep ADMIN_PASSWORD $DEPLOY/.env       # you'll type this into the sign-in form
cat > $DEPLOY/compose.bridge.yml <<'EOF'
services:
  lightpost:
    network_mode: !reset null
    ports:
      - "127.0.0.1:${LP_PORT}:${LP_PORT}"
    environment:
      HOST: 0.0.0.0
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
EOF
docker compose -p $PROJ --env-file $DEPLOY/.env -f $DEPLOY/compose.yml -f $DEPLOY/compose.bridge.yml up -d
curl -s http://127.0.0.1:$LP_PORT/api/session; echo
# expect: {"authenticated": false, "passwordRequired": true}

Add -f $DEPLOY/compose.bridge.yml to the down command when you tear this variant down. If you serve LightPost over HTTPS, also set COOKIE_SECURE=true.

Assumptions & decisions

  • Primary path only, as validated. The steps follow the Deployment Process Reviewer's PASS verdict (install plan path A: host network, loopback bind, no login). The bridge variant is marked untested because only path A ran.
  • Reader-friendly paths. The recorded run used pipeline paths (runs/lightpost-20261029-193937/...) and the project name shortform-lightpost-20261029-193937. The post uses ~/LightPost, ~/lightpost-deploy and PROJ=lightpost instead. Every command and the compose file are otherwise identical to the recorded install.sh/compose.yml.
  • Non-following logs. The plan used logs -f. The recorded script and this post use logs | tail -1, which needs no Ctrl-C.
  • Timing. We report the measured ~12 s (warm image cache) and avoid promising a cold-start number. The plan's 30–90 s estimate was not measured.
  • restart: "no" kept to match the recording, with a note to switch to unless-stopped for real use. That note follows from the README's statement that the app must keep running for scheduled delivery.
  • The Settings screenshot shows "Shortform Demo", the name the recording used. The text tells readers to use their own workspace name.
  • No publishing or account connection claims. The demo ran with no credentials. TikTok, Pinterest and Google Business Profile are called out as under development, matching upstream.
  • Screenshots are referenced by repo-root-relative paths under runs/lightpost-20261029-193937/browser/. All 10 referenced files exist. final.png is left for the feature image, because the ghost stage already picks it up.
  • Troubleshooting rows come from the install plan's documented errors, the README's configuration table and the reviewer's 403 check. The "connection refused on rootless" row is inferred from the plan's rootless note.
  • Replaced the stale Sablier howto.md from the previous job.