feat(elixir): add docker deployment
This commit is contained in:
48
.dockerignore
Normal file
48
.dockerignore
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
components_elixir-*.tar
|
||||||
|
|
||||||
|
# Temporary files, for example, from tests.
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# If NPM crashes, it generates a log, let's ignore it too.
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# The directory NPM downloads your dependencies sources to.
|
||||||
|
/assets/node_modules/
|
||||||
|
|
||||||
|
# Since we are building assets from assets/,
|
||||||
|
# we ignore priv/static. You may want to comment
|
||||||
|
# this depending on your deployment strategy.
|
||||||
|
/priv/static/
|
||||||
|
|
||||||
|
# Files matching config/*.secret.exs pattern contains sensitive
|
||||||
|
# data and you should not commit them into version control.
|
||||||
|
#
|
||||||
|
# Alternatively, you may comment the line below and commit the
|
||||||
|
# secrets files as long as you replace their contents by environment
|
||||||
|
# variables.
|
||||||
|
/config/*.secret.exs
|
||||||
|
|
||||||
|
.elixir_ls/
|
||||||
|
.DS_Store
|
||||||
99
Dockerfile
Normal file
99
Dockerfile
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
|
||||||
|
# instead of Alpine to avoid DNS resolution issues in production.
|
||||||
|
#
|
||||||
|
# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
|
||||||
|
# https://hub.docker.com/_/ubuntu?tab=tags
|
||||||
|
#
|
||||||
|
# This file is based on these images:
|
||||||
|
#
|
||||||
|
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
|
||||||
|
# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm - for the release image
|
||||||
|
# - https://pkgs.org/ - resource for finding needed packages
|
||||||
|
#
|
||||||
|
# Use more recent and available images
|
||||||
|
ARG ELIXIR_VERSION=1.15
|
||||||
|
ARG OTP_VERSION=26
|
||||||
|
ARG DEBIAN_VERSION=bookworm-slim
|
||||||
|
|
||||||
|
ARG BUILDER_IMAGE="elixir:${ELIXIR_VERSION}-otp-${OTP_VERSION}"
|
||||||
|
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
||||||
|
|
||||||
|
FROM ${BUILDER_IMAGE} as builder
|
||||||
|
|
||||||
|
# install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential git ca-certificates curl \
|
||||||
|
&& update-ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# prepare build dir
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# install hex + rebar
|
||||||
|
RUN mix local.hex --force && \
|
||||||
|
mix local.rebar --force
|
||||||
|
|
||||||
|
# set build ENV
|
||||||
|
ENV MIX_ENV="prod"
|
||||||
|
|
||||||
|
# install mix dependencies
|
||||||
|
COPY mix.exs mix.lock ./
|
||||||
|
RUN mix deps.get --only $MIX_ENV
|
||||||
|
RUN mkdir config
|
||||||
|
|
||||||
|
# copy compile-time config files before we compile dependencies
|
||||||
|
# to ensure any relevant config change will trigger the dependencies
|
||||||
|
# to be re-compiled.
|
||||||
|
COPY config/config.exs config/${MIX_ENV}.exs config/
|
||||||
|
RUN mix deps.compile
|
||||||
|
|
||||||
|
# Pre-install Tailwind and ESBuild to avoid network issues during assets.deploy
|
||||||
|
RUN mix tailwind.install --if-missing
|
||||||
|
RUN mix esbuild.install --if-missing
|
||||||
|
|
||||||
|
COPY priv priv
|
||||||
|
|
||||||
|
COPY lib lib
|
||||||
|
|
||||||
|
COPY assets assets
|
||||||
|
|
||||||
|
# Copy the apriltags.ps file required for compilation
|
||||||
|
COPY apriltags.ps .
|
||||||
|
|
||||||
|
# compile assets
|
||||||
|
RUN mix assets.deploy
|
||||||
|
|
||||||
|
# Compile the release
|
||||||
|
RUN mix compile
|
||||||
|
|
||||||
|
# Changes to config/runtime.exs don't require recompiling the code
|
||||||
|
COPY config/runtime.exs config/
|
||||||
|
|
||||||
|
COPY rel rel
|
||||||
|
RUN mix release
|
||||||
|
|
||||||
|
# start a new build stage so that the final image will only contain
|
||||||
|
# the compiled release and other runtime necessities
|
||||||
|
FROM ${RUNNER_IMAGE}
|
||||||
|
|
||||||
|
RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
|
||||||
|
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
|
||||||
|
|
||||||
|
# Set the locale
|
||||||
|
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||||
|
|
||||||
|
ENV LANG en_US.UTF-8
|
||||||
|
ENV LANGUAGE en_US:en
|
||||||
|
ENV LC_ALL en_US.UTF-8
|
||||||
|
|
||||||
|
WORKDIR "/app"
|
||||||
|
RUN chown nobody /app
|
||||||
|
|
||||||
|
# set runner ENV
|
||||||
|
ENV MIX_ENV="prod"
|
||||||
|
|
||||||
|
# Only copy the final release from the build stage
|
||||||
|
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/components_elixir ./
|
||||||
|
|
||||||
|
USER nobody
|
||||||
|
|
||||||
|
CMD ["/app/bin/server"]
|
||||||
124
README.md
124
README.md
@@ -216,7 +216,129 @@ mix ecto.migrations
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
For production deployment:
|
### 🐳 Docker Deployment (Recommended)
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Git for cloning the repository
|
||||||
|
|
||||||
|
#### Quick Start
|
||||||
|
|
||||||
|
1. **Clone the repository:**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd components_elixir
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build and run with Docker Compose:**
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Build Time Notice**: The initial build can take 5-15 minutes because:
|
||||||
|
- Tailwind CSS downloads ~500MB from GitHub (can be very slow)
|
||||||
|
- ESBuild downloads additional build tools
|
||||||
|
- Elixir compiles all dependencies
|
||||||
|
- Network conditions may significantly affect download speeds
|
||||||
|
|
||||||
|
3. **Access the application:**
|
||||||
|
- Open [http://localhost:4000](http://localhost:4000)
|
||||||
|
- Default password: `changeme`
|
||||||
|
|
||||||
|
#### Docker Configuration Files
|
||||||
|
|
||||||
|
The project includes these Docker files:
|
||||||
|
|
||||||
|
- **`Dockerfile`** - Multi-stage build for production
|
||||||
|
- **`docker-compose.yml`** - Local development/testing setup
|
||||||
|
- **`.dockerignore`** - Excludes unnecessary files from build context
|
||||||
|
|
||||||
|
#### Customizing Docker Deployment
|
||||||
|
|
||||||
|
1. **Environment Variables**: Edit `docker-compose.yml` to customize:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: "ecto://postgres:postgres@db:5432/components_elixir_prod"
|
||||||
|
SECRET_KEY_BASE: "your-secret-key-here" # Generate with: mix phx.gen.secret
|
||||||
|
PHX_HOST: "localhost" # Change to your domain
|
||||||
|
PHX_SERVER: "true"
|
||||||
|
PORT: "4000"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Generate a secure secret key:**
|
||||||
|
```bash
|
||||||
|
# Run this locally to generate a new secret
|
||||||
|
mix phx.gen.secret
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Database Configuration**: The default setup includes:
|
||||||
|
- PostgreSQL 15 container
|
||||||
|
- Automatic database creation
|
||||||
|
- Health checks to ensure proper startup order
|
||||||
|
- Persistent data storage with Docker volumes
|
||||||
|
|
||||||
|
#### Production Docker Deployment
|
||||||
|
|
||||||
|
For production environments:
|
||||||
|
|
||||||
|
1. **Create a production docker-compose.yml:**
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: components_user
|
||||||
|
POSTGRES_PASSWORD: secure_db_password
|
||||||
|
POSTGRES_DB: components_elixir_prod
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "80:4000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: "ecto://components_user:secure_db_password@db:5432/components_elixir_prod"
|
||||||
|
SECRET_KEY_BASE: "your-64-char-secret-key"
|
||||||
|
PHX_HOST: "yourdomain.com"
|
||||||
|
PHX_SERVER: "true"
|
||||||
|
PORT: "4000"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["/bin/sh", "-c", "/app/bin/migrate && /app/bin/server"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy to production:**
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Troubleshooting
|
||||||
|
|
||||||
|
**Build Issues:**
|
||||||
|
- **Slow Tailwind download**: This is normal - GitHub releases can be slow
|
||||||
|
- **Network timeouts**: Retry the build with `docker compose up --build`
|
||||||
|
- **AprilTag compilation errors**: Ensure `apriltags.ps` file exists in project root
|
||||||
|
|
||||||
|
**Runtime Issues:**
|
||||||
|
- **Database connection errors**: Wait for PostgreSQL health check to pass
|
||||||
|
- **Permission errors**: Check file ownership and Docker user permissions
|
||||||
|
- **Port conflicts**: Change the port mapping in docker-compose.yml
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- **Slow startup**: First-time container startup includes database initialization
|
||||||
|
- **Memory usage**: Elixir/Phoenix applications typically use 50-200MB RAM
|
||||||
|
- **Storage**: PostgreSQL data persists in Docker volumes
|
||||||
|
|
||||||
|
### 🚀 Traditional Deployment
|
||||||
|
|
||||||
|
For production deployment without Docker:
|
||||||
|
|
||||||
1. **Set environment variables:**
|
1. **Set environment variables:**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -22,14 +22,13 @@ import "phoenix_html"
|
|||||||
// Establish Phoenix Socket and LiveView configuration.
|
// Establish Phoenix Socket and LiveView configuration.
|
||||||
import {Socket} from "phoenix"
|
import {Socket} from "phoenix"
|
||||||
import {LiveSocket} from "phoenix_live_view"
|
import {LiveSocket} from "phoenix_live_view"
|
||||||
import {hooks as colocatedHooks} from "phoenix-colocated/components_elixir"
|
|
||||||
import topbar from "../vendor/topbar"
|
import topbar from "../vendor/topbar"
|
||||||
|
|
||||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||||
const liveSocket = new LiveSocket("/live", Socket, {
|
const liveSocket = new LiveSocket("/live", Socket, {
|
||||||
longPollFallbackMs: 2500,
|
longPollFallbackMs: 2500,
|
||||||
params: {_csrf_token: csrfToken},
|
params: {_csrf_token: csrfToken},
|
||||||
hooks: {...colocatedHooks},
|
hooks: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show progress bar on live navigation and form submits
|
// Show progress bar on live navigation and form submits
|
||||||
|
|||||||
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: components_elixir_prod
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: "ecto://postgres:postgres@db:5432/components_elixir_prod"
|
||||||
|
SECRET_KEY_BASE: "WYxST6ZVqFdeXRfjP3MUAc+hNVCJ6LW/+aqwuP27Ab77R4yXFQlO2HsukrOzCVif"
|
||||||
|
PHX_HOST: "localhost"
|
||||||
|
PHX_SERVER: "true"
|
||||||
|
PORT: "4000"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
command: ["/bin/sh", "-c", "/app/bin/migrate && /app/bin/server"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
30
lib/components_elixir/release.ex
Normal file
30
lib/components_elixir/release.ex
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
defmodule ComponentsElixir.Release do
|
||||||
|
@moduledoc """
|
||||||
|
Used for executing DB release tasks when run in production without Mix
|
||||||
|
installed.
|
||||||
|
"""
|
||||||
|
@app :components_elixir
|
||||||
|
|
||||||
|
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
|
||||||
|
# Many platforms require SSL when connecting to the database
|
||||||
|
Application.ensure_all_started(:ssl)
|
||||||
|
Application.ensure_loaded(@app)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user