iBoard - Deploy
Deployment Guide
Overview
The application is packaged as an Elixir release and shipped as a Docker image
(iboard/blog:latest). On container start the entrypoint automatically:
-
Loads
/app/.envif present - Creates the PostgreSQL database if it does not yet exist
- Runs all pending Elixir migrations
- Starts the Phoenix server
No manual migration step is required after deploying a new image.
Quick-start package
A ready-to-edit deployment package is included in the repository:
tar -xzf deploy.tgz -C /your/deploy/dir
Contents:
.env # production environment variables — fill in the blanks
docker-compose.yml # nginx + web + db stack (no extra editing needed)
nginx/conf.d/app.conf # nginx server blocks — replace "example.com" with your domain
nginx/certs/.gitkeep # place fullchain.pem and privkey.pem here
data/postgres/ # postgres data volume (created on first start)
data/uploads/ # user-upload volume (created on first start)
README.txt # five-step quick-start checklist
Minimum steps after extracting:
-
Edit
.env— setENDPOINT_HOST,SECRET_KEY_BASE,PGPASSWORD, andSENDGRID_API_KEY(or switch toMAILER_ADAPTER=local) -
Copy TLS certificates into
nginx/certs/ -
Replace
example.cominnginx/conf.d/app.confwith your domain -
docker compose pull && docker compose up -d
Environment variables
All configuration is read at runtime from environment variables. There are no compiled-in secrets.
Endpoint
| Variable | Description | Default |
|---|---|---|
ENDPOINT_HOST |
Required. Public hostname used in generated URLs and email links (e.g. example.com) |
— |
ENDPOINT_SCHEME |
URL scheme: http or https |
https |
ENDPOINT_PORT |
TCP port Bandit binds to inside the container |
4000 |
ENDPOINT_IP |
IP Bandit binds to. 0.0.0.0 = all IPv4, :: = all IPv4+IPv6 |
0.0.0.0 |
PHX_SERVER |
Set to true to start the HTTP server (always set by docker-compose.yml) |
— |
Note:
ENDPOINT_HOSTandENDPOINT_SCHEMEare used for URL generation only (email links, redirects). The application always binds onENDPOINT_PORTregardless of the scheme. In a typical setup nginx terminates TLS on port 443 and reverse-proxies toENDPOINT_PORT.
Database
| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
Required. Ecto database URL |
ecto://user:pass@host/dbname |
POOL_SIZE |
Database connection pool size |
10 |
ECTO_IPV6 |
Set to true to connect over IPv6 |
false |
Security
| Variable | Description |
|---|---|
SECRET_KEY_BASE |
Required. 64-byte secret used for cookies and tokens. Generate with mix phx.gen.secret. |
Mailer
| Variable | Description | Default |
|---|---|---|
MAILER_ADAPTER |
local — in-memory, /dev/mailbox UI available; sendgrid — sends real email |
local |
SENDGRID_API_KEY |
Required when MAILER_ADAPTER=sendgrid |
— |
Other
| Variable | Description | Default |
|---|---|---|
DNS_CLUSTER_QUERY |
DNS query for clustering multiple nodes | — |
LOG_LEVEL |
Logger level: debug, info, warning, error |
info |
Docker
Prerequisites
- Docker and Docker Compose installed on the host
-
An
.envfile in the same directory asdocker-compose.yml
Minimal .env for Docker
# Phoenix
SECRET_KEY_BASE=<output of: mix phx.gen.secret>
ENDPOINT_HOST=example.com
ENDPOINT_SCHEME=https
ENDPOINT_PORT=4000
# Port exposed to the host (nginx reverse-proxies to this)
EXPOSE_PORT=4000
# PostgreSQL
PGUSER=blog
PGPASSWORD=<strong password>
PGDATABASE=blog_prod
# Mailer
MAILER_ADAPTER=sendgrid
SENDGRID_API_KEY=SG.<your key>
Start
docker compose up -d
This starts:
-
db— PostgreSQL 16, data stored in./data/postgres/ -
web— Phoenix application, uploads stored in./data/uploads/
The database is only reachable inside the Docker network. The web service is
exposed on EXPOSE_PORT (default 4000).
Stop / restart
docker compose down # stop and remove containers (data volumes are preserved)
docker compose restart web # restart only the web service
View logs
docker compose logs -f web
docker compose logs -f db
Building the Docker image
A build.sh script is provided:
./build.sh # builds iboard/blog:latest
./build.sh --push # builds and pushes to Docker Hub
The build uses a multi-stage Dockerfile:
-
Builder —
hexpm/elixirimage; installs npm, compiles assets, builds the Elixir release -
Runner —
debian:bookworm-slim; copies only the release artifact
Nginx reverse proxy (example)
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:4000;
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;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
Backups
Two directories on the host contain all persistent data:
| Path | Contents |
|---|---|
./data/postgres/ |
PostgreSQL data directory |
./data/uploads/ |
User-uploaded files (avatars, post attachments) |
Back up both directories to preserve the full application state.
Releases (without Docker)
To build a release tarball directly:
MIX_ENV=prod mix assets.deploy
MIX_ENV=prod mix release
The tarball is written to _build/prod/rel/w_app_core/. Extract it on the
target host and run:
# Set all required environment variables, then:
bin/entrypoint.sh start
The entrypoint handles .env loading, upload directory setup, DB creation, and
migrations before starting the server.
Also, see README