Self-Hosting
Quick Start
Deploy Craftplan without cloning the repo. Just download the Compose file and start:
# 1. Download the Compose file and env template
curl -O https://raw.githubusercontent.com/puemos/craftplan/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/puemos/craftplan/main/.env.example
# 2. Create your environment file and fill in secrets (see below)
cp .env.example .env
# 3. Start everything
docker compose up -d
Craftplan will be available at http://localhost:4000 (or the PORT you configured). Migrations run automatically on startup.
First-time setup
On first launch, Craftplan shows a setup screen where you create your admin account. Once signed in, invite additional team members from Settings → Members — choose Staff or Admin role for each.
Single Container (docker run)
If you already have a PostgreSQL database and S3-compatible storage, you can run Craftplan as a single container:
docker run -d \
--name craftplan \
-p 4000:4000 \
-e SECRET_KEY_BASE="your-secret-key-base" \
-e TOKEN_SIGNING_SECRET="your-token-signing-secret" \
-e CLOAK_KEY="your-cloak-key" \
-e DATABASE_URL="ecto://postgres:password@your-db-host/craftplan" \
-e HOST="localhost" \
-e PORT="4000" \
-e AWS_ACCESS_KEY_ID="your-access-key" \
-e AWS_SECRET_ACCESS_KEY="your-secret-key" \
-e AWS_S3_BUCKET="craftplan" \
-e AWS_S3_SCHEME="https://" \
-e AWS_S3_HOST="s3.amazonaws.com" \
-e AWS_REGION="us-east-1" \
ghcr.io/puemos/craftplan:latest
Or with an env file:
docker run -d \
--name craftplan \
--env-file .env \
-e DATABASE_URL="ecto://postgres:password@your-db-host/craftplan" \
-p 4000:4000 \
ghcr.io/puemos/craftplan:latest
Deploy on Railway
Railway provisions a PostgreSQL database automatically. After deploying:
- Set the required environment variables (
SECRET_KEY_BASE,TOKEN_SIGNING_SECRET,CLOAK_KEY) in the Railway dashboard. - Set
HOSTto your Railway-provided domain. - For file storage, configure an external S3-compatible provider (e.g., AWS S3, Cloudflare R2, Tigris) using the
AWS_*environment variables.
Note: Railway does not include S3-compatible storage, so file uploads require an external provider.
Generating Secrets
Your .env file requires several secrets. Generate them with openssl:
SECRET_KEY_BASE and TOKEN_SIGNING_SECRET
Both are random 64-byte strings. Generate each one separately:
openssl rand -base64 48
Run the command twice – once for SECRET_KEY_BASE and once for TOKEN_SIGNING_SECRET.
CLOAK_KEY
A 32-byte AES key, base64-encoded:
openssl rand -base64 32
POSTGRES_PASSWORD
A password for the bundled PostgreSQL container (not needed if you supply your own DATABASE_URL):
openssl rand -base64 16
Configuration Reference
All configuration is done via environment variables in your .env file.
Required
| Variable | Description |
|---|---|
SECRET_KEY_BASE |
Phoenix secret for signing cookies and sessions |
TOKEN_SIGNING_SECRET |
Secret for API token signing |
CLOAK_KEY |
32-byte base64-encoded AES encryption key |
POSTGRES_PASSWORD |
Password for the PostgreSQL database |
Host & Networking
| Variable | Default | Description |
|---|---|---|
HOST |
localhost |
Public hostname (used in generated URLs) |
PORT |
4000 |
HTTP port the server listens on |
File Storage (S3 / SeaweedFS)
These defaults apply to the bundled Compose deployment. MinIO installations migrate automatically on the first upgrade; read the storage migration guide for capacity and recovery details.
| Variable | Compose default | Description |
|---|---|---|
AWS_ACCESS_KEY_ID |
MINIO_ROOT_USER or minioadmin |
Storage access key; change for production |
AWS_SECRET_ACCESS_KEY |
MINIO_ROOT_PASSWORD or minioadmin |
Storage secret; change for production |
AWS_S3_BUCKET |
craftplan |
Bucket created automatically |
AWS_S3_SCHEME |
http:// |
Internal S3 endpoint scheme |
AWS_S3_HOST |
seaweedfs |
Internal S3 endpoint hostname |
AWS_S3_PORT |
9000 |
Internal S3 endpoint port |
S3_PORT |
9000 |
Published host port (does not change the internal port) |
AWS_S3_PUBLIC_URL |
http://${HOST}:9000 |
Browser endpoint for signed photos; no bucket or path |
AWS_REGION |
us-east-1 |
Signing region |
AWS_ASSET_HOST |
unset | URL prefix for unsigned assets only |
With standalone docker run, specify all storage settings explicitly. The runtime
port defaults to 443 for HTTPS and 80 for HTTP when AWS_S3_PORT is absent.
Email (Optional)
Email is primarily configured from the Settings UI inside the app. Environment variables are a fallback for automated or headless setups.
| Variable | Description |
|---|---|
EMAIL_PROVIDER |
One of: sendgrid, postmark, brevo, mailgun, amazon_ses |
EMAIL_API_KEY |
API key for the chosen provider |
EMAIL_API_DOMAIN |
Mailgun domain (Mailgun only) |
EMAIL_API_SECRET |
Secret key (Amazon SES only) |
EMAIL_API_REGION |
AWS region (Amazon SES only, default: us-east-1) |
SMTP_HOST |
SMTP relay hostname (alternative to API providers) |
SMTP_PORT |
SMTP port (default: 587) |
SMTP_USERNAME |
SMTP username |
SMTP_PASSWORD |
SMTP password |
File Storage
Bundled SeaweedFS
The bundled storage image runs pinned SeaweedFS 4.51 and provides S3 storage on port 9000, creates the
configured bucket, and persists data in seaweedfs_data. Only the S3 port is
published; there is no MinIO console on port 9001. The service retains the name
minio so Compose replaces the old container during automatic migration.
Set AWS_S3_PUBLIC_URL to an endpoint reachable by users’ browsers. For an HTTPS
Craftplan site, use an HTTPS storage hostname behind your reverse proxy, forwarding
to port 9000 and preserving the original Host header. Otherwise browsers will block
HTTP photos as mixed content. Changing AWS_ASSET_HOST does not affect signed URLs.
Back up the entire SeaweedFS volume while the service is stopped, alongside your PostgreSQL backup. MinIO and SeaweedFS use different disk formats: never reuse the old MinIO volume for the new service.
External S3
Remove the minio service and its entry under craftplan.depends_on. Set
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET, AWS_REGION,
AWS_S3_SCHEME, AWS_S3_HOST, AWS_S3_PORT, and AWS_S3_PUBLIC_URL in .env.
For AWS S3, use your regional endpoint with HTTPS and port 443. The public URL is
the endpoint without a bucket/path, for example https://s3.us-east-1.amazonaws.com.
Reverse Proxy
For production deployments, place Craftplan behind a reverse proxy that handles TLS. Here’s an example nginx configuration:
upstream craftplan {
server 127.0.0.1:4000;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://craftplan;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Make sure to set HOST to your public domain (e.g. your-domain.com).
Note: The
UpgradeandConnectionheaders are required for Phoenix LiveView’s WebSocket connection.
Updating
docker compose pull
docker compose up -d
Migrations run automatically on each startup via the bin/server entrypoint.
Building from Source
If you are contributing to Craftplan or need a custom build, use the build-from-source Compose file:
git clone https://github.com/puemos/craftplan.git
cd craftplan
cp .env.example .env # fill in secrets
docker compose -f docker-compose.prod.yml up -d
Or build the image manually:
docker build -t craftplan .
See docker-compose.prod.yml for the full build-from-source configuration.