cloudunity commited on
Commit
4c1376d
·
verified ·
1 Parent(s): b4f3a29

Update Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +26 -283
Dockerfile CHANGED
@@ -1,294 +1,37 @@
1
- # syntax=docker/dockerfile:1
2
- # =============================================================================
3
- # Rochester-restricted Bluesky PDS + full Bluesky web app - single Dockerfile.
4
- #
5
- # The web client (bsky.app React Native Web SPA) is prebuilt by GitHub Actions
6
- # (workflow: .github/workflows/prebuild-web.yml) and force-pushed to the
7
- # `prebuilt-web` branch. This image simply copies that static bundle into the
8
- # official PDS image and patches it at boot to (a) restrict registration to
9
- # @rochesterschools.org and (b) serve the web app at / with an SPA fallback.
10
- #
11
- # No Node build happens here - so this builds fast and fits in low-memory
12
- # builders like HF Spaces free tier.
13
- #
14
- # Listens on port 7860 (HF forwards the public Space URL to it automatically).
15
- # =============================================================================
16
 
17
- FROM node:24-bookworm-slim AS webapp
 
18
 
19
- RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
20
 
21
- WORKDIR /bundle
 
22
 
23
- # The prebuilt bundle lives on the `prebuilt-web` branch (pushed by
24
- # prebuild-web.yml). Update SOURCE_REF to the commit of that branch whenever
25
- # the web app changes and the workflow has run.
26
- ARG SOURCE_REPO=https://github.com/CloudCompile/social-app.git
27
- ARG SOURCE_REF=prebuilt-web
28
- RUN git init . \
29
- && git remote add origin ${SOURCE_REPO} \
30
- && git fetch --depth 1 origin ${SOURCE_REF} \
31
- && git checkout FETCH_HEAD
32
 
33
- RUN test -f index.html || { echo "prebuilt-web branch has no index.html - run the prebuild-web workflow first"; exit 1; }
 
34
 
35
- # --------------------------------------------------------------- server stage
36
- FROM ghcr.io/bluesky-social/pds:0.4
 
 
37
 
38
- USER root
 
 
 
39
 
40
- # Web app bundle
41
- COPY --from=webapp /bundle /app/web
42
-
43
- # Write the email-allowlist patcher into the image.
44
- RUN <<'PATCH'
45
- cat > /app/allowlist-patch.cjs <<'EOF'
46
- /*
47
- * Patches the bundled @atproto/pds dist to enforce an email-domain allowlist
48
- * on com.atproto.server.createAccount. Idempotent; fails fast if upstream
49
- * code layout changes so the container never runs with open registration.
50
- */
51
- const fs = require('node:fs')
52
- const path = require('node:path')
53
-
54
- const pdsDist = path.join(__dirname, 'node_modules', '@atproto', 'pds', 'dist')
55
-
56
- const domains = (process.env.PDS_EMAIL_ALLOWED_DOMAINS || '')
57
- .split(',')
58
- .map((d) => d.trim().toLowerCase().replace(/^\./, ''))
59
- .filter(Boolean)
60
-
61
- if (domains.length === 0) {
62
- console.error('[allowlist] PDS_EMAIL_ALLOWED_DOMAINS is not set; refusing to patch (open registration is not allowed)')
63
- process.exit(1)
64
- }
65
-
66
- function findTarget(dir) {
67
- for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
68
- const full = path.join(dir, entry.name)
69
- if (entry.isDirectory()) {
70
- const found = findTarget(full)
71
- if (found) return found
72
- } else if (
73
- entry.name === 'createAccount.js' &&
74
- fs.readFileSync(full, 'utf8').includes('validateInputsForLocalPds') &&
75
- fs.readFileSync(full, 'utf8').includes('isDisposableEmail')
76
- ) {
77
- return full
78
- }
79
- }
80
- return null
81
- }
82
-
83
- const target = findTarget(pdsDist)
84
- if (!target) {
85
- console.error('[allowlist] could not locate createAccount.js under ' + pdsDist)
86
- process.exit(1)
87
- }
88
-
89
- let source = fs.readFileSync(target, 'utf8')
90
-
91
- if (source.includes('__ROCHESTER_EMAIL_ALLOWLIST__')) {
92
- console.log('[allowlist] already patched, skipping')
93
- process.exit(0)
94
- }
95
-
96
- const conditionOld = '!isEmailValid(email) || isDisposableEmail(email)'
97
- const conditionNew = conditionOld + ' || !__rochesterEmailAllowed(email)'
98
- if (!source.includes(conditionOld)) {
99
- console.error('[allowlist] email validity condition not found; upstream layout changed')
100
- process.exit(1)
101
- }
102
- source = source.replace(conditionOld, conditionNew)
103
-
104
- const messageOld = "'This email address is not supported, please use a different email.'"
105
- const messageNew = "'Registration is restricted to " + domains.join(', ') + " email addresses.'"
106
- if (!source.includes(messageOld)) {
107
- console.error('[allowlist] error message anchor not found; upstream layout changed')
108
- process.exit(1)
109
- }
110
- source = source.replace(messageOld, messageNew)
111
-
112
- const helper = [
113
- '',
114
- '// __ROCHESTER_EMAIL_ALLOWLIST__ start',
115
- 'function __rochesterEmailAllowed(email) {',
116
- " const domain = String(email || '').split('@')[1]?.toLowerCase()",
117
- ' return ' + JSON.stringify(domains) + '.includes(domain)',
118
- '}',
119
- '// __ROCHESTER_EMAIL_ALLOWLIST__ end',
120
- '',
121
- ].join('\n')
122
- const helperAnchor = 'export default function (server, ctx)'
123
- if (!source.includes(helperAnchor)) {
124
- console.error('[allowlist] helper injection point not found; upstream layout changed')
125
- process.exit(1)
126
- }
127
- source = source.replace(helperAnchor, helper + '\n' + helperAnchor)
128
-
129
- fs.writeFileSync(target, source)
130
- console.log('[allowlist] patched ' + target + ' (domains: ' + domains.join(', ') + ')')
131
- EOF
132
- PATCH
133
-
134
- # Write the entrypoint: generate secrets on first boot, patch, start.
135
- RUN <<'ENTRY'
136
- cat > /usr/local/bin/entrypoint.sh <<'EOF'
137
- #!/bin/sh
138
- set -e
139
- cd /app
140
-
141
- SECRETS_FILE=/app/data/generated-secrets.env
142
- mkdir -p /app/data
143
-
144
- # Generate persistent secrets on first boot so restarts keep sessions valid.
145
- # NOTE: a secp256k1 (K256) private key is just 32 random bytes, so no
146
- # @atproto/crypto import is needed (it is not resolvable under pnpm layout).
147
- if [ ! -f "$SECRETS_FILE" ]; then
148
- echo "[entrypoint] first boot - generating secrets"
149
- : > "$SECRETS_FILE"
150
- fi
151
-
152
- # Backfill any missing secret individually (covers files created by older
153
- # image versions that crashed mid-generation).
154
- . "$SECRETS_FILE" || true
155
-
156
- if [ -z "${PDS_JWT_SECRET:-}" ]; then
157
- PDS_JWT_SECRET=$(node -e 'console.log(require("crypto").randomBytes(16).toString("hex"))')
158
- echo "PDS_JWT_SECRET=$PDS_JWT_SECRET" >> "$SECRETS_FILE"
159
- fi
160
- if [ -z "${PDS_ADMIN_PASSWORD:-}" ]; then
161
- PDS_ADMIN_PASSWORD=$(node -e 'console.log(require("crypto").randomBytes(16).toString("hex"))')
162
- echo "PDS_ADMIN_PASSWORD=$PDS_ADMIN_PASSWORD" >> "$SECRETS_FILE"
163
- fi
164
- if [ -z "${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX:-}" ]; then
165
- PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')
166
- echo "PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX" >> "$SECRETS_FILE"
167
- fi
168
- chmod 600 "$SECRETS_FILE" || true
169
- export PDS_JWT_SECRET PDS_ADMIN_PASSWORD PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX
170
-
171
- # PDS_HOSTNAME must be a bare hostname (no scheme, no trailing slash).
172
- # did:web is built from it; normalize common full-URL mistakes.
173
- PDS_HOSTNAME=$(node -e '
174
- let h = process.env.PDS_HOSTNAME || ""
175
- h = h.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "").replace(/:.*$/, "")
176
- console.log(h)
177
- ')
178
- export PDS_HOSTNAME
179
-
180
- echo "================================================================"
181
- echo " Generated secrets (saved in $SECRETS_FILE):"
182
- echo " PDS_ADMIN_PASSWORD: $PDS_ADMIN_PASSWORD"
183
- echo "================================================================"
184
-
185
- node allowlist-patch.cjs
186
-
187
- # Serve the bundled Bluesky web app at / with an SPA fallback. Patched into
188
- # basic-routes.js so the middleware is registered before the '/' banner route;
189
- # the fallback skips API paths (/xrpc, /.well-known, /tls-check, files with
190
- # extensions) so the PDS API keeps working. Idempotent.
191
- node - <<'WEBAPP'
192
- const fs = require('node:fs')
193
- const path = require('node:path')
194
- const MARKER = '__ROCHESTER_WEB_APP__'
195
- function findBasicRoutes(dir) {
196
- try {
197
- for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
198
- const full = path.join(dir, entry.name)
199
- if (entry.isDirectory()) {
200
- const found = findBasicRoutes(full)
201
- if (found) return found
202
- } else if (entry.name === 'basic-routes.js') {
203
- return full
204
- }
205
- }
206
- } catch {}
207
- return null
208
- }
209
- const target = findBasicRoutes('/app/node_modules') || findBasicRoutes('/app/node_modules/.pnpm')
210
- if (!target) {
211
- console.error('[webapp] could not locate basic-routes.js')
212
- process.exit(1)
213
- }
214
- let src = fs.readFileSync(target, 'utf8')
215
- if (src.includes(MARKER)) {
216
- console.log('[webapp] already patched, skipping')
217
- process.exit(0)
218
- }
219
- const anchor = 'const router = Router()'
220
- if (!src.includes(anchor)) {
221
- console.error('[webapp] injection point not found in basic-routes.js')
222
- process.exit(1)
223
- }
224
- if (!src.includes("from 'express'") || !src.includes("import { Router }")) {
225
- console.error('[webapp] unexpected imports in basic-routes.js')
226
- process.exit(1)
227
- }
228
- const injection = [
229
- anchor,
230
- ' // ' + MARKER + ' start',
231
- " const webDir = '/app/web'",
232
- ' router.use(express.static(webDir, {index: \'index.html\', maxAge: \'1h\'}))',
233
- ' router.get(\'*\', function (req, res, next) {',
234
- " if (req.path.startsWith('/xrpc/') || req.path.startsWith('/.') || req.path.startsWith('/tls-check')) return next()",
235
- ' if (path.extname(req.path)) return next()',
236
- " res.sendFile(path.join(webDir, 'index.html'))",
237
- ' })',
238
- ' // ' + MARKER + ' end',
239
- ].join('\n')
240
- src = src.replace(anchor, injection)
241
- if (!src.match(/^import path\b/m)) {
242
- src = "import path from 'node:path'\n" + src
243
- }
244
- // express.static needs the default export; basic-routes only imports {Router}
245
- if (!src.match(/^import express\b/m)) {
246
- src = "import express from 'express'\n" + src
247
- }
248
- fs.writeFileSync(target, src)
249
- console.log('[webapp] web app serving patched into ' + target)
250
- WEBAPP
251
-
252
- exec node --enable-source-maps index.ts
253
- EOF
254
- chmod +x /usr/local/bin/entrypoint.sh
255
- ENTRY
256
-
257
- # Data directory (SQLite + blobs). Enable persistent storage in the Space
258
- # settings to keep it across restarts.
259
- RUN mkdir -p /app/data \
260
- && chown -R 1000:1000 /app \
261
- && chown 1000:1000 /usr/local/bin/entrypoint.sh
262
-
263
- # HF Spaces runs containers as uid 1000; the runtime patch needs write access
264
- # to node_modules, hence the chown above.
265
- USER 1000
266
-
267
- ENV PDS_DATA_DIRECTORY=/app/data
268
-
269
- # Disk blobstore - stores uploaded images/videos under /app/data (alongside
270
- # the SQLite db) so everything lives in the persistent volume.
271
- ENV PDS_BLOBSTORE_DISK_LOCATION=/app/data/blobs
272
- ENV PDS_BLOBSTORE_DISK_TMP_LOCATION=/app/data/blobs/tmp
273
-
274
- # HF Spaces route to port 7860
275
- ENV PDS_PORT=7860
276
-
277
- # Registration allowlist (comma-separated domains). The email domain check is
278
- # the only registration gate - no invite codes, no SMTP verification.
279
- ENV PDS_EMAIL_ALLOWED_DOMAINS=rochesterschools.org
280
- ENV PDS_INVITE_REQUIRED=false
281
- ENV PDS_EMAIL_SMTP_URL=
282
- ENV PDS_EMAIL_FROM_ADDRESS=
283
- # Skip email confirmation flow entirely (no SMTP on HF)
284
- ENV PDS_EMAIL_DISABLE_CONFIRMATION_LINK=true
285
-
286
- # Public hostname. Set this to your Space's public URL (as a Space secret or
287
- # variable named PDS_HOSTNAME) so the PDS advertises the right endpoint:
288
- # https://<owner>-<space>.hf.space
289
- # Users' handles will be <name>.<that hostname>.
290
- ENV PDS_HOSTNAME=cloudunity-gemma-api1.hf.space
291
 
 
 
292
  EXPOSE 7860
293
 
294
- ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
 
1
+ FROM python:3.11-slim
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ # Install system dependencies
4
+ RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev && rm -rf /var/lib/apt/lists/*
5
 
6
+ WORKDIR /app
7
 
8
+ # Install Python dependencies
9
+ RUN pip install --no-cache-dir flask flask-cors Pillow litert-lm huggingface_hub
10
 
11
+ # Create dummy Vulkan library to satisfy litert-lm on CPU
12
+ RUN printf 'void vkGetInstanceProcAddr(){}\nvoid vkCreateInstance(){}\nvoid vkDestroyInstance(){}\nvoid vkEnumeratePhysicalDevices(){}\nvoid vkGetDeviceProcAddr(){}' > /tmp/vk.c \
13
+ && gcc -shared -fPIC -o /app/libvulkan.so.1 /tmp/vk.c \
14
+ && rm /tmp/vk.c
 
 
 
 
 
15
 
16
+ # Create model directory
17
+ RUN mkdir -p /app/models/gemma
18
 
19
+ # --- KEY FIX: Updated 'hf download' to fetch the 12B model ---
20
+ RUN hf download litert-community/gemma-4-E2B-it-litert-lm gemma-4-E2B-it.litertlm --local-dir /app/models/gemma
21
+ # Copy the server code
22
+ COPY server.py .
23
 
24
+ # Setup permissions for Hugging Face Spaces (requires user 1000)
25
+ RUN useradd -m -u 1000 user
26
+ RUN chown -R user:user /app
27
+ USER user
28
 
29
+ # Set environment variables
30
+ ENV PORT=7860
31
+ ENV LD_LIBRARY_PATH=/app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ # --- KEY FIX: Updated path to point to the 12B model file ---
34
+ ENV GEMMA_MODEL_PATH=/app/models/gemma/gemma-4-E2B-it.litertlm
35
  EXPOSE 7860
36
 
37
+ CMD ["python3", "server.py"]