Skip to content

Commit 2d72fb5

Browse files
committed
feat(deploy): add Docker self-hosting and CI publish workflow
Add complete self-hosting support with Dockerfile, three compose files (pre-built image, build-from-source, dev services), GitHub Actions workflow for publishing to GHCR, Railway config, .env.example, release module with auto-migrations, and self-hosting documentation page.
1 parent 469837b commit 2d72fb5

15 files changed

Lines changed: 724 additions & 30 deletions

File tree

.dockerignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Build artifacts
2+
_build/
3+
deps/
4+
5+
# Node
6+
node_modules/
7+
docs/node_modules/
8+
9+
# Git
10+
.git
11+
.gitignore
12+
13+
# Docker
14+
.dockerignore
15+
Dockerfile
16+
17+
# Dev/test
18+
.env*
19+
!.env.example
20+
.postgres/
21+
.minio/
22+
23+
# IDE
24+
.elixir_ls/
25+
.vscode/
26+
.idea/
27+
28+
# Docs source (not needed in image)
29+
docs/
30+
31+
# Misc
32+
*.md
33+
!CLAUDE.md
34+
screenshots/
35+
tmp/

.env.example

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# =============================================================================
2+
# Craftplan — Environment Configuration
3+
# =============================================================================
4+
# Copy this file to .env and fill in the required values.
5+
#
6+
# cp .env.example .env
7+
#
8+
# Works with both docker-compose.yml (pre-built image) and standalone docker run.
9+
# Then generate the secrets below before starting the app.
10+
11+
# -----------------------------------------------------------------------------
12+
# Required Secrets
13+
# -----------------------------------------------------------------------------
14+
15+
# Phoenix secret key (min 64 bytes). Generate with:
16+
# mix phx.gen.secret
17+
# Or without Elixir installed:
18+
# docker run --rm hexpm/elixir:1.18.3-erlang-27.2.4-debian-bookworm-20260112-slim \
19+
# sh -c "mix local.hex --force --if-missing >/dev/null 2>&1 && mix phx.gen.secret"
20+
SECRET_KEY_BASE=
21+
22+
# Token signing secret for API authentication. Generate the same way:
23+
# mix phx.gen.secret
24+
TOKEN_SIGNING_SECRET=
25+
26+
# Cloak encryption key (32-byte AES key, base64-encoded). Generate with:
27+
# elixir -e ":crypto.strong_rand_bytes(32) |> Base.encode64() |> IO.puts()"
28+
# Or without Elixir installed:
29+
# docker run --rm hexpm/elixir:1.18.3-erlang-27.2.4-debian-bookworm-20260112-slim \
30+
# elixir -e ":crypto.strong_rand_bytes(32) |> Base.encode64() |> IO.puts()"
31+
CLOAK_KEY=
32+
33+
# PostgreSQL password for the bundled database container.
34+
POSTGRES_PASSWORD=
35+
36+
# -----------------------------------------------------------------------------
37+
# Host & Networking
38+
# -----------------------------------------------------------------------------
39+
40+
# The public hostname of your Craftplan instance (used for URLs, CORS, etc.)
41+
HOST=localhost
42+
43+
# Port the web server listens on (default: 4000)
44+
PORT=4000
45+
46+
# -----------------------------------------------------------------------------
47+
# File Storage (S3 / MinIO)
48+
# -----------------------------------------------------------------------------
49+
# The bundled MinIO service works out of the box with the defaults below.
50+
# To use an external S3 provider, set these values accordingly.
51+
52+
# MINIO_ROOT_USER=minioadmin
53+
# MINIO_ROOT_PASSWORD=minioadmin
54+
# AWS_S3_BUCKET=craftplan
55+
56+
# For external S3 (uncomment and set):
57+
# AWS_ACCESS_KEY_ID=
58+
# AWS_SECRET_ACCESS_KEY=
59+
# AWS_S3_SCHEME=https://
60+
# AWS_S3_HOST=s3.amazonaws.com
61+
# AWS_REGION=us-east-1
62+
# AWS_ASSET_HOST=https://your-bucket.s3.amazonaws.com
63+
64+
# -----------------------------------------------------------------------------
65+
# Email (Optional)
66+
# -----------------------------------------------------------------------------
67+
# Email is primarily configured from the Settings UI inside the app.
68+
# These environment variables are a fallback for automated/headless setups.
69+
#
70+
# Supported providers: sendgrid, postmark, brevo, mailgun, amazon_ses
71+
#
72+
# EMAIL_PROVIDER=
73+
# EMAIL_API_KEY=
74+
#
75+
# Mailgun only:
76+
# EMAIL_API_DOMAIN=
77+
#
78+
# Amazon SES only:
79+
# EMAIL_API_SECRET=
80+
# EMAIL_API_REGION=us-east-1
81+
#
82+
# SMTP (alternative to API providers):
83+
# SMTP_HOST=
84+
# SMTP_PORT=587
85+
# SMTP_USERNAME=
86+
# SMTP_PASSWORD=
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Docker
2+
3+
on:
4+
push:
5+
branches: [main]
6+
release:
7+
types: [published]
8+
9+
env:
10+
REGISTRY: ghcr.io
11+
IMAGE_NAME: ${{ github.repository }}
12+
13+
jobs:
14+
build-and-push:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
packages: write
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: docker/setup-qemu-action@v3
24+
25+
- uses: docker/setup-buildx-action@v3
26+
27+
- name: Log in to GHCR
28+
uses: docker/login-action@v3
29+
with:
30+
registry: ${{ env.REGISTRY }}
31+
username: ${{ github.actor }}
32+
password: ${{ secrets.GITHUB_TOKEN }}
33+
34+
- name: Extract metadata
35+
id: meta
36+
uses: docker/metadata-action@v5
37+
with:
38+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
39+
tags: |
40+
type=ref,event=branch
41+
type=semver,pattern={{version}}
42+
type=semver,pattern={{major}}.{{minor}}
43+
type=raw,value=latest,enable={{is_default_branch}}
44+
45+
- name: Build and push
46+
uses: docker/build-push-action@v6
47+
with:
48+
context: .
49+
platforms: linux/amd64,linux/arm64
50+
push: true
51+
tags: ${{ steps.meta.outputs.tags }}
52+
labels: ${{ steps.meta.outputs.labels }}
53+
cache-from: type=gha
54+
cache-to: type=gha,mode=max

Dockerfile

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# === Build stage ===
2+
ARG ELIXIR_VERSION=1.18.3
3+
ARG OTP_VERSION=27.2.4
4+
ARG DEBIAN_VERSION=bookworm-20260112-slim
5+
6+
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
7+
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
8+
9+
FROM ${BUILDER_IMAGE} AS builder
10+
11+
RUN apt-get update -y && \
12+
apt-get install -y build-essential git curl && \
13+
apt-get clean && rm -f /var/lib/apt/lists/*_*
14+
15+
WORKDIR /app
16+
17+
RUN mix local.hex --force && mix local.rebar --force
18+
19+
ENV MIX_ENV=prod
20+
21+
COPY mix.exs mix.lock ./
22+
RUN mix deps.get --only $MIX_ENV
23+
RUN mkdir config
24+
25+
COPY config/config.exs config/${MIX_ENV}.exs config/
26+
RUN mix deps.compile
27+
28+
COPY priv priv
29+
COPY lib lib
30+
COPY assets assets
31+
32+
RUN mix assets.setup
33+
RUN mix assets.deploy
34+
35+
# Copy runtime config last so earlier layers are cached
36+
COPY config/runtime.exs config/
37+
38+
COPY rel rel
39+
RUN mix release
40+
41+
# === Runtime stage ===
42+
FROM ${RUNNER_IMAGE}
43+
44+
RUN apt-get update -y && \
45+
apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates && \
46+
apt-get clean && rm -f /var/lib/apt/lists/*_*
47+
48+
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
49+
50+
ENV LANG=en_US.UTF-8
51+
ENV LANGUAGE=en_US:en
52+
ENV LC_ALL=en_US.UTF-8
53+
54+
WORKDIR /app
55+
56+
RUN chown nobody /app
57+
ENV MIX_ENV=prod
58+
59+
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/craftplan ./
60+
61+
USER nobody
62+
63+
CMD ["bin/server"]

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,30 @@ Elixir · [Ash Framework](https://ash-hq.org/) · Phoenix LiveView · PostgreSQL
8989
> **Prerequisites:** Docker, Elixir ~> 1.15, Erlang/OTP 27
9090
9191
```bash
92-
docker-compose up -d # Start PostgreSQL + MinIO
92+
docker compose -f docker-compose.dev.yml up -d # Start PostgreSQL + MinIO + Mailpit
9393
mix setup # Install deps, migrate, build assets, seed
9494
mix phx.server # Start at localhost:4000
9595
```
9696

9797
See the [setup guide](https://puemos.github.io/craftplan/docs/getting-started/) for detailed instructions.
9898

99+
## Self-Hosting with Docker
100+
101+
Deploy Craftplan on your own server — no need to clone the repo:
102+
103+
```bash
104+
curl -O https://raw.githubusercontent.com/puemos/craftplan/main/docker-compose.yml
105+
curl -O https://raw.githubusercontent.com/puemos/craftplan/main/.env.example
106+
cp .env.example .env # Fill in the required secrets (see .env.example)
107+
docker compose up -d
108+
```
109+
110+
This starts Craftplan, PostgreSQL, and MinIO with migrations running automatically.
111+
112+
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/craftplan)
113+
114+
See the [self-hosting guide](https://puemos.github.io/craftplan/docs/self-hosting/) for single-container mode, Railway deployment, reverse proxy setup, and more.
115+
99116
## Why Craftplan?
100117

101118
- **Purpose-built for artisanal manufacturing** — not a generic ERP adapted to fit; workflows are designed around small-batch, made-to-order production

config/runtime.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ if config_env() == :prod do
4444
You can generate one by calling: mix phx.gen.secret
4545
"""
4646

47-
host = System.get_env("PHX_HOST") || "example.com"
47+
host = System.get_env("HOST") || System.get_env("PHX_HOST") || "example.com"
4848
port = String.to_integer(System.get_env("PORT") || "4000")
4949

5050
config :craftplan, Craftplan.Repo,
@@ -86,7 +86,7 @@ if config_env() == :prod do
8686
secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
8787
region: System.get_env("AWS_REGION") || "us-east-1",
8888
s3: [
89-
scheme: "https://",
89+
scheme: System.get_env("AWS_S3_SCHEME") || "https://",
9090
host: System.get_env("AWS_S3_HOST") || "s3.amazonaws.com",
9191
region: System.get_env("AWS_REGION") || "us-east-1"
9292
]

docker-compose.dev.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Development services — PostgreSQL, MinIO, and Mailpit for local development.
2+
# Usage: docker compose -f docker-compose.dev.yml up -d
3+
4+
services:
5+
postgres:
6+
container_name: craftplan-postgres
7+
image: postgres:16
8+
ports:
9+
- 5432:5432
10+
environment:
11+
- POSTGRES_DB=craftplan_dev
12+
- POSTGRES_USER=postgres
13+
- POSTGRES_PASSWORD=postgres
14+
- PGDATA=/var/lib/postgresql/data/pgdata
15+
volumes:
16+
- ./.postgres:/var/lib/postgresql/data
17+
minio:
18+
container_name: craftplan-minio
19+
image: minio/minio:latest
20+
entrypoint: sh
21+
command: -c 'mkdir -p /data/craftplan && /usr/bin/minio server /data --console-address ":9001"'
22+
hostname: minio
23+
ports:
24+
- 9000:9000
25+
- 9001:9001
26+
environment:
27+
MINIO_ROOT_USER: minioadmin
28+
MINIO_ROOT_PASSWORD: minioadmin
29+
MINIO_ACCESS_KEY: "minio"
30+
MINIO_SECRET_KEY: "minio123"
31+
volumes:
32+
- .minio:/data
33+
deploy:
34+
restart_policy:
35+
condition: on-failure
36+
mailpit:
37+
container_name: craftplan-mailpit
38+
image: axllent/mailpit
39+
ports:
40+
- 1025:1025
41+
- 8025:8025

0 commit comments

Comments
 (0)