This week, the conversation around Docker and auto-updating containers has sparked a lot of debate, especially around the tradeoffs between convenience and control. As engineers who've spent time wrestling with container orchestration, we’ve found that sometimes the simplest setups are the most reliable - and that includes self-hosting.
We’re running a self-hosted instance of Postiz, a self-hostable Bluesky client, using Docker Compose. The goal is to have a lightweight, zero-ops setup that can be managed directly from a Python script. This approach avoids the complexity of Kubernetes or managed services while still giving us full control over the deployment and API interactions.
Our Docker Compose setup is minimal, pulling the latest Postiz image from GitHub Packages and exposing the necessary ports. Here's a simplified version of our docker-compose.yml:
version: '3.8'
services:
postiz:
image: ghcr.io/postiz/postiz:latest
ports:
- "8080:8080"
environment:
POSTIZ_DATABASE_URL: "postgres://user:pass@postgres:5432/postiz"
depends_on:
- postgres
postgres:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: postiz
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
The Python client communicates with the Postiz instance via the /api/public/v1/posts endpoint. This endpoint supports both creating a new post and fetching a list of posts, depending on the HTTP method used. The JSON payload for a new post includes the text, visibility, and optionally a reply to another post. Here's the shape of the request body:
{
"text": "This is a new post from the Python client.",
"visibility": "public",
"reply": {
"uri": "at://example.com/0123456789abcdef0123456789abcdef"
}
}
The Authorization header is required and must be a valid Bearer token, typically obtained via the /auth/token endpoint. This token is scoped to the user and provides access to the /api/public/v1/posts endpoint.
One of the quirks we've encountered is that the /api/public/v1/posts endpoint can return either a single post object or a list of posts, depending on the context of the request. This behavior is not always well-documented, so we've implemented a helper function in our Python client to normalize the response:
import requests
def fetch_posts(url, headers):
response = requests.get(f"{url}/api/public/v1/posts", headers=headers)
response.raise_for_status()
data = response.json()
return data if isinstance(data, list) else [data]
This function ensures that regardless of whether the API returns a single object or a list, our code can handle it consistently.
We’re currently working on extending the Python client to support more of Postiz’s API endpoints, including user management and moderation tools. We’re also exploring ways to integrate this setup with a local development environment that mirrors production as closely as possible.
What would you prioritize next if you were building a self-hosted Bluesky client?
Top comments (0)