forked from cogini/phoenix_container_example_old
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Earthfile
500 lines (372 loc) · 14.1 KB
/
Earthfile
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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# Build Elixir/Phoenix app
# App versions
ARG ELIXIR_VERSION=1.13.1
# ARG OTP_VERSION=23.3.4
ARG OTP_VERSION=24.2
ARG NODE_VERSION=14.4
# ARG ALPINE_VERSION=3.14.3
ARG ALPINE_VERSION=3.15.0
ARG POSTGRES_IMAGE_NAME=postgres
ARG POSTGRES_IMAGE_TAG=14.1-alpine
# ARG CREDO_OPTS="--ignore refactor,duplicated --mute-exit-status"
ARG CREDO_OPTS=""
# Build image
ARG BUILD_IMAGE_NAME=hexpm/elixir
ARG BUILD_IMAGE_TAG=${ELIXIR_VERSION}-erlang-${OTP_VERSION}-alpine-${ALPINE_VERSION}
# Docker-in-Docker host image, used for testing
ARG DIND_IMAGE_NAME=earthly/dind
ARG DIND_IMAGE_TAG=alpine
# Deploy base image
ARG DEPLOY_IMAGE_NAME=alpine
ARG DEPLOY_IMAGE_TAG=$ALPINE_VERSION
# Docker registry for base images, default is docker.io
# If specified, should have a trailing slash
ARG REGISTRY=""
ARG PUBLIC_REGISTRY=""
# Output image
# ARG EARTHLY_GIT_HASH
ARG OUTPUT_IMAGE_NAME=foo-app
ARG IMAGE_TAG=latest
ARG OUTPUT_IMAGE_TAG="$IMAGE_TAG"
ARG REPO_URL="${REGISTRY}${OUTPUT_IMAGE_NAME}"
ARG OUTPUT_URL=$REPO_URL
# By default, packages come from the APK index for the base Alpine image.
# Package versions are consistent between builds, and we normally upgrade by
# upgrading the Alpine version.
ARG APK_UPDATE=":"
ARG APK_UPGRADE=":"
# If a vulnerability is fixed in packages but not yet released in an Alpine base image,
# Then we can run update/upgrade as part of the build.
# ARG APK_UPDATE="apk update"
# ARG APK_UPGRADE="apk upgrade --update-cache -a"
# Elixir release env to build
ARG MIX_ENV=prod
# Name of Elixir release
ARG RELEASE=prod
# This should match mix.exs, e.g.
# defp releases do
# [
# prod: [
# include_executables_for: [:unix],
# ],
# ]
# end
# App name, used to name directories
ARG APP_NAME=app
# OS user that app runs under
ARG APP_USER=app
# OS group that app runs under
ARG APP_GROUP="$APP_USER"
# Dir that app runs under
ARG APP_DIR=/app
# App listen port
ARG APP_PORT=4000
ARG HOME=$APP_DIR
# Build cache dirs
ARG MIX_HOME=/opt/mix
ARG HEX_HOME=/opt/hex
ARG XDG_CACHE_HOME=/opt/cache
# Set a specific LOCALE
ARG LANG=C.UTF-8
# ARG http_proxy
# ARG https_proxy=$http_proxy
ARG RUNTIME_PKGS="ca-certificates shared-mime-info tzdata"
# Left blank, allowing additional packages to be injected
ARG DEV_PKGS=""
# The inner buildkit requires Docker hub creds to prevent rate-limiting issues.
# ARG DOCKERHUB_USER_SECRET
# ARG DOCKERHUB_TOKEN_SECRET
# RUN --secret USERNAME=$DOCKERHUB_USER_SECRET \
# --secret TOKEN=$DOCKERHUB_TOKEN_SECRET \
# if [ "$USERNAME" != "" ]; then \
# docker login --username="$USERNAME" --password="$TOKEN" ;\
# fi
ARG TARGETPLATFORM
ARG USERPLATFORM
# External targets
all-platforms:
BUILD --platform=linux/amd64 --platform=linux/arm64 +all
all:
BUILD +test
BUILD +deploy
BUILD +deploy-scan
# These can also be called individually
test:
BUILD +test-app
# BUILD +test-credo
# BUILD +test-format
# BUILD +test-deps-audit
# BUILD +test-sobelow
# BUILD +test-dialyzer
# Create base build image with OS dependencies
build-os-deps:
FROM ${PUBLIC_REGISTRY}${BUILD_IMAGE_NAME}:${BUILD_IMAGE_TAG}
# See https://wiki.alpinelinux.org/wiki/Local_APK_cache for details
# on the local cache and need for the symlink
RUN --mount=type=cache,target=/var/cache/apk \
$APK_UPDATE && $APK_UPGRADE && \
# Install build tools
# apk add --no-progress alpine-sdk && \
apk add --no-progress git build-base curl && \
apk add --no-progress nodejs npm && \
# apk add --no-progress python3 && \
# Vulnerability checking
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Database command line clients to check if DBs are up when performing integration tests
# RUN apk add --no-progress postgresql-client mysql-client
# RUN apk add --no-progress --no-cache curl gnupg --virtual .build-dependencies -- && \
# curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.2.1-1_amd64.apk && \
# curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.2.1-1_amd64.apk && \
# echo y | apk add --allow-untrusted msodbcsql17_17.5.2.1-1_amd64.apk mssql-tools_17.5.2.1-1_amd64.apk && \
# apk del .build-dependencies && rm -f msodbcsql*.sig mssql-tools*.apk
# ENV PATH="/opt/mssql-tools/bin:${PATH}"
SAVE IMAGE --push ${OUTPUT_URL}:os-deps
# Get app deps
build-deps-get:
FROM +build-os-deps
WORKDIR $APP_DIR
# Get Elixir app deps
COPY --dir config ./
COPY mix.exs mix.lock ./
# Install build tools and get app deps
RUN mix do local.rebar --force, local.hex --force, deps.get
RUN mix esbuild.install
# SAVE ARTIFACT deps /deps
SAVE IMAGE --push ${OUTPUT_URL}:deps
# Compile deps separately from application, allowing it to be cached
test-deps-compile:
FROM +build-deps-get
ENV MIX_ENV=test
WORKDIR $APP_DIR
RUN mix deps.compile
# Base image used for running tests
test-image:
FROM +test-deps-compile
ENV LANG=$LANG
ENV HOME=$APP_DIR
ENV MIX_HOME=$MIX_HOME
ENV HEX_HOME=$HEX_HOME
ENV XDG_CACHE_HOME=$XDG_CACHE_HOME
ENV MIX_ENV=test
WORKDIR $APP_DIR
COPY --if-exists coveralls.json .formatter.exs .credo.exs dialyzer-ignore ./
# Non-umbrella
COPY --dir lib priv test bin ./
RUN mix compile --warnings-as-errors
# Umbrella
# COPY --dir apps priv ./
# For umbrella, using `mix cmd` ensures each app is compiled in
# isolation https://github.com/elixir-lang/elixir/issues/9407
# RUN mix cmd mix compile --warnings-as-errors
# SAVE IMAGE test-image:latest
SAVE IMAGE --push ${OUTPUT_URL}:test
test-dialyzer-plt:
FROM +build-deps-get
ENV MIX_ENV=dev
WORKDIR $APP_DIR
RUN mix dialyzer --plt
SAVE IMAGE --push ${OUTPUT_URL}:dialyzer-plt
test-image-dialyzer:
FROM +test-dialyzer-plt
ENV LANG=$LANG
ENV HOME=$APP_DIR
ENV MIX_HOME=$MIX_HOME
ENV HEX_HOME=$HEX_HOME
ENV XDG_CACHE_HOME=$XDG_CACHE_HOME
ENV MIX_ENV=dev
WORKDIR $APP_DIR
# Non-umbrella
COPY --dir lib priv test bin ./
# Umbrella
# COPY --dir apps ./
SAVE IMAGE test-dializer:latest
# Create database for tests
postgres:
FROM "${PUBLIC_REGISTRY}${POSTGRES_IMAGE_NAME}:${POSTGRES_IMAGE_TAG}"
ENV POSTGRES_USER=postgres
ENV POSTGRES_PASSWORD=postgres
EXPOSE 5432
SAVE IMAGE app-db:latest
# tests:
# FROM earthly/dind:alpine
#
# COPY docker-compose.test.yml ./docker-compose.yml
#
# WITH DOCKER \
# --load test:latest=+test-image \
# --load app-db:latest=+postgres \
# --compose docker-compose.yml
# RUN docker-compose run test mix test && \
# docker-compose run test mix credo && \
# docker-compose run test mix deps.audit && \
# docker-compose run test mix sobelow && \
# docker-compose run test mix dialyzer
# END
# Run app tests in test environment with database
test-app:
FROM ${DIND_IMAGE_NAME}:${DIND_IMAGE_TAG}
COPY docker-compose.test.yml ./docker-compose.yml
WITH DOCKER \
# Image names need to match docker-compose.test.yml
--pull ${PUBLIC_REGISTRY}${POSTGRES_IMAGE_NAME}:${POSTGRES_IMAGE_TAG} \
# --load app-db:latest=+postgres \
--load test:latest=+test-image \
--compose docker-compose.yml
RUN \
docker-compose run test mix ecto.setup && \
docker-compose run test mix test && \
docker-compose run test mix coveralls
END
test-credo:
FROM ${PUBLIC_REGISTRY}${DIND_IMAGE_NAME}:${DIND_IMAGE_TAG}
WITH DOCKER --load test:latest=+test-image
RUN docker run test mix credo ${CREDO_OPTS}
END
test-format:
FROM ${PUBLIC_REGISTRY}${DIND_IMAGE_NAME}:${DIND_IMAGE_TAG}
WITH DOCKER --load test:latest=+test-image
RUN docker run test mix format --check-formatted
END
test-deps-audit:
FROM ${PUBLIC_REGISTRY}${DIND_IMAGE_NAME}:${DIND_IMAGE_TAG}
WITH DOCKER --load test:latest=+test-image
RUN docker run test mix deps.audit
END
test-sobelow:
FROM ${PUBLIC_REGISTRY}${DIND_IMAGE_NAME}:${DIND_IMAGE_TAG}
WITH DOCKER --load test:latest=+test-image
RUN docker run test mix sobelow --exit
END
test-dialyzer:
FROM ${PUBLIC_REGISTRY}${DIND_IMAGE_NAME}:${DIND_IMAGE_TAG}
WITH DOCKER --load test-dialyzer:latest=+test-image-dialyzer
RUN docker run test-dialyzer mix dialyzer --halt-exit-status
END
# Compile deps separately from application, allowing it to be cached
deploy-deps-compile:
FROM +build-deps-get
WORKDIR $APP_DIR
RUN mix deps.compile
# Build Phoenix assets, i.e. JS and CS
deploy-assets-webpack:
FROM +deploy-deps-compile
WORKDIR $APP_DIR
# COPY +deps/deps deps
WORKDIR /app/assets
COPY assets/package.json ./
COPY assets/package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm --prefer-offline --no-audit --progress=false --loglevel=error ci
COPY assets ./
RUN npm run deploy
SAVE ARTIFACT ../priv /priv
SAVE IMAGE --push ${OUTPUT_URL}:assets
# Build Phoenix assets, i.e. JS and CS
deploy-assets-esbuild:
FROM +deploy-deps-compile
WORKDIR $APP_DIR
COPY --dir assets priv ./
RUN mix assets.deploy
SAVE ARTIFACT priv /priv
# Create digested version of assets
deploy-digest:
FROM +deploy-assets-esbuild
# FROM +deploy-deps-compile
# COPY +deploy-assets-esbuild/priv priv
# https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Digest.html
RUN mix phx.digest
# This does a partial compile.
# Doing "mix do compile, phx.digest, release" in a single stage is worse,
# because a change to application code causes a complete recompile.
# With the stages separated most of the compilation is cached.
SAVE IMAGE --push ${OUTPUT_URL}:digest
# SAVE IMAGE --cache-hint
# Create Erlang release
deploy-release:
# FROM +deploy-digest
FROM +deploy-deps-compile
COPY +deploy-assets-esbuild/priv priv
# Non-umbrella
COPY --dir lib rel ./
# Umbrella
# COPY --dir apps ./
RUN mix do compile, release "$RELEASE"
SAVE ARTIFACT "_build/$MIX_ENV/rel/${RELEASE}" /release AS LOCAL "build/release/${RELEASE}"
# SAVE ARTIFACT "_build/$MIX_ENV/rel/${RELEASE}" /release
# SAVE ARTIFACT priv/static /static AS LOCAL build/static
# SAVE ARTIFACT priv/static /static
# Create final deploy image
deploy:
FROM ${REGISTRY}${DEPLOY_IMAGE_NAME}:${DEPLOY_IMAGE_TAG}
# Set environment vars used by the app
# SECRET_KEY_BASE and DATABASE_URL env vars should be set when running the application
# maybe set COOKIE and other things
ENV LANG=$LANG
ENV HOME=$APP_DIR
ENV PORT=$APP_PORT
ENV RELEASE_TMP="/run/$APP_NAME"
ENV RELEASE=${RELEASE}
# Install Alpine runtime libraries
# See https://wiki.alpinelinux.org/wiki/Local_APK_cache for details
# on the local cache and need for the symlink
RUN --mount=type=cache,target=/var/cache/apk \
ln -s /var/cache/apk /etc/apk/cache && \
# Upgrading ensures that we get the latest packages, but makes the build nondeterministic
$APK_UPDATE && $APK_UPGRADE && \
apk add --no-progress $RUNTIME_PACKAGES && \
# https://github.com/krallin/tini
# apk add tini && \
# Make DNS resolution more reliable
# https://github.com/sourcegraph/godockerize/commit/5cf4e6d81720f2551e6a7b2b18c63d1460bbbe4e
# apk add bind-tools && \
# Install openssl, allowing the app to listen on HTTPS.
# May not be needed if HTTPS is handled outside the application, e.g. in load balancer.
apk add openssl ncurses-libs
# Create user and group to run under with specific uid
RUN addgroup -g 10001 -S "$APP_GROUP" && \
adduser -u 10000 -S "$APP_USER" -G "$APP_GROUP" -h "$HOME"
# Create app dirs
RUN mkdir -p "/run/$APP_NAME" && \
# Make dirs writable by app
chown -R "$APP_USER:$APP_GROUP" \
# Needed for RELEASE_TMP
"/run/$APP_NAME"
# USER $APP_USER
# Setting WORKDIR after USER makes directory be owned by the user.
# Setting it before makes it owned by root, which is more secure.
# The app needs to be able to write to a tmp directory on startup, which by
# default is under the release. This can be changed by setting RELEASE_TMP to
# /tmp or, more securely, /run/foo
WORKDIR $APP_DIR
USER $APP_USER
# Chown files while copying. Running "RUN chown -R app:app /app"
# adds an extra layer which is about 10Mb, a huge difference when the
# image for a new phoenix app is around 20Mb.
# TODO: For more security, change specific files to have group read/execute
# permissions while leaving them owned by root
COPY +deploy-release/release ./
EXPOSE $PORT
# "bin" is the directory under the unpacked release, and "prod" is the name of the release
ENTRYPOINT ["bin/prod"]
# ENTRYPOINT ["/sbin/tini", "--", "bin/prod"]
# Run app in foreground
CMD ["start"]
SAVE IMAGE --push ${OUTPUT_URL}:${OUTPUT_IMAGE_TAG}
# Scan for security vulnerabilities in release image
deploy-scan:
FROM +deploy
USER root
RUN --mount=type=cache,target=/var/cache/apk \
apk add curl && \
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Fail build if there are any issues of severity = CRITICAL
# Succeed for issues of severity = HIGH
RUN --mount=type=cache,target=/var/cache/apk \
# --mount=type=cache,target=/root/.cache/trivy \
--mount=type=cache,target=/root/.cache \
trivy filesystem --exit-code 0 --severity HIGH --no-progress / && \
trivy filesystem --exit-code 1 --severity CRITICAL --no-progress /
# Fail build if there are any issues
# trivy filesystem -d --exit-code 1 --no-progress /
# curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin \
# grype -vv --fail-on medium dir:/ \