{
"path": "/3lyhwwy26nk22",
"site": "at://did:plc:6ll5xi67lyuyovt6fiv4fnjo/site.standard.publication/3lyht3qgykk2g",
"$type": "site.standard.document",
"title": "How I run my ATProto PDS",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Hi!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 20,
"byteStart": 13
},
"features": [
{
"uri": "https://atproto.com/guides/overview",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 24,
"byteStart": 21
},
"features": [
{
"uri": "https://atproto.com/guides/glossary#pds-personal-data-server",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "I run my own ATProto PDS, for various reasons:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "I want to own my data."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "It's a good way to make the ATProto network resilient against centralized infrastructure."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "I'm a nerd."
},
"children": []
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Last year I set myself a challenge: buy the cheapest VPS during the Black Friday deals and host my PDS on it."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.bskyPost",
"postRef": {
"cid": "bafyreiggdlxl6veyso6tfzkzjejtutet6gncxdkorxfhrfofhjivirus5q",
"uri": "at://did:plc:6ll5xi67lyuyovt6fiv4fnjo/app.bsky.feed.post/3lc7bzgmf2s25"
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "It hasn't been a flawless experience - mostly due to deliberately choosing cheap hosting - but the process gave me the knowledge and confidence to host my main ATProto account on it: this post explains how I did it."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "PDS hosting doesn't require many resources: posts are lightweight, the only media you're storing is your own, and the nature of the relay architecture means you have limited traffic aimed directly at your server - except if you're hosting your own website on it!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 22,
"byteStart": 10
},
"features": [
{
"uri": "https://alpinelinux.org/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "As a huge Alpine Linux fan, I installed it through the VPS administration panel, and as expected, it has performed flawlessly."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 47,
"byteStart": 41
},
"features": [
{
"uri": "https://podman.io/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 83,
"byteStart": 74
},
"features": [
{
"uri": "https://www.portainer.io/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 118,
"byteStart": 110
},
"features": [
{
"uri": "https://github.com/bluesky-social/pds",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 138,
"byteStart": 127
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://github.com/bluesky-social/pds/blob/main/compose.yaml",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "The entire stack is executed on rootless Podman, managed remotely through Portainer: I use a variation of the official Bluesky compose.yml file that I extracted their repository."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 27,
"byteStart": 17
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "I don't like the install.sh approach as it feels brittle and hard to maintain long-term: I run many services on my machines, all of them containerized, to avoid messing with the host configuration files and runtime."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "I kept Caddy as my reverse proxy of choice because it's set-and-forget once configured properly, and the setup Bluesky provides just works."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 49,
"byteStart": 28
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 186,
"byteStart": 180
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Bluesky suggests the use of containrrr/watchtower, but I'd advise against it: I've been bitten in the past by indiscriminate upgrades, mostly due to pulling Docker images with the latest tag, and I'd like not to repeat that experience anymore."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Instead of automated updates, I subscribed to Bluesky's Github notifications to keep myself up to date with new releases."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 102,
"byteStart": 83
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "My philosophy for this deployment is to do the best you can: posting on Bluesky is not a vital service for me, I'm okay with a few hours of downtime."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 75,
"byteStart": 69
},
"features": [
{
"uri": "https://semver.org",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "So far the PDS software seems to be managed in a sane way, following semver best practices."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 44,
"byteStart": 33
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "I never had issues stemming from docker pulling every once in a while."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "The only problem I encountered was due to the host system clock losing track of time, which I promptly solved by enabling an NTP daemon."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 74,
"byteStart": 66
},
"features": [
{
"uri": "https://borgbase.com",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 86,
"byteStart": 80
},
"features": [
{
"uri": "https://restic.net/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "To mitigate the risk of data loss, I backup the data directory on Borgbase with Restic every few hours - I also schedule regular scrubs and checks with the same Docker image."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 43,
"byteStart": 32
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Without further ado, here's the compose.yml file:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "version: \"3.9\"\nservices:\n pds:\n container_name: pds\n image: ghcr.io/bluesky-social/pds:0.4\n restart: unless-stopped\n env_file: \"./stack.env\"\n volumes:\n - type: bind\n source: ${DATADIR}\n target: /pds\n caddy:\n image: caddy:latest\n restart: unless-stopped\n command: caddy run --config /etc/caddy/Caddyfile\n ports:\n - \"80:80\"\n - \"443:443\"\n - \"443:443/udp\"\n volumes:\n - ${DATADIR}/geesawra.industries:/geesawra.industries\n - ${DATADIR}/caddy/Caddyfile:/etc/caddy/Caddyfile\n - ${DATADIR}/caddy/data:/data\n - ${DATADIR}/caddy/config:/config\n\n backup:\n image: mazzolino/restic\n hostname: docker\n restart: unless-stopped\n environment:\n RUN_ON_STARTUP: \"true\"\n BACKUP_CRON: \"*/45 * * * *\"\n RESTIC_CHECK_ARGS: >-\n --read-data-subset=10%\n RESTIC_REPOSITORY: rest:[REDACTED]\n RESTIC_PASSWORD: [REDACTED]\n RESTIC_BACKUP_SOURCES: /mnt/pds\n RESTIC_BACKUP_ARGS: >-\n --tag pds\n RESTIC_FORGET_ARGS: >-\n --keep-last 10\n --keep-daily 7\n --keep-weekly 5\n --keep-monthly 12\n TZ: Europe/Berlin\n volumes:\n - ${DATADIR}:/mnt/pds:ro\n\n prune:\n image: mazzolino/restic\n hostname: docker\n restart: unless-stopped\n environment:\n SKIP_INIT: \"true\"\n RUN_ON_STARTUP: \"false\"\n PRUNE_CRON: \"0 0 4 * * *\"\n RESTIC_REPOSITORY: rest:[REDACTED]\n RESTIC_PASSWORD: [REDACTED]\n TZ: Europe/Berlin\n\n check:\n image: mazzolino/restic\n hostname: docker\n restart: unless-stopped\n environment:\n SKIP_INIT: \"true\"\n RUN_ON_STARTUP: \"false\"\n CHECK_CRON: \"0 15 5 * * *\"\n RESTIC_CHECK_ARGS: >-\n --read-data-subset=10%\n RESTIC_REPOSITORY: rest:[REDACTED]\n RESTIC_PASSWORD: [REDACTED]\n TZ: Europe/Berlin"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 50,
"byteStart": 39
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "There are two notable features in this compose.yml:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 6,
"byteStart": 3
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 83,
"byteStart": 74
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "As pds is configured through environment variables, I'm storing them in a stack.env - this file is auto-generated by Portainer, managed through its web UI."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 46,
"byteStart": 36
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 77,
"byteStart": 68
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "I'm binding the host volume path to ${DATADIR}, which is defined in stack.env."
},
"children": []
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Running a PDS isn't exactly a walk in the park, but if you've played around with containers before and feel at home in the Linux shell, you should be good to go."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 57,
"byteStart": 46
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "On a positive note, migrating my account from bsky.social to my own PDS has been a great experience, and it's certainly something the ActivityPub folks should look into for their next protocol iteration."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.bskyPost",
"postRef": {
"cid": "bafyreif2bhgzp4ilwhsb6iciqjchjvjrtofd65aonii5gzzkmmpcqiqr2y",
"uri": "at://did:plc:6ll5xi67lyuyovt6fiv4fnjo/app.bsky.feed.post/3l772nfieyk2t"
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.bskyPost",
"postRef": {
"cid": "bafyreic4zrihyt4qne7mr7nyb6wxcvrnjoelc4i65srqckyovzfcltwggi",
"uri": "at://did:plc:6ll5xi67lyuyovt6fiv4fnjo/app.bsky.feed.post/3l772vddndc2t"
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "Until next time!"
}
}
]
}
]
},
"description": "",
"publishedAt": "2025-04-30T09:41:33.541Z"
}