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:
jamey
2026-02-08 16:21:05 +00:00
parent eaa4bbb3fa
commit 1ee37c853d
15 changed files with 320 additions and 32 deletions

View File

@@ -13,11 +13,10 @@ defmodule SimpleshopTheme.Images.Optimizer do
# Only affects <5% of users (legacy browsers without AVIF/WebP support)
@pregenerated_formats [:avif, :webp]
@thumb_size 200
@cache_dir "priv/static/image_cache"
@max_stored_width 2000
@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
@doc """
@@ -72,7 +71,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
{:ok, :svg_skipped}
%{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
widths = applicable_widths(width)
@@ -93,7 +92,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
end
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 ->
with {:ok, thumb} <- Image.thumbnail(image, @thumb_size),
@@ -104,7 +103,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
end
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 ->
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
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 =
Enum.all?(widths, fn w ->
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)
@@ -159,12 +158,12 @@ defmodule SimpleshopTheme.Images.Optimizer do
"""
def generate_variant_on_demand(image_data, image_id, width, format)
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
{:ok, path}
else
File.mkdir_p!(@cache_dir)
File.mkdir_p!(cache_dir())
with {:ok, vips_image} <- Image.from_binary(image_data),
{:ok, resized} <- Image.thumbnail(vips_image, width),

View File

@@ -19,7 +19,7 @@ defmodule SimpleshopTheme.Images.VariantCache do
alias SimpleshopTheme.Sync.ImageDownloadWorker
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
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
@@ -87,9 +87,9 @@ defmodule SimpleshopTheme.Images.VariantCache do
end
defp ensure_mockup_variants do
if File.dir?(@mockup_dir) do
if File.dir?(mockup_dir()) do
sources =
Path.wildcard(Path.join(@mockup_dir, "*.webp"))
Path.wildcard(Path.join(mockup_dir(), "*.webp"))
|> Enum.reject(&is_variant?/1)
missing = Enum.reject(sources, &mockup_variants_exist?/1)

View 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