
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Shipping Elixir applications reliably requires a pipeline that understands the BEAM's unique compilation model and dependency graph. Setting up CI/CD for Phoenix with GitHub Actions eliminates manual release friction while enforcing test coverage and security standards before code reaches production. This guide provides a battle-tested workflow configuration that handles Elixir version pinning, PostgreSQL service containers, and secure artifact delivery without exposing long-lived credentials.
How do you structure CI/CD for Phoenix with GitHub Actions?
A robust pipeline for Elixir differs from Node.js or Python because compilation is expensive and environment-specific. Your CI/CD for Phoenix with GitHub Actions must separate validation from artifact creation to avoid rebuilding on every deploy. The architecture below splits responsibilities into three distinct phases triggered by git events.
In practice, I recommend keeping the test job lightweight and fast. Developers lose momentum when feedback loops exceed five minutes. Use matrix builds only when supporting multiple Elixir versions; otherwise, pin to your exact production OTP/Elixir pair. For teams managing database-heavy applications, understanding PostgreSQL administration essentials helps debug service container issues that frequently appear in Elixir CI environments.
Workflow file placement and permissions
Create .github/workflows/ci.yml at your repository root. Always define minimal permissions explicitly rather than relying on defaults. This follows the principle of least privilege critical for SOC 2 compliance:
name: Phoenix CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write How do you optimize Elixir caching in GitHub Actions?
Elixir compilation is CPU-intensive. Without proper caching, CI/CD for Phoenix with GitHub Actions can take 15+ minutes per run. The key is caching both dependencies and compiled artifacts separately, since they change at different frequencies.
- Cache deps directory: Hash
mix.lockto restore downloaded packages instantly. - Cache _build directory: Hash both
mix.lockand source files to reuse compiled beam files. - Use setup-beam action: It includes built-in caching support that handles OTP path quirks.
- Set MIX_ENV=test explicitly: Prevents accidental dev/prod compilation in CI.
- uses: erlef/setup-beam@v1
with:
otp-version: '27.0'
elixir-version: '1.17.2'
install-hex: true
install-rebar: true
- name: Restore dependencies cache
uses: actions/cache@v4
with:
path: deps
key: ${{ runner.os }}-deps-${{ hashFiles('/mix.lock') }}
restore-keys: ${{ runner.os }}-deps-
- name: Restore build cache
uses: actions/cache@v4
with:
path: _build
key: ${{ runner.os }}-build-${{ hashFiles('/mix.lock') }}-${{ hashFiles('/*.ex', '/*.exs') }}
restore-keys: |
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
${{ runner.os }}-build- A common mistake is caching _build with only mix.lock as the key. When you modify application code but don't update dependencies, the stale cache restores old beam files and causes confusing test failures. Always include source file hashes in the primary key while keeping lock-only hashes as fallback restore keys.
How do you configure PostgreSQL service containers for Phoenix tests?
Phoenix applications typically require PostgreSQL during testing. GitHub Actions service containers provide isolated databases without external infrastructure. Proper health checks prevent race conditions where tests start before the database accepts connections.
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: ecto://postgres:postgres@localhost/myapp_test
MIX_ENV: test The pg_isready health check is non-negotiable. Without it, your first migration or test query will fail intermittently because PostgreSQL hasn't finished initialization. If you're new to configuring databases for CI, review PostgreSQL backup and restore with pg_dump to understand how schema loading interacts with ephemeral test databases.
How do you build optimized Docker images for Phoenix releases?
Multi-stage builds reduce final image size from ~800MB to under 150MB. This matters for cold-start latency and storage costs. The pattern compiles inside a full SDK image then copies only the release to a minimal runtime base.
# Build stage
FROM hexpm/elixir:1.17.2-erlang-27.0-alpine-3.20 AS builder
RUN apk add --no-cache git npm
WORKDIR /app
COPY mix.exs mix.lock ./
RUN mix local.hex --force && mix deps.get --only prod
COPY assets/package.json assets/package-lock.json ./assets/
RUN npm ci --prefix assets
COPY . .
RUN npm run deploy --prefix assets && \
mix phx.digest && \
mix release
# Runtime stage
FROM alpine:3.20 AS runtime
RUN apk add --no-cache libstdc++ openssl ncurses-libs
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/myapp ./
ENV PHX_SERVER=true PORT=4000
EXPOSE 4000
CMD ["bin/myapp", "start"] Always use hexpm/elixir images rather than generic Elixir Docker Hub images. They receive security patches faster and match exact OTP/Elixir combinations. Pin Alpine versions too; floating tags break reproducibility during incidents when you need identical rebuilds.
How do you handle secrets securely in Phoenix GitHub Actions pipelines?
Static access keys are an audit failure waiting to happen. In 2026, every serious team uses OpenID Connect (OIDC) for cloud authentication. This eliminates long-lived credentials entirely and satisfies ISO 27001 control requirements around secret management.
| Approach | Security Posture | Audit Trail | Rotation Burden | Recommended For |
|---|---|---|---|---|
| Repository Secrets (static) | Low — persists until manual rotation | No usage attribution | High — quarterly manual work | Prototypes only |
| Environment Secrets | Medium — scoped to deployment target | Partial — environment-level logs | Medium — per-environment rotation | Small teams, single cloud |
| OIDC Federation | High — short-lived tokens, no stored secrets | Full — IAM identity per workflow run | None — automatic expiration | All production workloads |
Configure AWS IAM role trust policies to accept tokens only from specific repositories and branches. This prevents compromised forks from accessing production resources. Teams evaluating their broader observability strategy alongside deployment security should explore the four golden signals of monitoring to ensure deployed releases emit actionable telemetry immediately.
Configuring aws-actions/configure-aws-credentials
Add the role ARN to your workflow. No access keys needed in repository settings:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/phoenix-deploy-role
aws-region: us-east-1
role-session-name: github-actions-${{ github.run_id }} What deployment targets work best for Phoenix in 2026?
Your choice depends on operational capacity and traffic patterns. Each platform has trade-offs that affect how you structure CI/CD for Phoenix with GitHub Actions.
- Fly.io: Lowest operational overhead for solo developers. Built-in Postgres clustering and global distribution. Deploy step is a single
flyctl deploycommand after Docker push. - AWS ECS/Fargate: Best for teams already invested in AWS ecosystem. Requires more YAML but integrates with ALB, CloudWatch, and VPC peering natively.
- Kubernetes (EKS/GKE): Necessary when running 10+ services sharing infrastructure. Overkill for single Phoenix apps unless you anticipate significant scale or multi-service orchestration needs.
- Gigalixir / Render: Managed PaaS options specifically designed for Elixir. Zero-config clustering and hot upgrades, but less flexibility for custom networking or compliance requirements.
For Nepal-based teams serving local users, consider latency implications. AWS Mumbai or Singapore regions typically offer better connectivity than US-East. Budget-conscious startups should evaluate whether managed PaaS pricing exceeds self-hosted VPS costs at their current scale before committing to a platform.
Implementing Secure CI/CD for Phoenix with GitHub Actions
Automated pipelines remove human error from releases while enforcing consistent quality gates. Start with the test and cache configuration above, validate it on pull requests for two weeks, then add Docker builds and OIDC deployment. Monitor pipeline duration weekly; if tests creep past eight minutes, split into parallel jobs or investigate slow query performance in your test suite. Security and reliability compound over time when baked into automation rather than bolted on afterward.
If you need help designing a compliant deployment pipeline or auditing your existing Elixir infrastructure, reach out to discuss your specific requirements.