add Docker deployment with Alpine image, release config and health check
- Alpine multi-stage Dockerfile (131 MB image) - Release overlays (bin/server, bin/migrate), env.sh, Release module - Health check endpoint at GET /health - Fly.io config with SQLite volume mount - Fix hardcoded paths in optimizer.ex and variant_cache.ex to use Application.app_dir/2 (breaks in releases where Plug.Static serves from a different directory than CWD) - strip_beams: true in release config - Optimised .dockerignore and .gitignore for mockup variants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eaa4bbb3fa
commit
1ee37c853d
52
.dockerignore
Normal file
52
.dockerignore
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Build artifacts
|
||||||
|
_build/
|
||||||
|
deps/
|
||||||
|
.elixir_ls/
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Test and dev
|
||||||
|
test/
|
||||||
|
.formatter.exs
|
||||||
|
.credo.exs
|
||||||
|
.dialyzer_ignore.exs
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# CI/docs
|
||||||
|
CLAUDE.md
|
||||||
|
PROGRESS.md
|
||||||
|
ROADMAP.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Generated image variants (regenerated on startup from DB)
|
||||||
|
priv/static/image_cache/
|
||||||
|
|
||||||
|
# Mockup variants, digested copies, and converted formats.
|
||||||
|
# Only the source .webp files (no size suffix) should enter the build context —
|
||||||
|
# phx.digest creates its own hash-suffixed copies inside the builder.
|
||||||
|
priv/static/mockups/*-400*
|
||||||
|
priv/static/mockups/*-800*
|
||||||
|
priv/static/mockups/*-1200*
|
||||||
|
priv/static/mockups/*-thumb*
|
||||||
|
priv/static/mockups/*.avif
|
||||||
|
priv/static/mockups/*.jpg
|
||||||
|
priv/static/mockups/*.gz
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@ -49,13 +49,15 @@ simpleshop_theme-*.tar
|
|||||||
/priv/static/images/*-*.svg.gz
|
/priv/static/images/*-*.svg.gz
|
||||||
/priv/static/images/*.gz
|
/priv/static/images/*.gz
|
||||||
|
|
||||||
# Generated mockup variants (auto-generated on startup via Oban)
|
# Generated mockup variants, digested copies, and converted formats.
|
||||||
# Source .webp files are tracked, variants are regenerated
|
# Only source .webp files (no size suffix) are tracked — everything else regenerates.
|
||||||
/priv/static/mockups/*-400.*
|
/priv/static/mockups/*-400*
|
||||||
/priv/static/mockups/*-800.*
|
/priv/static/mockups/*-800*
|
||||||
/priv/static/mockups/*-1200.*
|
/priv/static/mockups/*-1200*
|
||||||
/priv/static/mockups/*-thumb.*
|
/priv/static/mockups/*-thumb*
|
||||||
# Digested versions of mockup variants and sources (from mix phx.digest)
|
/priv/static/mockups/*.avif
|
||||||
|
/priv/static/mockups/*.jpg
|
||||||
|
/priv/static/mockups/*.gz
|
||||||
/priv/static/mockups/*-????????????????????????????????.*
|
/priv/static/mockups/*-????????????????????????????????.*
|
||||||
|
|
||||||
# Generated image variants cache (regenerated from source images)
|
# Generated image variants cache (regenerated from source images)
|
||||||
|
|||||||
113
Dockerfile
Normal file
113
Dockerfile
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Lean Alpine-based image for SimpleshopTheme.
|
||||||
|
#
|
||||||
|
# Builder and runner both use Alpine (musl). The vix NIF is compiled against
|
||||||
|
# Alpine's system libvips instead of the glibc-linked precompiled binary.
|
||||||
|
#
|
||||||
|
# Build: docker build -t simpleshop_theme .
|
||||||
|
# Run: docker run --rm -p 4000:4000 \
|
||||||
|
# -e SECRET_KEY_BASE=$(mix phx.gen.secret) \
|
||||||
|
# -e DATABASE_PATH=/data/simpleshop_theme.db \
|
||||||
|
# -e PHX_HOST=localhost \
|
||||||
|
# -v simpleshop_data:/data \
|
||||||
|
# simpleshop_theme
|
||||||
|
#
|
||||||
|
# Images:
|
||||||
|
# https://hub.docker.com/r/hexpm/elixir/tags?name=alpine
|
||||||
|
# https://hub.docker.com/_/alpine
|
||||||
|
|
||||||
|
ARG ELIXIR_VERSION=1.19.5
|
||||||
|
ARG OTP_VERSION=28.2
|
||||||
|
ARG ALPINE_VERSION=3.23.3
|
||||||
|
|
||||||
|
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-alpine-${ALPINE_VERSION}"
|
||||||
|
ARG RUNNER_IMAGE="alpine:${ALPINE_VERSION}"
|
||||||
|
|
||||||
|
# --- Builder ---
|
||||||
|
|
||||||
|
FROM ${BUILDER_IMAGE} AS builder
|
||||||
|
|
||||||
|
# Build deps: C compiler for NIFs, git for hex deps, vips-dev for image processing
|
||||||
|
RUN apk add --no-cache build-base git vips-dev
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN mix local.hex --force && mix local.rebar --force
|
||||||
|
|
||||||
|
ENV MIX_ENV="prod"
|
||||||
|
|
||||||
|
# Use Alpine's system libvips instead of the glibc-linked precompiled binary
|
||||||
|
ENV VIX_COMPILATION_MODE="PLATFORM_PROVIDED_LIBVIPS"
|
||||||
|
|
||||||
|
# Install mix dependencies
|
||||||
|
COPY mix.exs mix.lock ./
|
||||||
|
RUN mix deps.get --only $MIX_ENV
|
||||||
|
RUN mkdir config
|
||||||
|
|
||||||
|
# Compile dependencies (config needed first)
|
||||||
|
COPY config/config.exs config/${MIX_ENV}.exs config/
|
||||||
|
RUN mix deps.compile
|
||||||
|
|
||||||
|
# Install tailwind and esbuild CLI tools
|
||||||
|
RUN mix assets.setup
|
||||||
|
|
||||||
|
# Copy priv in layers ordered by change frequency.
|
||||||
|
# Mockups/fonts are stable; migrations and seeds change more often.
|
||||||
|
COPY priv/static/mockups priv/static/mockups
|
||||||
|
COPY priv/static/fonts priv/static/fonts
|
||||||
|
COPY priv/static/images priv/static/images
|
||||||
|
COPY priv/static/favicon.ico priv/static/robots.txt priv/static/
|
||||||
|
COPY priv/repo priv/repo
|
||||||
|
COPY priv/gettext priv/gettext
|
||||||
|
|
||||||
|
# Application code and frontend assets (change most often)
|
||||||
|
COPY lib lib
|
||||||
|
COPY assets assets
|
||||||
|
|
||||||
|
# Compile the application
|
||||||
|
RUN mix compile
|
||||||
|
|
||||||
|
# Build assets (both tailwind profiles + esbuild + phx.digest)
|
||||||
|
RUN mix assets.deploy
|
||||||
|
|
||||||
|
# Remove gzipped copies of mockup images — WebP is already compressed,
|
||||||
|
# gzip saves ~0% and just wastes space in the release
|
||||||
|
RUN find priv/static/mockups -name "*.gz" -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# Runtime config doesn't need recompilation
|
||||||
|
COPY config/runtime.exs config/
|
||||||
|
|
||||||
|
# Release overlay scripts
|
||||||
|
COPY rel rel
|
||||||
|
|
||||||
|
# Build the release
|
||||||
|
RUN mix release
|
||||||
|
|
||||||
|
# --- Runner ---
|
||||||
|
|
||||||
|
FROM ${RUNNER_IMAGE} AS runner
|
||||||
|
|
||||||
|
# Runtime deps only — no compilers, no -dev packages
|
||||||
|
RUN apk add --no-cache libstdc++ openssl ncurses-libs vips
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Non-root user for security
|
||||||
|
RUN adduser -D -h /app appuser
|
||||||
|
RUN chown appuser:appuser /app
|
||||||
|
|
||||||
|
# Prepare the data directory (for SQLite database)
|
||||||
|
RUN mkdir -p /data && chown appuser:appuser /data
|
||||||
|
|
||||||
|
ENV MIX_ENV="prod"
|
||||||
|
|
||||||
|
# Copy the release from the builder
|
||||||
|
COPY --from=builder --chown=appuser:appuser /app/_build/${MIX_ENV}/rel/simpleshop_theme ./
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Ensure /data is the default database location
|
||||||
|
ENV DATABASE_PATH="/data/simpleshop_theme.db"
|
||||||
|
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
CMD ["/app/bin/server"]
|
||||||
@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
### Tier 2 — Production readiness (can deploy and run reliably)
|
### Tier 2 — Production readiness (can deploy and run reliably)
|
||||||
|
|
||||||
5. **Hosting & deployment** — Fly.io or similar deployment config. Release configuration, runtime env setup, health checks. Observability: structured logging, error tracking (Sentry or similar), basic metrics.
|
5. **Hosting & deployment** — In progress. Alpine Docker image (131 MB), Fly.io config, release overlays, health check endpoint, hardcoded path fixes for releases. Still to do: observability (structured logging, error tracking, basic metrics).
|
||||||
6. **Litestream / SQLite replication** — Litestream for continuous SQLite backup to S3-compatible storage. Point-in-time recovery. Simple sidecar process, no code changes needed, works with vanilla SQLite. For the hosted platform (Tier 5), evaluate [Turso](https://turso.tech/) (libSQL fork of SQLite) with embedded read replicas via [ecto_libsql](https://github.com/ocean/ecto_libsql) adapter — gives multi-node reads without a separate replication daemon, but adds a dependency on the libSQL fork.
|
6. **Litestream / SQLite replication** — Litestream for continuous SQLite backup to S3-compatible storage. Point-in-time recovery. Simple sidecar process, no code changes needed, works with vanilla SQLite. For the hosted platform (Tier 5), evaluate [Turso](https://turso.tech/) (libSQL fork of SQLite) with embedded read replicas via [ecto_libsql](https://github.com/ocean/ecto_libsql) adapter — gives multi-node reads without a separate replication daemon, but adds a dependency on the libSQL fork.
|
||||||
7. ~~**CI pipeline**~~ — ✅ Complete. `mix ci` alias: compile --warning-as-errors, deps.unlock --unused, format --check-formatted, credo, dialyzer, test. Credo configured with sensible defaults. Dialyzer with ignore file for false positives (Stripe types, Mix tasks, ExUnit internals). 612 tests, 0 failures.
|
7. ~~**CI pipeline**~~ — ✅ Complete. `mix ci` alias: compile --warning-as-errors, deps.unlock --unused, format --check-formatted, credo, dialyzer, test. Credo configured with sensible defaults. Dialyzer with ignore file for false positives (Stripe types, Mix tasks, ExUnit internals). 612 tests, 0 failures.
|
||||||
8. **PageSpeed in CI** — Lighthouse CI to catch regressions. Fail the build if score drops below threshold. Protects the current 100% score.
|
8. **PageSpeed in CI** — Lighthouse CI to catch regressions. Fail the build if score drops below threshold. Protects the current 100% score.
|
||||||
@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
17. **Hosted platform** — Marketing/brochure site for SimpleShop as a service. Subscribe/sign-up flow. Multi-tenancy with per-tenant databases. Stripe Connect for customer shops (each merchant connects their own Stripe account via OAuth).
|
17. **Hosted platform** — Marketing/brochure site for SimpleShop as a service. Subscribe/sign-up flow. Multi-tenancy with per-tenant databases. Stripe Connect for customer shops (each merchant connects their own Stripe account via OAuth).
|
||||||
18. **Migration & export** — Let shop owners export their data (products, orders, customers, theme settings). Import from other platforms (Shopify, WooCommerce). Portable data as a selling point for the self-hosted story.
|
18. **Migration & export** — Let shop owners export their data (products, orders, customers, theme settings). Import from other platforms (Shopify, WooCommerce). Portable data as a selling point for the self-hosted story.
|
||||||
19. **Internationalisation (i18n)** — Multi-language support via Gettext (already in Phoenix). Currency formatting (ex_cldr already in deps). RTL layout support. Per-shop locale configuration.
|
19. **Internationalisation (i18n)** — Multi-language support via Gettext (already in Phoenix). Currency formatting. RTL layout support. Per-shop locale configuration. **Note:** `ex_money`/`ex_cldr` are currently used *only* for `Cart.format_price/1` (a single GBP formatting call) but add ~13 MB to the release (ex_cldr 9.5 MB, digital_token 3.7 MB, ex_cldr_numbers, ex_cldr_currencies). Consider replacing with a simple `format_price/2` function that handles GBP/EUR/USD directly — all three use 2 decimal places and are trivial to format. Re-add `ex_money` later if proper locale-aware number formatting is needed (e.g., German `12.345,67 €`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ if config_env() == :prod do
|
|||||||
System.get_env("DATABASE_PATH") ||
|
System.get_env("DATABASE_PATH") ||
|
||||||
raise """
|
raise """
|
||||||
environment variable DATABASE_PATH is missing.
|
environment variable DATABASE_PATH is missing.
|
||||||
For example: /etc/simpleshop_theme/simpleshop_theme.db
|
For example: /data/simpleshop_theme.db
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config :simpleshop_theme, SimpleshopTheme.Repo,
|
config :simpleshop_theme, SimpleshopTheme.Repo,
|
||||||
@ -97,22 +97,32 @@ if config_env() == :prod do
|
|||||||
|
|
||||||
# ## Configuring the mailer
|
# ## Configuring the mailer
|
||||||
#
|
#
|
||||||
# In production you need to configure the mailer to use a different adapter.
|
# Uncomment ONE of the blocks below and set the matching env vars.
|
||||||
# Here is an example configuration for Mailgun:
|
# The API client is already configured in config/prod.exs (Swoosh.ApiClient.Req).
|
||||||
|
#
|
||||||
|
# Postmark (recommended):
|
||||||
|
#
|
||||||
|
# config :simpleshop_theme, SimpleshopTheme.Mailer,
|
||||||
|
# adapter: Swoosh.Adapters.Postmark,
|
||||||
|
# api_key: System.get_env("POSTMARK_API_KEY")
|
||||||
|
#
|
||||||
|
# Mailgun:
|
||||||
#
|
#
|
||||||
# config :simpleshop_theme, SimpleshopTheme.Mailer,
|
# config :simpleshop_theme, SimpleshopTheme.Mailer,
|
||||||
# adapter: Swoosh.Adapters.Mailgun,
|
# adapter: Swoosh.Adapters.Mailgun,
|
||||||
# api_key: System.get_env("MAILGUN_API_KEY"),
|
# api_key: System.get_env("MAILGUN_API_KEY"),
|
||||||
# domain: System.get_env("MAILGUN_DOMAIN")
|
# domain: System.get_env("MAILGUN_DOMAIN")
|
||||||
#
|
#
|
||||||
# Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney,
|
# SMTP (any provider):
|
||||||
# and Finch out-of-the-box. This configuration is typically done at
|
|
||||||
# compile-time in your config/prod.exs:
|
|
||||||
#
|
#
|
||||||
# config :swoosh, :api_client, Swoosh.ApiClient.Req
|
# config :simpleshop_theme, SimpleshopTheme.Mailer,
|
||||||
#
|
# adapter: Swoosh.Adapters.SMTP,
|
||||||
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
|
# relay: System.get_env("SMTP_HOST"),
|
||||||
|
# port: String.to_integer(System.get_env("SMTP_PORT") || "587"),
|
||||||
|
# username: System.get_env("SMTP_USERNAME"),
|
||||||
|
# password: System.get_env("SMTP_PASSWORD"),
|
||||||
|
# tls: :if_available
|
||||||
|
|
||||||
# Stripe keys are stored encrypted in the database and loaded at runtime
|
# Stripe and Printify keys are stored encrypted in the database and loaded
|
||||||
# by SimpleshopTheme.Secrets. No env vars needed.
|
# at runtime by SimpleshopTheme.Secrets. No env vars needed for those.
|
||||||
end
|
end
|
||||||
|
|||||||
50
fly.toml
Normal file
50
fly.toml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# fly.toml — Fly.io deployment configuration
|
||||||
|
#
|
||||||
|
# Getting started:
|
||||||
|
# 1. Install flyctl: curl -L https://fly.io/install.sh | sh
|
||||||
|
# 2. Sign up / log in: fly auth login
|
||||||
|
# 3. Create the app: fly launch --no-deploy
|
||||||
|
# 4. Create a volume: fly volumes create simpleshop_data --size 1
|
||||||
|
# 5. Set secrets:
|
||||||
|
# fly secrets set SECRET_KEY_BASE=$(mix phx.gen.secret)
|
||||||
|
# fly secrets set PHX_HOST=your-domain.com
|
||||||
|
# 6. Deploy: fly deploy
|
||||||
|
# 7. Open: fly open
|
||||||
|
|
||||||
|
app = "simpleshop-theme"
|
||||||
|
primary_region = "lhr"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
|
||||||
|
# SQLite needs a persistent volume — without this, your database is lost on every deploy.
|
||||||
|
[mounts]
|
||||||
|
source = "simpleshop_data"
|
||||||
|
destination = "/data"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
PHX_SERVER = "true"
|
||||||
|
DATABASE_PATH = "/data/simpleshop_theme.db"
|
||||||
|
|
||||||
|
[http_service]
|
||||||
|
internal_port = 4000
|
||||||
|
force_https = true
|
||||||
|
auto_stop_machines = "stop"
|
||||||
|
auto_start_machines = true
|
||||||
|
min_machines_running = 0
|
||||||
|
|
||||||
|
[http_service.concurrency]
|
||||||
|
type = "connections"
|
||||||
|
hard_limit = 1000
|
||||||
|
soft_limit = 1000
|
||||||
|
|
||||||
|
[[http_service.checks]]
|
||||||
|
grace_period = "30s"
|
||||||
|
interval = "15s"
|
||||||
|
method = "GET"
|
||||||
|
timeout = "5s"
|
||||||
|
path = "/health"
|
||||||
|
|
||||||
|
[[vm]]
|
||||||
|
size = "shared-cpu-1x"
|
||||||
|
memory = "1gb"
|
||||||
@ -13,11 +13,10 @@ defmodule SimpleshopTheme.Images.Optimizer do
|
|||||||
# Only affects <5% of users (legacy browsers without AVIF/WebP support)
|
# Only affects <5% of users (legacy browsers without AVIF/WebP support)
|
||||||
@pregenerated_formats [:avif, :webp]
|
@pregenerated_formats [:avif, :webp]
|
||||||
@thumb_size 200
|
@thumb_size 200
|
||||||
@cache_dir "priv/static/image_cache"
|
|
||||||
@max_stored_width 2000
|
@max_stored_width 2000
|
||||||
@storage_quality 90
|
@storage_quality 90
|
||||||
|
|
||||||
def cache_dir, do: @cache_dir
|
def cache_dir, do: Application.app_dir(:simpleshop_theme, "priv/static/image_cache")
|
||||||
def all_widths, do: @all_widths
|
def all_widths, do: @all_widths
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@ -72,7 +71,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
|
|||||||
{:ok, :svg_skipped}
|
{:ok, :svg_skipped}
|
||||||
|
|
||||||
%{data: data, source_width: width} = image ->
|
%{data: data, source_width: width} = image ->
|
||||||
File.mkdir_p!(@cache_dir)
|
File.mkdir_p!(cache_dir())
|
||||||
|
|
||||||
with {:ok, vips_image} <- Image.from_binary(data) do
|
with {:ok, vips_image} <- Image.from_binary(data) do
|
||||||
widths = applicable_widths(width)
|
widths = applicable_widths(width)
|
||||||
@ -93,7 +92,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp generate_thumbnail(image, id) do
|
defp generate_thumbnail(image, id) do
|
||||||
path = Path.join(@cache_dir, "#{id}-thumb.jpg")
|
path = Path.join(cache_dir(), "#{id}-thumb.jpg")
|
||||||
|
|
||||||
return_if_exists(path, fn ->
|
return_if_exists(path, fn ->
|
||||||
with {:ok, thumb} <- Image.thumbnail(image, @thumb_size),
|
with {:ok, thumb} <- Image.thumbnail(image, @thumb_size),
|
||||||
@ -104,7 +103,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp generate_variant(image, id, width, format) do
|
defp generate_variant(image, id, width, format) do
|
||||||
path = Path.join(@cache_dir, "#{id}-#{width}.#{format_ext(format)}")
|
path = Path.join(cache_dir(), "#{id}-#{width}.#{format_ext(format)}")
|
||||||
|
|
||||||
return_if_exists(path, fn ->
|
return_if_exists(path, fn ->
|
||||||
with {:ok, resized} <- Image.thumbnail(image, width),
|
with {:ok, resized} <- Image.thumbnail(image, width),
|
||||||
@ -140,12 +139,12 @@ defmodule SimpleshopTheme.Images.Optimizer do
|
|||||||
"""
|
"""
|
||||||
def disk_variants_exist?(image_id, source_width) do
|
def disk_variants_exist?(image_id, source_width) do
|
||||||
widths = applicable_widths(source_width)
|
widths = applicable_widths(source_width)
|
||||||
thumb = File.exists?(Path.join(@cache_dir, "#{image_id}-thumb.jpg"))
|
thumb = File.exists?(Path.join(cache_dir(), "#{image_id}-thumb.jpg"))
|
||||||
|
|
||||||
variants =
|
variants =
|
||||||
Enum.all?(widths, fn w ->
|
Enum.all?(widths, fn w ->
|
||||||
Enum.all?(@pregenerated_formats, fn fmt ->
|
Enum.all?(@pregenerated_formats, fn fmt ->
|
||||||
File.exists?(Path.join(@cache_dir, "#{image_id}-#{w}.#{format_ext(fmt)}"))
|
File.exists?(Path.join(cache_dir(), "#{image_id}-#{w}.#{format_ext(fmt)}"))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@ -159,12 +158,12 @@ defmodule SimpleshopTheme.Images.Optimizer do
|
|||||||
"""
|
"""
|
||||||
def generate_variant_on_demand(image_data, image_id, width, format)
|
def generate_variant_on_demand(image_data, image_id, width, format)
|
||||||
when is_binary(image_data) and format in [:avif, :webp, :jpg] do
|
when is_binary(image_data) and format in [:avif, :webp, :jpg] do
|
||||||
path = Path.join(@cache_dir, "#{image_id}-#{width}.#{format_ext(format)}")
|
path = Path.join(cache_dir(), "#{image_id}-#{width}.#{format_ext(format)}")
|
||||||
|
|
||||||
if File.exists?(path) do
|
if File.exists?(path) do
|
||||||
{:ok, path}
|
{:ok, path}
|
||||||
else
|
else
|
||||||
File.mkdir_p!(@cache_dir)
|
File.mkdir_p!(cache_dir())
|
||||||
|
|
||||||
with {:ok, vips_image} <- Image.from_binary(image_data),
|
with {:ok, vips_image} <- Image.from_binary(image_data),
|
||||||
{:ok, resized} <- Image.thumbnail(vips_image, width),
|
{:ok, resized} <- Image.thumbnail(vips_image, width),
|
||||||
|
|||||||
@ -19,7 +19,7 @@ defmodule SimpleshopTheme.Images.VariantCache do
|
|||||||
alias SimpleshopTheme.Sync.ImageDownloadWorker
|
alias SimpleshopTheme.Sync.ImageDownloadWorker
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@mockup_dir "priv/static/mockups"
|
defp mockup_dir, do: Application.app_dir(:simpleshop_theme, "priv/static/mockups")
|
||||||
|
|
||||||
def start_link(opts) do
|
def start_link(opts) do
|
||||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||||
@ -87,9 +87,9 @@ defmodule SimpleshopTheme.Images.VariantCache do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_mockup_variants do
|
defp ensure_mockup_variants do
|
||||||
if File.dir?(@mockup_dir) do
|
if File.dir?(mockup_dir()) do
|
||||||
sources =
|
sources =
|
||||||
Path.wildcard(Path.join(@mockup_dir, "*.webp"))
|
Path.wildcard(Path.join(mockup_dir(), "*.webp"))
|
||||||
|> Enum.reject(&is_variant?/1)
|
|> Enum.reject(&is_variant?/1)
|
||||||
|
|
||||||
missing = Enum.reject(sources, &mockup_variants_exist?/1)
|
missing = Enum.reject(sources, &mockup_variants_exist?/1)
|
||||||
|
|||||||
32
lib/simpleshop_theme/release.ex
Normal file
32
lib/simpleshop_theme/release.ex
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
defmodule SimpleshopTheme.Release do
|
||||||
|
@moduledoc """
|
||||||
|
Release tasks that can be run via `bin/migrate` or `bin/simpleshop_theme eval`.
|
||||||
|
|
||||||
|
Migrations run automatically on startup (see Application), so this is mainly
|
||||||
|
useful as a standalone tool for debugging or manual recovery.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app :simpleshop_theme
|
||||||
|
|
||||||
|
def migrate do
|
||||||
|
load_app()
|
||||||
|
|
||||||
|
for repo <- repos() do
|
||||||
|
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rollback(repo, version) do
|
||||||
|
load_app()
|
||||||
|
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp repos do
|
||||||
|
Application.fetch_env!(@app, :ecto_repos)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_app do
|
||||||
|
Application.ensure_all_started(:ssl)
|
||||||
|
Application.load(@app)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
defmodule SimpleshopThemeWeb.HealthController do
|
||||||
|
use SimpleshopThemeWeb, :controller
|
||||||
|
|
||||||
|
def show(conn, _params) do
|
||||||
|
json(conn, %{status: "ok"})
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -52,6 +52,13 @@ defmodule SimpleshopThemeWeb.Router do
|
|||||||
post "/checkout", CheckoutController, :create
|
post "/checkout", CheckoutController, :create
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Health check (no auth, no theme loading — for load balancers and uptime monitors)
|
||||||
|
scope "/", SimpleshopThemeWeb do
|
||||||
|
pipe_through [:api]
|
||||||
|
|
||||||
|
get "/health", HealthController, :show
|
||||||
|
end
|
||||||
|
|
||||||
# Cart API (session persistence for LiveView)
|
# Cart API (session persistence for LiveView)
|
||||||
scope "/api", SimpleshopThemeWeb do
|
scope "/api", SimpleshopThemeWeb do
|
||||||
pipe_through [:browser]
|
pipe_through [:browser]
|
||||||
|
|||||||
1
mix.exs
1
mix.exs
@ -10,6 +10,7 @@ defmodule SimpleshopTheme.MixProject do
|
|||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
deps: deps(),
|
deps: deps(),
|
||||||
|
releases: [simpleshop_theme: [strip_beams: true]],
|
||||||
compilers: [:phoenix_live_view] ++ Mix.compilers(),
|
compilers: [:phoenix_live_view] ++ Mix.compilers(),
|
||||||
listeners: [Phoenix.CodeReloader],
|
listeners: [Phoenix.CodeReloader],
|
||||||
dialyzer: [ignore_warnings: ".dialyzer_ignore.exs"]
|
dialyzer: [ignore_warnings: ".dialyzer_ignore.exs"]
|
||||||
|
|||||||
7
rel/env.sh.eex
Normal file
7
rel/env.sh.eex
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Single-node by default (no distributed Erlang)
|
||||||
|
export RELEASE_DISTRIBUTION=none
|
||||||
|
|
||||||
|
# Writable temp dir for the BEAM
|
||||||
|
export RELEASE_TMP="${RELEASE_TMP:-/tmp}"
|
||||||
4
rel/overlays/bin/migrate
Executable file
4
rel/overlays/bin/migrate
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
exec bin/simpleshop_theme eval "SimpleshopTheme.Release.migrate()"
|
||||||
4
rel/overlays/bin/server
Executable file
4
rel/overlays/bin/server
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PHX_SERVER=true exec bin/simpleshop_theme start
|
||||||
Loading…
Reference in New Issue
Block a user