forked from we-promise/sure
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile.preview
More file actions
385 lines (325 loc) · 14.8 KB
/
Copy pathDockerfile.preview
File metadata and controls
385 lines (325 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# syntax = docker/dockerfile:1
# Preview Dockerfile for Cloudflare Containers
# Includes PostgreSQL and Redis for self-contained development testing
ARG RUBY_VERSION=3.4.9
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base
WORKDIR /rails
# Install base packages including PostgreSQL and Redis servers
RUN apt-get update -qq \
&& apt-get install --no-install-recommends -y \
curl libvips postgresql postgresql-client redis-server libyaml-0-2 procps sudo openssl strace \
&& rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set development environment
ARG BUILD_COMMIT_SHA
ENV RAILS_ENV="development" \
BUNDLE_PATH="/usr/local/bundle" \
BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA}
# Build stage
FROM base AS build
RUN apt-get update -qq \
&& apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config libyaml-dev \
&& rm -rf /var/lib/apt/lists /var/cache/apt/archives
COPY .ruby-version Gemfile Gemfile.lock ./
RUN bundle install \
&& rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git \
&& bundle exec bootsnap precompile --gemfile -j 0
COPY . .
RUN bundle exec bootsnap precompile -j 0 app/ lib/
# Precompile assets
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage
FROM base
# Create rails user and configure PostgreSQL/Redis permissions
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
echo "rails ALL=(ALL) NOPASSWD: /usr/bin/pg_ctlcluster, /usr/bin/redis-server" > /etc/sudoers.d/rails && \
chmod 0440 /etc/sudoers.d/rails
# Configure PostgreSQL to allow local connections. Target the highest-version
# cluster's pg_hba.conf -- the same one the entrypoint starts (ls
# /etc/postgresql | sort -V | tail -1) -- so the trust rules always land on the
# cluster that actually runs, even if multiple majors are present.
RUN PG_VERSION="$(ls /etc/postgresql 2>/dev/null | sort -V | tail -1)" && \
PG_HBA="/etc/postgresql/${PG_VERSION}/main/pg_hba.conf" && \
if [ -z "$PG_VERSION" ] || [ ! -f "$PG_HBA" ]; then \
echo "ERROR: pg_hba.conf not found for cluster '${PG_VERSION:-none}'; cannot configure local trust" >&2; \
exit 1; \
fi && \
echo "local all all trust" > "$PG_HBA" && \
echo "host all all 127.0.0.1/32 trust" >> "$PG_HBA" && \
echo "host all all ::1/128 trust" >> "$PG_HBA"
# Use file-backed dynamic shared memory instead of POSIX /dev/shm.
# Cloudflare Containers provide only a tiny /dev/shm, and PostgreSQL's default
# dynamic_shared_memory_type = posix FATALs on startup there with
# "could not resize shared memory segment ... No space left on device", which
# kills the container before it can serve a port. mmap keeps DSM in the data
# directory ($PGDATA/pg_dynshmem), removing the /dev/shm dependency. Local
# Docker hides this because its default /dev/shm is 64MB.
#
# Select the highest-version cluster's config -- the same one the entrypoint
# starts (ls /etc/postgresql | sort -V | tail -1) -- so the override always
# lands on the cluster that actually runs, even if multiple majors are present.
RUN PG_VERSION="$(ls /etc/postgresql 2>/dev/null | sort -V | tail -1)" && \
PG_CONF="/etc/postgresql/${PG_VERSION}/main/postgresql.conf" && \
if [ -z "$PG_VERSION" ] || [ ! -f "$PG_CONF" ]; then \
echo "ERROR: postgresql.conf not found for cluster '${PG_VERSION:-none}'; cannot disable /dev/shm DSM dependency" >&2; \
exit 1; \
fi && \
sed -i 's/^[[:space:]]*dynamic_shared_memory_type[[:space:]]*=/# &/' "$PG_CONF" && \
printf '\n# Preview: avoid /dev/shm dependency (small in Cloudflare Containers)\ndynamic_shared_memory_type = mmap\n' >> "$PG_CONF" && \
grep -qx 'dynamic_shared_memory_type = mmap' "$PG_CONF" && \
echo "Configured dynamic_shared_memory_type=mmap in $PG_CONF"
# Create database directory with correct permissions
RUN mkdir -p /var/run/postgresql && \
chown -R postgres:postgres /var/run/postgresql && \
chmod 2775 /var/run/postgresql
# Copy built artifacts
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails
# Create preview entrypoint script inline
RUN cat > /rails/bin/preview-entrypoint << 'ENTRYPOINT_EOF'
#!/bin/bash
set -e
cd /rails
# Diagnostics posts are best-effort and must NEVER stall boot: the worker's
# Durable Object can be unresponsive while it waits for this container's port,
# so an unbounded curl here deadlocks startup against the port check.
emit_status_sync() {
if [ -n "$PREVIEW_ORIGIN" ] && [ -n "$PREVIEW_DIAGNOSTICS_NONCE" ]; then
local stage="$1"
local detail="$2"
local payload
payload=$(STAGE="$stage" DETAIL="$detail" ruby -rjson -e 'print JSON.generate({stage: ENV.fetch("STAGE"), detail: ENV.fetch("DETAIL", "")})' 2>/dev/null) || return 0
curl -fsS --connect-timeout 2 --max-time 5 -X POST "$PREVIEW_ORIGIN/_container_event" \
-H 'content-type: application/json' \
-H "x-preview-diagnostics-nonce: $PREVIEW_DIAGNOSTICS_NONCE" \
--data "$payload" >/dev/null || true
fi
}
emit_status() {
emit_status_sync "$1" "$2" &
}
summarize_log_tail() {
local file="$1"
local label="$2"
if [ ! -f "$file" ]; then
printf '%s log unavailable' "$label"
return 0
fi
tail -n 80 "$file" 2>&1 |
sed 's/"/'"'"'/g' |
tr '\n' ' ' |
sed 's/ */ /g' |
cut -c 1-1600
}
fail_preview() {
local detail="$1"
trap - ERR
# Always log to stderr too: the HTTP diagnostics channel can be unreachable
# while the worker's Durable Object is still waiting for this container's
# port, but stderr is captured by Cloudflare container observability, so the
# real reason survives even when the event POST does not.
echo "PREVIEW FAILED: ${detail}" >&2
emit_status_sync failed "$detail"
exit 1
}
postgres_cluster_snapshot() {
local snapshot=""
local cluster_status
local postgres_log
if command -v pg_lsclusters >/dev/null 2>&1; then
cluster_status="$(pg_lsclusters 2>&1 | tr '\n' '|' | sed 's/"/'"'"'/g' | cut -c 1-500)"
snapshot="clusters=${cluster_status}"
fi
postgres_log="/var/log/postgresql/postgresql-${PG_VERSION}-main.log"
if [ -f "$postgres_log" ]; then
snapshot="${snapshot} log=$(summarize_log_tail "$postgres_log" postgres)"
fi
printf '%s' "$snapshot"
}
trap 'emit_status_sync failed "preview-entrypoint failed on line ${LINENO}"' ERR
emit_status boot "preview-entrypoint started"
# Bind :3000 immediately with a tiny placeholder responder. Cloudflare's
# container supervisor only waits ~20s for the port, while the full stack
# (redis, postgres, migrations) needs 60s+ on a basic instance. The
# placeholder answers 503 with a meta-refresh; the worker still gates
# previewReady on the real Rails /up probe and sample data, so readiness
# semantics are unchanged. It is replaced by the real server below.
ruby -rsocket -e '
server = TCPServer.new("0.0.0.0", 3000)
body = "<!doctype html><html><head><meta http-equiv=\"refresh\" content=\"3\"><title>Sure preview warming</title></head><body>Sure preview is booting (database setup in progress). This page refreshes automatically.</body></html>"
loop do
client = begin
server.accept
rescue StandardError
next
end
begin
# Never read from the client: the response is static, and a blocking read
# would let one silent connection (e.g. a bare TCP port probe) wedge this
# single-threaded loop and starve every later probe.
client.write("HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: #{body.bytesize}\r\nRetry-After: 3\r\nConnection: close\r\n\r\n#{body}")
rescue StandardError
ensure
begin
client.close
rescue StandardError
end
end
end
' >/tmp/port-placeholder.log 2>&1 &
PLACEHOLDER_PID=$!
# Confirm the placeholder actually bound :3000 before reporting it. A plain
# TCP connect is used because the placeholder intentionally answers 503.
PLACEHOLDER_BOUND=0
for i in {1..10}; do
if (exec 3<>/dev/tcp/127.0.0.1/3000) 2>/dev/null; then
PLACEHOLDER_BOUND=1
break
fi
if ! kill -0 "$PLACEHOLDER_PID" 2>/dev/null; then
break
fi
sleep 0.2
done
if [ "$PLACEHOLDER_BOUND" -eq 1 ]; then
emit_status port-placeholder "bound :3000 placeholder responder (pid ${PLACEHOLDER_PID})"
else
echo "Warning: :3000 placeholder did not come up; continuing boot" >&2
emit_status port-placeholder-missing "placeholder responder failed to bind :3000: $(summarize_log_tail /tmp/port-placeholder.log port-placeholder)"
fi
REDIS_READY=0
POSTGRES_READY=0
# Start Redis
echo "Starting Redis..."
emit_status redis-start "starting redis"
sudo redis-server --daemonize yes --bind 127.0.0.1
# Wait for Redis to be ready
echo "Waiting for Redis to be ready..."
for i in {1..10}; do
if redis-cli ping > /dev/null 2>&1; then
echo "Redis is ready"
emit_status redis-ready "redis is ready"
REDIS_READY=1
break
fi
sleep 1
done
if [ "$REDIS_READY" -ne 1 ]; then
fail_preview "redis did not become ready in time"
fi
# Start PostgreSQL
echo "Starting PostgreSQL..."
emit_status postgres-start "starting postgres"
PG_VERSION=$(ls /etc/postgresql/ | sort -V | tail -1)
if [ -z "$PG_VERSION" ]; then
fail_preview "could not determine installed PostgreSQL version"
fi
if sudo pg_ctlcluster --skip-systemctl-redirect "$PG_VERSION" main status > /dev/null 2>&1; then
emit_status postgres-already-running "postgres cluster already running"
else
POSTGRES_START_LOG=/tmp/postgres-start.log
if ! sudo pg_ctlcluster --skip-systemctl-redirect "$PG_VERSION" main start >"$POSTGRES_START_LOG" 2>&1; then
fail_preview "pg_ctlcluster start failed: $(summarize_log_tail "$POSTGRES_START_LOG" pg_ctlcluster-start) | $(postgres_cluster_snapshot)"
fi
fi
# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL to be ready..."
for i in {1..30}; do
if pg_isready -h localhost -U postgres > /dev/null 2>&1; then
echo "PostgreSQL is ready"
emit_status postgres-ready "postgres is ready"
POSTGRES_READY=1
break
fi
sleep 1
done
if [ "$POSTGRES_READY" -ne 1 ]; then
fail_preview "postgres did not become ready in time: $(postgres_cluster_snapshot)"
fi
# Create database user and database if they don't exist
echo "Setting up database..."
emit_status db-setup "setting up database"
psql -h localhost -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='rails'" | grep -q 1 || \
psql -h localhost -U postgres -c "CREATE USER rails WITH SUPERUSER PASSWORD 'rails';"
psql -h localhost -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='sure_development'" | grep -q 1 || \
psql -h localhost -U postgres -c "CREATE DATABASE sure_development OWNER rails;"
# Set DATABASE_URL if not already set
export DATABASE_URL="${DATABASE_URL:-postgres://rails:rails@localhost:5432/sure_development}"
# Set REDIS_URL if not already set
export REDIS_URL="${REDIS_URL:-redis://localhost:6379/0}"
# Generate SECRET_KEY_BASE if not set
export SECRET_KEY_BASE="${SECRET_KEY_BASE:-$(openssl rand -hex 64)}"
# Run database migrations
echo "Running database migrations..."
emit_status db-prepare "running rails db:prepare"
/rails/bin/rails db:prepare
emit_status db-prepare-done "rails db:prepare finished"
# Defer all demo-data creation until after Rails is up so preview can boot first
echo "Checking demo dataset..."
emit_status demo-data-check "checking for default demo user"
DEMO_EMAIL="${DEMO_USER_EMAIL:-user@example.com}"
DEMO_EMAIL_SQL=${DEMO_EMAIL//\'/\'\'}
DEMO_SEED="${DEMO_DATA_SEED:-880}"
DEMO_HAS_USER=0
DEMO_HAS_DATA=0
if psql "$DATABASE_URL" -tAc "SELECT 1 FROM users WHERE email = '${DEMO_EMAIL_SQL}' LIMIT 1" | grep -q 1; then
DEMO_HAS_USER=1
emit_status demo-data-user-present "default demo user already exists"
fi
if psql "$DATABASE_URL" -tAc "SELECT 1 FROM accounts a JOIN users u ON u.family_id = a.family_id WHERE u.email = '${DEMO_EMAIL_SQL}' LIMIT 1" | grep -q 1; then
DEMO_HAS_DATA=1
emit_status demo-data-skip "demo financial data already exists"
else
emit_status demo-data-deferred "deferring demo data creation until after rails boot"
fi
# Release :3000 for the real server. The brief listener gap is handled by the
# worker, which catches containerFetch errors and serves its wait page.
kill "$PLACEHOLDER_PID" 2>/dev/null || true
wait "$PLACEHOLDER_PID" 2>/dev/null || true
# Execute the main command with an internal readiness probe
echo "Starting Rails server..."
emit_status rails-start "starting rails server"
"$@" > /tmp/rails.log 2>&1 &
RAILS_PID=$!
for i in {1..180}; do
if curl -fsS http://127.0.0.1:3000/up > /dev/null 2>&1; then
emit_status rails-up-ready "rails responded on localhost:3000/up"
if [ "$DEMO_HAS_USER" -ne 1 ] || [ "$DEMO_HAS_DATA" -ne 1 ]; then
emit_status demo-data-load "creating/backfilling demo dataset in background (seed=${DEMO_SEED})"
(
(
DEMO_USER_EMAIL="$DEMO_EMAIL" DEMO_DATA_SEED="$DEMO_SEED" /rails/bin/rails runner '
email = ENV.fetch("DEMO_USER_EMAIL")
generator = Demo::Generator.new(seed: ENV.fetch("DEMO_DATA_SEED"))
user = User.find_by(email: email)
unless user
generator.generate_empty_data!(skip_clear: true)
user = User.find_by!(email: email)
end
has_accounts = user.family.accounts.exists?
generator.generate_new_user_data_for!(user.family, email: user.email) unless has_accounts
'
) > /tmp/demo-data.log 2>&1 && \
emit_status demo-data-ready "default demo dataset loaded in background" || \
emit_status demo-data-failed "background demo dataset load failed: $(summarize_log_tail /tmp/demo-data.log demo-data)"
) &
fi
break
fi
sleep 1
done
if ! curl -fsS http://127.0.0.1:3000/up > /dev/null 2>&1; then
emit_status rails-up-timeout "rails did not answer localhost:3000/up in time"
emit_status rails-process-status "$(ps -o pid=,ppid=,stat=,comm=,args= -p "$RAILS_PID" 2>/dev/null | tr -s ' ' | sed 's/^ //')"
emit_status rails-process-wchan "$(cat /proc/$RAILS_PID/wchan 2>/dev/null | tr '\n' ' ' | cut -c 1-200)"
emit_status rails-process-children "$(ps -o pid=,ppid=,stat=,comm=,args= --ppid "$RAILS_PID" 2>/dev/null | tail -n +2 | tr '\n' '|' | cut -c 1-600)"
emit_status rails-socket-state "$(ruby -e 'hex="0BB8"; rows=File.readlines("/proc/net/tcp")+File.readlines("/proc/net/tcp6"); hits=rows.select{|l| l.include?(":#{hex} ")}.map{|l| l.strip.split[3] rescue nil}.compact; puts(hits.empty? ? "no-listener" : hits.join(","))' 2>&1 | tr '\n' ' ' | cut -c 1-400)"
emit_status rails-log-tail "$(tail -n 40 /tmp/rails.log 2>&1 | sed 's/"/'"'"'/g' | tr '\n' ' ' | cut -c 1-1200)"
fi
wait "$RAILS_PID"
ENTRYPOINT_EOF
RUN chmod 755 /rails/bin/preview-entrypoint && chown rails:rails /rails/bin/preview-entrypoint
USER 1000:1000
ENTRYPOINT ["/rails/bin/preview-entrypoint"]
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]