A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add single-container Docker deployment and auto image publishing

Package backend, frontend, scheduler, and Chromium into one runtime image so users can run the app with a single docker command. Add GHCR and optional Docker Hub GitHub Actions workflows to publish multi-arch latest tags automatically on pushes and releases.

jack 85544e44 251f0c7d

+381 -1
+12
.dockerignore
··· 1 + .git 2 + .gitignore 3 + node_modules 4 + dist 5 + web/dist 6 + data 7 + processed 8 + config.json 9 + .env 10 + npm-debug.log 11 + README.md 12 + TROUBLESHOOTING.md
+62
.github/workflows/docker-publish-dockerhub.yml
··· 1 + name: Publish Docker Image (Docker Hub) 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + tags: 9 + - 'v*' 10 + workflow_dispatch: 11 + 12 + concurrency: 13 + group: dockerhub-publish-${{ github.ref }} 14 + cancel-in-progress: true 15 + 16 + permissions: 17 + contents: read 18 + 19 + jobs: 20 + publish: 21 + if: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} 22 + runs-on: ubuntu-latest 23 + steps: 24 + - name: Checkout 25 + uses: actions/checkout@v4 26 + 27 + - name: Set up QEMU 28 + uses: docker/setup-qemu-action@v3 29 + 30 + - name: Set up Docker Buildx 31 + uses: docker/setup-buildx-action@v3 32 + 33 + - name: Log in to Docker Hub 34 + uses: docker/login-action@v3 35 + with: 36 + username: ${{ secrets.DOCKERHUB_USERNAME }} 37 + password: ${{ secrets.DOCKERHUB_TOKEN }} 38 + 39 + - name: Extract Docker metadata 40 + id: meta 41 + uses: docker/metadata-action@v5 42 + with: 43 + images: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/tweets-2-bsky 44 + tags: | 45 + type=raw,value=latest,enable={{is_default_branch}} 46 + type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }} 47 + type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} 48 + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} 49 + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} 50 + type=sha,prefix=sha- 51 + 52 + - name: Build and push 53 + uses: docker/build-push-action@v6 54 + with: 55 + context: . 56 + file: ./Dockerfile 57 + push: true 58 + platforms: linux/amd64,linux/arm64 59 + tags: ${{ steps.meta.outputs.tags }} 60 + labels: ${{ steps.meta.outputs.labels }} 61 + cache-from: type=gha 62 + cache-to: type=gha,mode=max
+63
.github/workflows/docker-publish.yml
··· 1 + name: Publish Docker Image 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + tags: 9 + - 'v*' 10 + workflow_dispatch: 11 + 12 + concurrency: 13 + group: docker-publish-${{ github.ref }} 14 + cancel-in-progress: true 15 + 16 + permissions: 17 + contents: read 18 + packages: write 19 + 20 + jobs: 21 + publish: 22 + runs-on: ubuntu-latest 23 + steps: 24 + - name: Checkout 25 + uses: actions/checkout@v4 26 + 27 + - name: Set up QEMU 28 + uses: docker/setup-qemu-action@v3 29 + 30 + - name: Set up Docker Buildx 31 + uses: docker/setup-buildx-action@v3 32 + 33 + - name: Log in to GHCR 34 + uses: docker/login-action@v3 35 + with: 36 + registry: ghcr.io 37 + username: ${{ github.actor }} 38 + password: ${{ secrets.GITHUB_TOKEN }} 39 + 40 + - name: Extract Docker metadata 41 + id: meta 42 + uses: docker/metadata-action@v5 43 + with: 44 + images: ghcr.io/${{ github.repository }} 45 + tags: | 46 + type=raw,value=latest,enable={{is_default_branch}} 47 + type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }} 48 + type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} 49 + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} 50 + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} 51 + type=sha,prefix=sha- 52 + 53 + - name: Build and push 54 + uses: docker/build-push-action@v6 55 + with: 56 + context: . 57 + file: ./Dockerfile 58 + push: true 59 + platforms: linux/amd64,linux/arm64 60 + tags: ${{ steps.meta.outputs.tags }} 61 + labels: ${{ steps.meta.outputs.labels }} 62 + cache-from: type=gha 63 + cache-to: type=gha,mode=max
+59
Dockerfile
··· 1 + # syntax=docker/dockerfile:1.7 2 + 3 + FROM node:22-bookworm-slim AS build 4 + 5 + WORKDIR /app 6 + 7 + RUN apt-get update \ 8 + && apt-get install -y --no-install-recommends \ 9 + python3 \ 10 + make \ 11 + g++ \ 12 + ca-certificates \ 13 + && rm -rf /var/lib/apt/lists/* 14 + 15 + COPY package.json package-lock.json ./ 16 + COPY scripts ./scripts 17 + 18 + RUN npm ci 19 + 20 + COPY . . 21 + 22 + RUN npm run build \ 23 + && npm prune --omit=dev 24 + 25 + 26 + FROM node:22-bookworm-slim AS runtime 27 + 28 + WORKDIR /app 29 + 30 + ENV NODE_ENV=production \ 31 + HOST=0.0.0.0 \ 32 + PORT=3000 \ 33 + CHROME_BIN=/usr/bin/chromium \ 34 + PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium 35 + 36 + RUN apt-get update \ 37 + && apt-get install -y --no-install-recommends \ 38 + chromium \ 39 + ca-certificates \ 40 + tini \ 41 + && rm -rf /var/lib/apt/lists/* 42 + 43 + COPY --from=build /app/package.json ./package.json 44 + COPY --from=build /app/node_modules ./node_modules 45 + COPY --from=build /app/dist ./dist 46 + COPY --from=build /app/web/dist ./web/dist 47 + COPY --from=build /app/public ./public 48 + 49 + RUN mkdir -p /app/data \ 50 + && ln -sf /app/data/config.json /app/config.json 51 + 52 + VOLUME ["/app/data"] 53 + 54 + EXPOSE 3000 55 + 56 + HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/api/auth/bootstrap-status').then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))"] 57 + 58 + ENTRYPOINT ["/usr/bin/tini", "--"] 59 + CMD ["node", "dist/index.js"]
+171 -1
README.md
··· 51 51 ./install.sh --skip-native-rebuild 52 52 ``` 53 53 54 - If you prefer full manual setup, skip to [Manual Setup](#manual-setup-technical). 54 + If you prefer full manual setup, skip to [Manual Setup](#manual-setup-technical). For a portable single-container setup, use [Docker](#docker-single-container-backend--frontend--scheduler). 55 + 56 + ## Docker (Single-Container, Backend + Frontend + Scheduler) 57 + 58 + This repo now includes a single `Dockerfile` that runs: 59 + 60 + - the backend API 61 + - the scheduler/worker loop 62 + - the built frontend dashboard 63 + - Chromium (for quote-tweet screenshot fallback support) 64 + 65 + The container aims for feature parity with normal installs while giving one-command startup. 66 + 67 + ### 1) Pull and run (recommended) 68 + 69 + After publishing an image (see [Publishing](#publishing-multi-platform-images-linuxamd64--linuxarm64)), run: 70 + 71 + ```bash 72 + docker run -d \ 73 + --name tweets-2-bsky \ 74 + -p 3000:3000 \ 75 + -v tweets2bsky_data:/app/data \ 76 + --restart unless-stopped \ 77 + ghcr.io/j4ckxyz/tweets-2-bsky:latest 78 + ``` 79 + 80 + Open `http://localhost:3000`. 81 + 82 + PowerShell equivalent: 83 + 84 + ```powershell 85 + docker run -d --name tweets-2-bsky -p 3000:3000 -v tweets2bsky_data:/app/data --restart unless-stopped ghcr.io/j4ckxyz/tweets-2-bsky:latest 86 + ``` 87 + 88 + ### 2) Build locally (if you do not want to pull) 89 + 90 + ```bash 91 + docker build -t tweets-2-bsky:local . 92 + 93 + docker run -d \ 94 + --name tweets-2-bsky \ 95 + -p 3000:3000 \ 96 + -v tweets2bsky_data:/app/data \ 97 + --restart unless-stopped \ 98 + tweets-2-bsky:local 99 + ``` 100 + 101 + ### 3) Environment variables 102 + 103 + Pass environment values with `-e` or `--env-file` (same values as normal install): 104 + 105 + ```bash 106 + docker run -d \ 107 + --name tweets-2-bsky \ 108 + -p 3000:3000 \ 109 + -v tweets2bsky_data:/app/data \ 110 + --env-file .env \ 111 + ghcr.io/j4ckxyz/tweets-2-bsky:latest 112 + ``` 113 + 114 + Common variables: 115 + 116 + - `PORT` (default `3000`) 117 + - `JWT_SECRET` (recommended to set explicitly) 118 + - `JWT_EXPIRES_IN` 119 + - `CORS_ALLOWED_ORIGINS` 120 + - `BSKY_APPVIEW_URL` (optional override) 121 + 122 + ### 4) Persistent data inside Docker 123 + 124 + Store all app state in `/app/data` (mounted via volume): 125 + 126 + - `/app/data/config.json` (mappings, users, credentials) 127 + - `/app/data/database.sqlite` 128 + - `/app/data/.jwt-secret` 129 + 130 + Note: inside the container, `/app/config.json` is linked to `/app/data/config.json` so one volume preserves everything important. 131 + 132 + ### 5) CLI usage in container 133 + 134 + You can run CLI commands without leaving Docker: 135 + 136 + ```bash 137 + docker exec -it tweets-2-bsky node dist/cli.js status 138 + docker exec -it tweets-2-bsky node dist/cli.js run-now 139 + docker exec -it tweets-2-bsky node dist/cli.js list 140 + ``` 141 + 142 + ### 6) Updating Docker deployments 143 + 144 + For Docker installs, update by pulling a newer image and recreating the container with the same volume: 145 + 146 + ```bash 147 + docker pull ghcr.io/j4ckxyz/tweets-2-bsky:latest 148 + docker stop tweets-2-bsky 149 + docker rm tweets-2-bsky 150 + docker run -d \ 151 + --name tweets-2-bsky \ 152 + -p 3000:3000 \ 153 + -v tweets2bsky_data:/app/data \ 154 + --restart unless-stopped \ 155 + ghcr.io/j4ckxyz/tweets-2-bsky:latest 156 + ``` 157 + 158 + ### 7) Platform support 159 + 160 + The Docker build is designed for multi-platform images: 161 + 162 + - `linux/amd64` (typical Linux servers, many Windows machines) 163 + - `linux/arm64` (Apple Silicon Macs, ARM Linux servers) 164 + 165 + This means the same image tag can be pulled on Docker Desktop (Windows/macOS) and Linux hosts. 166 + On Windows, use Docker Desktop in **Linux container** mode. 167 + 168 + ### Publishing (multi-platform images: linux/amd64 + linux/arm64) 169 + 170 + Automatic publishing is included via GitHub Actions: 171 + 172 + - `.github/workflows/docker-publish.yml` for GHCR 173 + - `.github/workflows/docker-publish-dockerhub.yml` for Docker Hub (only runs when Docker Hub secrets are set) 174 + 175 + - pushes to `master` or `main` publish fresh multi-arch images and update `:latest` 176 + - tags like `v2.0.0` publish versioned tags (`:2.0.0`, `:2.0`) 177 + - manual publish is available with **Actions -> Publish Docker Image -> Run workflow** 178 + - after first publish, set GHCR package visibility to **Public** so anyone can pull 179 + 180 + To enable automatic Docker Hub publishing with GitHub CLI: 181 + 182 + ```bash 183 + gh secret set DOCKERHUB_USERNAME --body "<dockerhub-username>" 184 + gh secret set DOCKERHUB_TOKEN --body "<dockerhub-access-token>" 185 + ``` 186 + 187 + If you keep your default branch as `master`, users can always pull the newest build with: 188 + 189 + ```bash 190 + docker pull ghcr.io/j4ckxyz/tweets-2-bsky:latest 191 + ``` 192 + 193 + #### Option A: GitHub Container Registry (GHCR) 194 + 195 + ```bash 196 + docker login ghcr.io -u <github-username> 197 + docker buildx create --name t2b-builder --use 198 + docker buildx inspect --bootstrap 199 + 200 + docker buildx build \ 201 + --platform linux/amd64,linux/arm64 \ 202 + -t ghcr.io/j4ckxyz/tweets-2-bsky:latest \ 203 + -t ghcr.io/j4ckxyz/tweets-2-bsky:2.0.0 \ 204 + --push . 205 + ``` 206 + 207 + Then set the GHCR package visibility to **Public** in GitHub package settings. 208 + 209 + #### Option B: Docker Hub 210 + 211 + ```bash 212 + docker login 213 + docker buildx create --name t2b-builder --use 214 + docker buildx inspect --bootstrap 215 + 216 + docker buildx build \ 217 + --platform linux/amd64,linux/arm64 \ 218 + -t <dockerhub-user>/tweets-2-bsky:latest \ 219 + -t <dockerhub-user>/tweets-2-bsky:2.0.0 \ 220 + --push . 221 + ``` 222 + 223 + Once published, users only need `docker pull` + `docker run`. 55 224 56 225 ## Linux VPS Without Domain (Secure HTTPS via Tailscale) 57 226 ··· 116 285 - PM2 (for managed background runtime) 117 286 - Chrome/Chromium (used for some quote-tweet screenshot fallbacks) 118 287 - build tools for native modules (`better-sqlite3`) if your platform needs source compilation 288 + - Docker Desktop / Docker Engine (if using containerized deployment) 119 289 120 290 ## Manual Setup (Technical) 121 291
+14
TROUBLESHOOTING.md
··· 57 57 ```bash 58 58 npm run cli -- status 59 59 ``` 60 + 61 + ### Docker: permissions writing `/app/data` 62 + If the container fails to write `config.json` or `database.sqlite`, ensure `/app/data` is writable by the container process. 63 + 64 + For easiest portability, use a named Docker volume: 65 + 66 + ```bash 67 + docker volume create tweets2bsky_data 68 + docker run -d --name tweets-2-bsky -p 3000:3000 -v tweets2bsky_data:/app/data ghcr.io/j4ckxyz/tweets-2-bsky:latest 69 + ``` 70 + 71 + ### Docker: updating image 72 + In Docker mode, update by pulling a newer image and recreating the container with the same volume. 73 + `/api/update` / `update.sh` are source-install workflows.