{
  "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"
}