-
-
Notifications
You must be signed in to change notification settings - Fork 50
feat: image-based docker self-hosting #225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,6 +25,25 @@ concurrency: | |||||||||||||||||||
| cancel-in-progress: false | ||||||||||||||||||||
|
|
||||||||||||||||||||
| jobs: | ||||||||||||||||||||
| # Attach the self-host compose to the release so | ||||||||||||||||||||
| # releases/latest/download/docker-compose.yml always serves the file | ||||||||||||||||||||
| # matching the newest image. | ||||||||||||||||||||
| assets: | ||||||||||||||||||||
| if: github.event_name == 'release' | ||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||
| permissions: | ||||||||||||||||||||
| contents: write | ||||||||||||||||||||
| steps: | ||||||||||||||||||||
| - name: Checkout | ||||||||||||||||||||
| uses: actions/checkout@v7 | ||||||||||||||||||||
| with: | ||||||||||||||||||||
| persist-credentials: false | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - name: Upload self-host compose to release | ||||||||||||||||||||
| env: | ||||||||||||||||||||
| GH_TOKEN: ${{ github.token }} | ||||||||||||||||||||
| run: gh release upload ${{ github.event.release.tag_name }} selfhost/docker-compose.yml --clobber | ||||||||||||||||||||
|
Comment on lines
+42
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Pass
🔒️ Proposed fix: use env var instead of direct interpolation - name: Upload self-host compose to release
env:
GH_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
- run: gh release upload ${{ github.event.release.tag_name }} selfhost/docker-compose.yml --clobber
+ run: gh release upload "$RELEASE_TAG" selfhost/docker-compose.yml --clobber📝 Committable suggestion
Suggested change
🧰 Tools🪛 zizmor (1.26.1)[error] 45-45: code injection via template expansion (template-injection): may expand into attacker-controllable code (template-injection) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||
|
|
||||||||||||||||||||
| build: | ||||||||||||||||||||
| # releases always build; manual dispatch skips the build when an existing | ||||||||||||||||||||
| # image tag is supplied (rollback path) | ||||||||||||||||||||
|
|
@@ -42,6 +61,12 @@ jobs: | |||||||||||||||||||
| with: | ||||||||||||||||||||
| persist-credentials: false | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # QEMU enables the arm64 leg of the multi-arch build (Apple Silicon, | ||||||||||||||||||||
| # Raspberry Pi, Graviton/ARM VPS self-hosters). Wheels are prebuilt | ||||||||||||||||||||
| # for both arches so emulation cost is mostly I/O. | ||||||||||||||||||||
| - name: Set up QEMU | ||||||||||||||||||||
| uses: docker/setup-qemu-action@v3 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - name: Set up Docker Buildx | ||||||||||||||||||||
| uses: docker/setup-buildx-action@v4 | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -76,7 +101,7 @@ jobs: | |||||||||||||||||||
| APP_VERSION=${{ steps.meta.outputs.version }} | ||||||||||||||||||||
| cache-from: type=gha | ||||||||||||||||||||
| cache-to: type=gha,mode=max | ||||||||||||||||||||
| platforms: linux/amd64 | ||||||||||||||||||||
| platforms: linux/amd64,linux/arm64 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| deploy: | ||||||||||||||||||||
| needs: build | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,40 @@ | ||
| # ── Builder: resolve and install dependencies only ────────────────────── | ||
| FROM python:3.14-slim AS builder | ||
|
|
||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Dependency layer keyed on the lockfile inputs alone — code changes never | ||
| # invalidate it, so rebuilds after app edits skip the entire install. | ||
| COPY pyproject.toml uv.lock ./ | ||
| RUN uv sync --frozen --no-dev --no-cache --compile-bytecode | ||
|
|
||
|
|
||
| # ── Runtime: venv + code, no build machinery, non-root ────────────────── | ||
| FROM python:3.14-slim | ||
|
|
||
| # Injected by CI: release version (2.1.0) or short sha for edge builds | ||
| ARG APP_VERSION=dev | ||
| ENV APP_VERSION=${APP_VERSION} | ||
| # UV_NO_SYNC: `uv run` entrypoints must never mutate the baked venv at | ||
| # container start (the dev-dependency group isn't installed, and a sync | ||
| # would try to pull it). | ||
| ENV APP_VERSION=${APP_VERSION} \ | ||
| UV_NO_SYNC=1 \ | ||
| PATH="/app/.venv/bin:${PATH}" | ||
|
|
||
| # Install curl for healthchecks (10MB) and clean up apt cache | ||
| # curl for healthchecks (10MB) and clean up apt cache | ||
| RUN apt-get update && apt-get install -y --no-install-recommends curl \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && useradd --uid 1000 --create-home appuser | ||
|
|
||
| # Install uv. | ||
| # uv stays available so `uv run ...` compose commands keep working | ||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ | ||
|
|
||
| # Copy the application into the container. | ||
| WORKDIR /app | ||
| COPY --from=builder /app/.venv /app/.venv | ||
| COPY . /app/ | ||
|
|
||
| # Install the application dependencies. | ||
| WORKDIR /app | ||
| RUN uv sync --frozen --no-cache | ||
| USER appuser | ||
|
|
||
| CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # spoo.me — self-hosted | ||
| # | ||
| # Quick start (no clone, no build, no .env required): | ||
| # wget https://github.com/spoo-me/spoo/releases/latest/download/docker-compose.yml | ||
| # docker compose up -d | ||
| # → http://localhost:8000 | ||
| # | ||
| # Upgrade: | ||
| # docker compose pull && docker compose up -d | ||
| # | ||
| # Pin a version (recommended for production): create a .env file next to | ||
| # this compose with SPOO_VERSION=2.0.2 — or export it in your shell. | ||
| # Every ${VAR:-default} below can be overridden the same way; the file | ||
| # itself never needs editing. | ||
| # | ||
| # Everything optional is off by default and the app degrades gracefully: | ||
| # no OAuth → no social login, no Zepto token → no emails, no hCaptcha → | ||
| # captcha checks pass, no Sentry → no tracking. MongoDB is the only hard | ||
| # requirement, and it's bundled below. | ||
|
|
||
| name: spoo | ||
|
|
||
| services: | ||
| app: | ||
| image: ghcr.io/spoo-me/spoo:${SPOO_VERSION:-latest} | ||
| restart: unless-stopped | ||
| ports: | ||
| - "${SPOO_PORT:-8000}:8000" | ||
| depends_on: | ||
| mongo: | ||
| condition: service_healthy | ||
| redis: | ||
| condition: service_healthy | ||
| environment: | ||
| # ── Core ──────────────────────────────────────────────────────────── | ||
| MONGODB_URI: ${MONGODB_URI:-mongodb://mongo:27017} | ||
| REDIS_URI: ${REDIS_URI:-redis://redis:6379/0} | ||
| # Set to the public URL your instance is reached at (e.g. | ||
| # https://s.example.com) — it's used to build the short links you | ||
| # hand out. | ||
| APP_URL: ${APP_URL:-http://localhost:8000} | ||
| ENV: production | ||
| LOG_FORMAT: ${LOG_FORMAT:-console} | ||
|
|
||
| # ── Secrets ───────────────────────────────────────────────────────── | ||
| # The app boots and shortens URLs without these, but account tokens | ||
| # are only tamper-proof once they're set. Generate each with: | ||
| # openssl rand -hex 32 | ||
| SECRET_KEY: ${SECRET_KEY:-} | ||
| JWT_SECRET: ${JWT_SECRET:-} | ||
| # Login cookies require HTTPS when true. Flip to true once you're | ||
| # behind a TLS-terminating reverse proxy. | ||
| COOKIE_SECURE: ${COOKIE_SECURE:-false} | ||
|
|
||
| # ── Optional integrations (empty = feature off) ───────────────────── | ||
| # Social login — see https://docs.spoo.me/self-hosting/setting-up-authentication | ||
| GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID:-} | ||
| GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET:-} | ||
| GOOGLE_OAUTH_REDIRECT_URI: ${GOOGLE_OAUTH_REDIRECT_URI:-} | ||
| GITHUB_OAUTH_CLIENT_ID: ${GITHUB_OAUTH_CLIENT_ID:-} | ||
| GITHUB_OAUTH_CLIENT_SECRET: ${GITHUB_OAUTH_CLIENT_SECRET:-} | ||
| GITHUB_OAUTH_REDIRECT_URI: ${GITHUB_OAUTH_REDIRECT_URI:-} | ||
| DISCORD_OAUTH_CLIENT_ID: ${DISCORD_OAUTH_CLIENT_ID:-} | ||
| DISCORD_OAUTH_CLIENT_SECRET: ${DISCORD_OAUTH_CLIENT_SECRET:-} | ||
| DISCORD_OAUTH_REDIRECT_URI: ${DISCORD_OAUTH_REDIRECT_URI:-} | ||
| # Transactional email (account verification, password reset) | ||
| ZEPTO_API_TOKEN: ${ZEPTO_API_TOKEN:-} | ||
| ZEPTO_FROM_EMAIL: ${ZEPTO_FROM_EMAIL:-} | ||
| # Contact / abuse-report forms → Discord webhooks | ||
| CONTACT_WEBHOOK: ${CONTACT_WEBHOOK:-} | ||
| URL_REPORT_WEBHOOK: ${URL_REPORT_WEBHOOK:-} | ||
| # Bot protection on public forms | ||
| HCAPTCHA_SITEKEY: ${HCAPTCHA_SITEKEY:-} | ||
| HCAPTCHA_SECRET: ${HCAPTCHA_SECRET:-} | ||
| # Error tracking | ||
| SENTRY_DSN: ${SENTRY_DSN:-} | ||
| healthcheck: | ||
| test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"] | ||
| interval: 15s | ||
| timeout: 5s | ||
| retries: 3 | ||
| start_period: 30s | ||
|
|
||
| mongo: | ||
| image: mongo:8 | ||
| restart: unless-stopped | ||
| # Not exposed on the host — reachable only inside the compose network. | ||
| # Point MONGODB_URI at your own server (e.g. Atlas) to skip this | ||
| # container entirely; compose still starts it, but nothing talks to it. | ||
| volumes: | ||
| - mongo-data:/data/db | ||
| - mongo-config:/data/configdb | ||
| healthcheck: | ||
| test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 5 | ||
| start_period: 10s | ||
|
|
||
| redis: | ||
| image: redis:7-alpine | ||
| restart: unless-stopped | ||
| # Cache semantics: bounded memory, LRU eviction, no persistence — the | ||
| # cache rebuilds from Mongo. The app runs fine without Redis at all | ||
| # (set REDIS_URI to empty), just slower under load. | ||
| command: > | ||
| redis-server | ||
| --maxmemory 256mb | ||
| --maxmemory-policy allkeys-lru | ||
| --save "" | ||
| --appendonly no | ||
| healthcheck: | ||
| test: ["CMD", "redis-cli", "ping"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 5 | ||
|
|
||
| volumes: | ||
| mongo-data: | ||
| mongo-config: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): The workflow assumes
ghis available on the runner, which might break if GitHub changes the default image.The
assetsjob callsgh release uploadand assumes the GitHub CLI is preinstalled onubuntu-latest. To avoid brittle, image-dependent failures, consider adding an explicit install/setup step forghor pinning the runner image to one that guaranteesghis available.