Skip to content

Deploying a Phoenix App

Phoenix is a powerful, modern web framework for Elixir that emphasizes scalability, developer productivity, and real-time capabilities. Built on top of the Erlang VM, Phoenix leverages Elixir’s functional programming model and fault-tolerant architecture to enable building high-concurrency web applications, real-time features with WebSockets, and Live views that sync state automatically without writing JavaScript. Its comprehensive tooling, excellent documentation, and integration with Ecto ORM make it ideal for building robust, maintainable web applications.

This guide explains how to deploy a Phoenix application to Klutch.sh, both with and without a Dockerfile, along with database configuration, environment setup, persistent storage, real-time patterns, security, and production deployment strategies.

Prerequisites

  • Elixir 1.14 or higher
  • Erlang 25 or higher
  • Phoenix 1.7 or higher
  • Git and a GitHub account
  • Klutch.sh account
  • Basic knowledge of Elixir and web framework concepts

Getting Started: Installing Elixir and Creating a Phoenix App

  1. Install Elixir and Erlang (if not already installed):

    Use asdf for version management (recommended):

    Terminal window
    asdf install elixir 1.16.0
    asdf install erlang 27.0
    asdf local elixir 1.16.0 erlang 27.0

    Or follow Elixir’s official installation guide.

  2. Install Phoenix:

    Terminal window
    mix archive.install hex phx_new
  3. Create a new Phoenix app with PostgreSQL:

    Terminal window
    mix phx.new my-phoenix-app
    cd my-phoenix-app
  4. Create the database locally:

    Terminal window
    mix ecto.create
  5. Start the development server:

    Terminal window
    mix phx.server

    Your app should be running at http://localhost:4000.

Sample Code for Getting Started

Create a complete Phoenix application with controllers, models, and real-time capabilities:

lib/my_phoenix_app_web/controllers/article_html.ex
defmodule MyPhoenixAppWeb.ArticleHTML do
embed_templates "article_html/*"
end
# lib/my_phoenix_app_web/controllers/article_controller.ex
defmodule MyPhoenixAppWeb.ArticleController do
use MyPhoenixAppWeb, :controller
alias MyPhoenixApp.Blog
alias MyPhoenixApp.Blog.Article
def index(conn, _params) do
articles = Blog.list_articles()
render(conn, :index, articles: articles)
end
def show(conn, %{"id" => id}) do
article = Blog.get_article!(id)
render(conn, :show, article: article)
end
def new(conn, _params) do
changeset = Blog.change_article(%Article{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"article" => article_params}) do
case Blog.create_article(article_params) do
{:ok, article} ->
conn
|> put_flash(:info, "Article created successfully.")
|> redirect(to: ~p"/articles/#{article}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
end
# lib/my_phoenix_app/blog/article.ex
defmodule MyPhoenixApp.Blog.Article do
use Ecto.Schema
import Ecto.Changeset
schema "articles" do
field :title, :string
field :content, :string
field :published, :boolean, default: false
timestamps(type: :utc_datetime_usec)
end
def changeset(article, attrs) do
article
|> cast(attrs, [:title, :content, :published])
|> validate_required([:title, :content])
|> validate_length(:title, min: 3, max: 255)
end
end
# lib/my_phoenix_app/blog.ex
defmodule MyPhoenixApp.Blog do
import Ecto.Query
alias MyPhoenixApp.Repo
alias MyPhoenixApp.Blog.Article
def list_articles do
Repo.all(Article)
end
def get_article!(id), do: Repo.get!(Article, id)
def create_article(attrs \\ %{}) do
%Article{}
|> Article.changeset(attrs)
|> Repo.insert()
end
def change_article(%Article{} = article, attrs \\ %{}) do
Article.changeset(article, attrs)
end
end

Deploying Without a Dockerfile (Using Nixpacks)

Klutch.sh uses Nixpacks to automatically detect and build your Phoenix application. Nixpacks analyzes your project and determines the necessary dependencies, build steps, and runtime configuration.

Steps to Deploy with Nixpacks

  1. Prepare your Phoenix app for production:

    Update config/prod.exs with production settings:

    config :my_phoenix_app, MyPhoenixAppWeb.Endpoint,
    url: [scheme: "https", host: System.get_env("PHX_HOST"), port: 443],
    http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
    secret_key_base: System.get_env("SECRET_KEY_BASE"),
    cache_static_manifest: "priv/static/cache_manifest.json",
    server: true
    config :my_phoenix_app, MyPhoenixApp.Repo,
    url: System.get_env("DATABASE_URL"),
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
  2. Set up environment variables locally:

    Create a .env file for local testing:

    Terminal window
    PHX_HOST=example-app.klutch.sh
    PORT=4000
    SECRET_KEY_BASE=$(mix phx.gen.secret)
    DATABASE_URL=postgresql://user:pass@localhost/myapp_dev
  3. Compile assets for production:

    Terminal window
    mix assets.deploy
  4. Commit your code to GitHub:

    Terminal window
    git add .
    git commit -m "Initial Phoenix app setup"
    git push origin main
  5. Log in to Klutch.sh dashboard:

    Visit https://klutch.sh/app

  6. Create a new project:

    • Click “Create Project”
    • Enter your project name (e.g., “My Phoenix App”)
    • Select your organization or personal account
  7. Create a new app:

    • Click “Create App”
    • Select your Phoenix GitHub repository
    • Select the branch (typically main or master)
    • Select HTTP as the traffic type
    • Set the internal port to 4000 (the default Phoenix port)
    • Choose your desired region, compute power, and number of instances
  8. Add environment variables:

    In the app creation form, add these essential environment variables:

    PHX_HOST=example-app.klutch.sh
    PORT=4000
    SECRET_KEY_BASE=<generated-with-mix-phx.gen.secret>
    DATABASE_URL=postgresql://user:password@host:5432/myapp_prod
    POOL_SIZE=10
    MIX_ENV=prod
  9. Deploy:

    Click “Create” to deploy. Klutch.sh will automatically:

    • Detect your Elixir/Phoenix project
    • Install dependencies with mix deps.get
    • Compile assets
    • Build a release with mix release
    • Start the application on port 4000

Your app will be available at example-app.klutch.sh.

Customizing Build and Start Commands

If you need to customize the build or start command for Nixpacks, add these environment variables:

NIXPACKS_BUILD_CMD=mix compile && mix assets.deploy && mix release
NIXPACKS_START_CMD=bin/my_phoenix_app start

Deploying With a Dockerfile

For more control over your build process and runtime environment, you can provide a custom Dockerfile. Klutch.sh will automatically detect and use a Dockerfile in your repository’s root directory.

Creating a Multi-Stage Dockerfile

  1. Create a Dockerfile in your project root:

    # Build stage
    FROM elixir:1.16-alpine AS builder
    WORKDIR /app
    # Install build dependencies
    RUN apk add --no-cache build-base git npm
    # Install Elixir dependencies
    COPY mix.exs mix.lock ./
    RUN mix deps.get && mix deps.compile
    # Copy source code
    COPY . .
    # Compile assets
    RUN npm install --prefix assets && npm run deploy --prefix assets
    RUN mix assets.deploy
    # Build release
    RUN MIX_ENV=prod mix compile && mix release
    # Runtime stage
    FROM alpine:latest
    RUN apk add --no-cache libstdc++ ncurses-libs openssl bash
    WORKDIR /app
    # Copy release from builder
    COPY --from=builder /app/_build/prod/rel/my_phoenix_app .
    # Health check
    HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1
    # Set environment
    ENV PORT=4000 MIX_ENV=prod
    # Expose port
    EXPOSE 4000
    # Start application
    CMD ["bin/my_phoenix_app", "start"]
  2. Commit the Dockerfile to GitHub:

    Terminal window
    git add Dockerfile
    git commit -m "Add production Dockerfile"
    git push origin main
  3. Log in to Klutch.sh dashboard:

    Visit https://klutch.sh/app

  4. Create a new app:

    • Click “Create App”
    • Select your Phoenix GitHub repository with the Dockerfile
    • Select the branch
    • Select HTTP as the traffic type
    • Set the internal port to 4000
    • Choose region, compute, and instances
  5. Add environment variables:

    PHX_HOST=example-app.klutch.sh
    PORT=4000
    SECRET_KEY_BASE=<your-secret-key>
    DATABASE_URL=postgresql://user:password@host:5432/myapp_prod
    POOL_SIZE=10
    MIX_ENV=prod
  6. Deploy:

    Click “Create”. Klutch.sh will automatically build your Docker image and deploy your app.


Database Configuration

Phoenix applications typically use Ecto with PostgreSQL or MySQL for data persistence. Here’s how to configure databases with Klutch.sh.

PostgreSQL Configuration

  1. Ecto is already configured in your Phoenix project for PostgreSQL by default.

  2. Update config/prod.exs to use the DATABASE_URL environment variable:

    config :my_phoenix_app, MyPhoenixApp.Repo,
    url: System.get_env("DATABASE_URL"),
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
    ssl: true,
    timeout: 20_000,
    connect_timeout: 5_000
  3. Create migrations for your database schema:

    Terminal window
    mix ecto.gen.migration create_articles_table
  4. Edit the migration file in priv/repo/migrations/:

    defmodule MyPhoenixApp.Repo.Migrations.CreateArticlesTable do
    use Ecto.Migration
    def change do
    create table(:articles) do
    add :title, :string, null: false
    add :content, :text, null: false
    add :published, :boolean, default: false
    timestamps(type: :utc_datetime_usec)
    end
    create index(:articles, [:inserted_at])
    end
    end
  5. During app creation on Klutch.sh, set:

    DATABASE_URL=postgresql://username:password@postgres-host:5432/myapp_prod
  6. Run migrations after deployment:

    The release will automatically run migrations if configured properly.

MySQL Configuration

  1. Create a new app with MySQL:

    Terminal window
    mix phx.new my-phoenix-app --database mysql
  2. Update config/prod.exs:

    config :my_phoenix_app, MyPhoenixApp.Repo,
    url: System.get_env("DATABASE_URL"),
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
  3. Set the DATABASE_URL environment variable:

    DATABASE_URL=mysql://username:password@mysql-host:3306/myapp_prod

Environment Variables

Phoenix applications use environment variables for configuration. Here are the essential variables:

MIX_ENV=prod
PORT=4000
PHX_HOST=example-app.klutch.sh
SECRET_KEY_BASE=<generated-secret-key>
DATABASE_URL=postgresql://user:password@host:5432/dbname
POOL_SIZE=10
REDIS_URL=redis://:password@redis-host:6379/0
LOG_LEVEL=info

Generating a Secret Key

Generate a production secret key:

Terminal window
mix phx.gen.secret

Copy the output and set it as the SECRET_KEY_BASE environment variable.


Real-Time Features with WebSockets

Phoenix’s strength is real-time capabilities through WebSockets and LiveView:

Setting Up WebSocket Channels

lib/my_phoenix_app_web/channels/room_channel.ex
defmodule MyPhoenixAppWeb.RoomChannel do
use MyPhoenixAppWeb, :channel
@impl true
def join("room:" <> room_id, _payload, socket) do
{:ok, assign(socket, :room_id, room_id)}
end
@impl true
def handle_in("new_message", %{"body" => body}, socket) do
broadcast(socket, "message_added", %{body: body})
{:noreply, socket}
end
end
# lib/my_phoenix_app_web/user_socket.ex
defmodule MyPhoenixAppWeb.UserSocket do
use Phoenix.Socket
channel "room:*", MyPhoenixAppWeb.RoomChannel
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
@impl true
def id(_socket), do: nil
end

LiveView for Real-Time UIs

lib/my_phoenix_app_web/live/articles_live.ex
defmodule MyPhoenixAppWeb.ArticlesLive do
use MyPhoenixAppWeb, :live_view
alias MyPhoenixApp.Blog
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :articles, Blog.list_articles())}
end
@impl true
def handle_event("delete_article", %{"id" => id}, socket) do
Blog.delete_article(id)
{:noreply, assign(socket, :articles, Blog.list_articles())}
end
end

Caching with Redis

Phoenix applications can leverage Redis for caching to improve performance:

  1. Add Redis to your mix.exs:

    defp deps do
    [
    {:redis, "~> 1.2"},
    {:phoenix, "~> 1.7"}
    ]
    end
  2. Configure Redis in config/prod.exs:

    config :my_phoenix_app, :redis,
    url: System.get_env("REDIS_URL") || "redis://localhost:6379/0"
  3. Use caching in your application:

    defmodule MyPhoenixApp.Cache do
    def get_or_fetch(key, fun) do
    case Redis.get(key) do
    {:ok, value} when is_binary(value) ->
    {:ok, Jason.decode!(value)}
    _ ->
    case fun.() do
    {:ok, data} ->
    Redis.set(key, Jason.encode!(data), ex: 3600)
    {:ok, data}
    error ->
    error
    end
    end
    end
    end
  4. Set the REDIS_URL environment variable on Klutch.sh:

    REDIS_URL=redis://:password@redis-host:6379/0

Persistent Storage

Phoenix applications may need persistent storage for uploaded files or logs. Klutch.sh supports mounting persistent volumes.

Configuring File Upload Storage

  1. Configure Arc for file uploads (or similar library):

    Add to mix.exs:

    {:arc, "~> 0.11.0"}
  2. Create a storage module:

    defmodule MyPhoenixApp.Uploaders.ArticleImage do
    use Arc.Definition
    use Arc.Ecto.Definition
    @versions [:original, :thumb]
    @extension_whitelist ~w(.jpg .jpeg .gif .png)
    def storage_dir(_version, {_file, scope}) do
    storage_path = System.get_env("STORAGE_PATH", "uploads")
    "#{storage_path}/article/#{scope.id}"
    end
    def validate({file, _}) do
    file_extension = file.file_name |> Path.extname() |> String.downcase()
    Enum.member?(@extension_whitelist, file_extension)
    end
    end
  3. Use uploads in your schema:

    defmodule MyPhoenixApp.Blog.Article do
    use Ecto.Schema
    import Ecto.Changeset
    schema "articles" do
    field :title, :string
    field :image, MyPhoenixApp.Uploaders.ArticleImage.Type
    field :content, :string
    timestamps()
    end
    def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :content])
    |> cast_attachments(attrs, [:image])
    |> validate_required([:title, :content])
    end
    end
  4. On Klutch.sh, attach a persistent volume:

    • During app creation, navigate to the “Storage” section
    • Click “Add Volume”
    • Set mount path to /app/uploads
    • Allocate size (e.g., 10GB)
  5. Set the STORAGE_PATH environment variable:

    STORAGE_PATH=/app/uploads

Security Best Practices

1. Enforce HTTPS

Configure in config/prod.exs:

config :my_phoenix_app, MyPhoenixAppWeb.Endpoint,
force_ssl: [rewrite_on: [:x_forwarded_proto], hsts: 31_536_000]

2. Configure CORS

Use plug_cowboy with CORS middleware:

plug CORSPlug,
origin: [System.get_env("ALLOWED_ORIGINS") || "http://localhost:3000"]

3. Use Environment Variables for Secrets

Never hardcode secrets:

config :my_phoenix_app,
api_key: System.get_env("API_KEY"),
jwt_secret: System.get_env("JWT_SECRET")

4. Implement Authentication

Use phx_gen_auth (built into Phoenix 1.7):

Terminal window
mix phx.gen.auth Accounts User users

5. Use Plugs for Security Headers

defmodule MyPhoenixAppWeb.SecurityHeaders do
def init(opts), do: opts
def call(conn, _opts) do
conn
|> put_resp_header("x-frame-options", "DENY")
|> put_resp_header("x-content-type-options", "nosniff")
|> put_resp_header("x-xss-protection", "1; mode=block")
|> put_resp_header("referrer-policy", "strict-origin-when-cross-origin")
end
end

6. Implement Rate Limiting

Use hammer library:

defmodule MyPhoenixAppWeb.RateLimitPlug do
def init(opts), do: opts
def call(conn, _opts) do
case Hammer.check_rate("requests:#{conn.remote_ip |> Tuple.to_list() |> Enum.join(".")}", 60_000, 100) do
{:allow, _count} -> conn
{:deny, _limit} -> conn |> put_status(429) |> halt()
end
end
end

7. Enable Content Security Policy

plug Phoenix.LiveDashboard.Router, at: "/dashboard"
config :my_phoenix_app, MyPhoenixAppWeb.Endpoint,
csp_policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'"

Monitoring and Logging

Health Check Endpoint

Add a health check route:

lib/my_phoenix_app_web/controllers/health_controller.ex
defmodule MyPhoenixAppWeb.HealthController do
use MyPhoenixAppWeb, :controller
def index(conn, _params) do
status = %{
status: "healthy",
timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
}
json(conn, status)
end
end
# lib/my_phoenix_app_web/router.ex
scope "/", MyPhoenixAppWeb do
pipe_through :api
get "/health", HealthController, :index
end

Logging Configuration

Configure logging in config/prod.exs:

config :logger,
level: :info,
compile_time_purge_matching: [
[level_lower_than: :info]
]
config :logger, :console,
format: {Jason, :encode!},
metadata: [:request_id, :user_id]

Monitoring with Telemetry

Setup telemetry for monitoring:

defmodule MyPhoenixApp.Telemetry do
use Supervisor
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
{:telemetry_poller, telemetry_poller_metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
defp telemetry_poller_metrics do
[
{:metrics, [
Telemetry.Metrics.ProcessorCounter.new("vm.memory.total"),
Telemetry.Metrics.ProcessorCounter.new("vm.memory.processes_used")
]}
]
end
end

Custom Domains

To use a custom domain with your Phoenix app on Klutch.sh:

  1. Access the app settings in your Klutch.sh dashboard

  2. Navigate to the “Domains” section

  3. Add your custom domain (e.g., example.com)

  4. Update your DNS records at your domain registrar:

    Create a CNAME record pointing to example-app.klutch.sh

    Name: www
    Type: CNAME
    Value: example-app.klutch.sh
  5. Wait for DNS propagation (usually 5-30 minutes)

  6. Verify the domain in Klutch.sh

Your app will be accessible at www.example.com.


Troubleshooting

1. App Won’t Start - Mix Compile Errors

Problem: Deployment fails with “error: could not compile application”

Solution:

  • Ensure mix.exs, mix.lock, and assets/package-lock.json are committed to Git
  • Run mix deps.get && mix compile locally to verify the build works
  • Check for deprecated Elixir/Phoenix APIs and update code
  • Verify all dependencies are compatible with your Elixir version

2. Database Connection Failures

Problem: Application crashes with “connection refused” or “econnrefused”

Solution:

  • Verify DATABASE_URL is set correctly with proper credentials and host
  • Ensure the database is accessible from the application container
  • Test the connection string locally: mix ecto.create
  • Check database firewall rules allow connections from Klutch.sh
  • Set ssl: true in your database config if required

3. Port Binding Issues

Problem: Application fails with “address already in use” or “eaddrinuse”

Solution:

  • Verify your config/prod.exs binds to 0.0.0.0 not 127.0.0.1
  • Set PORT=4000 environment variable correctly
  • Check that the Dockerfile exposes port 4000
  • Confirm the internal port in Klutch.sh is set to 4000

4. Asset Compilation Failures

Problem: Stylesheets and JavaScript not loading, or build fails during mix assets.deploy

Solution:

  • Ensure assets/package.json and assets/package-lock.json are committed
  • Run npm install --prefix assets && npm run deploy --prefix assets locally
  • Check Node.js version compatibility
  • Verify all CSS/JS dependencies are correctly specified
  • Look for errors in npm run deploy output

5. Release Build Failures

Problem: Build fails during mix release or app won’t start after release

Solution:

  • Run mix release locally to verify it works
  • Check all environment variables are set correctly
  • Ensure SECRET_KEY_BASE is at least 32 characters
  • Review rel/overlays/ directory if you have custom configurations
  • Check application startup code in lib/my_phoenix_app/application.ex

Best Practices for Phoenix on Klutch.sh

  1. Use Ecto changesets thoroughly - Leverage Ecto’s changesets for data validation and transformation, making your code safer and more maintainable.

  2. Organize code with contexts - Use domain-driven design with Phoenix contexts to organize your application logic and keep modules focused.

  3. Implement proper supervision - Design your supervision tree carefully to handle failures gracefully and leverage Erlang’s fault tolerance.

  4. Use Plug pipelines effectively - Create reusable plugs for common concerns like authentication, authorization, and logging.

  5. Monitor application health - Implement health checks and telemetry to understand your application’s behavior in production.

  6. Configure database connection pooling - Set POOL_SIZE appropriately based on your concurrency needs and database capacity.

  7. Use LiveView for real-time features - Leverage Phoenix LiveView to build real-time, interactive applications without writing JavaScript.

  8. Implement proper error handling - Use Phoenix error handlers and Ecto error changesets to provide meaningful feedback to users.

  9. Keep dependencies up to date - Regularly update Elixir, Phoenix, and other dependencies to benefit from improvements and security patches.

  10. Test with proper OTP applications - Ensure your tests properly start the OTP application and clean up resources between tests.


External Resources