deploy: docker compose stack, api image, install script, proxy configs
Postgres and AMI secrets are hex (the password goes into DATABASE_URL). Task: M0-T6
This commit is contained in:
1 parent
909ac2d62d
commit
d0de0c9039
9 files changed
+269
-10
No files matched your search
@@ -0,0 +1,6 @@
|
||||
**/node_modules
|
||||
**/dist
|
||||
data
|
||||
.env
|
||||
.git
|
||||
asterisk
|
||||
@@ -0,0 +1,16 @@
|
||||
# --- required ---
|
||||
PUBLIC_HOSTNAME=pbx.example.com # DNS name browsers and phones use
|
||||
POSTGRES_PASSWORD=change-me # install.sh generates a random one (hex: it goes into a URL)
|
||||
AMI_SECRET=change-me # install.sh generates a random one
|
||||
APP_SECRET=change-me # 32 random bytes, base64. Encrypts secrets in the DB. NEVER change after first run.
|
||||
INITIAL_ADMIN_EMAIL=admin@example.com
|
||||
INITIAL_ADMIN_PASSWORD=change-me # only used if no admin exists yet
|
||||
|
||||
# --- optional ---
|
||||
TZ=America/Chicago
|
||||
API_PORT=8080
|
||||
POSTGRES_PORT=5433
|
||||
APP_UID=1000
|
||||
APP_GID=1000
|
||||
LOG_LEVEL=info
|
||||
TRUST_PROXY=true # true when behind Caddy/nginx
|
||||
@@ -0,0 +1,30 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
FROM node:24-trixie-slim AS build
|
||||
RUN npm install -g corepack@latest && corepack enable
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN pnpm install --frozen-lockfile \
|
||||
&& pnpm --filter @selfphoneng/shared build \
|
||||
&& pnpm --filter @selfphoneng/web build \
|
||||
&& pnpm --filter @selfphoneng/api build \
|
||||
&& pnpm --filter @selfphoneng/api deploy --legacy --prod /out
|
||||
|
||||
FROM node:24-trixie-slim
|
||||
# pg_dump must be the same major version as the server (18). Debian's own
|
||||
# postgresql-client is older, so use the official PostgreSQL apt repo (PGDG).
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg tini ca-certificates curl gnupg \
|
||||
&& install -d /usr/share/postgresql-common/pgdg \
|
||||
&& curl -fsSL -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||
&& . /etc/os-release \
|
||||
&& echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends postgresql-client-18 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/package.json ./package.json
|
||||
COPY --from=build /out/node_modules ./node_modules
|
||||
COPY --from=build /src/apps/api/dist ./dist
|
||||
COPY --from=build /src/apps/web/dist ./public
|
||||
# ponytail: migrations (M1) and provisioning templates (M10) get their COPY lines when they exist.
|
||||
ENV NODE_ENV=production
|
||||
ENTRYPOINT ["/usr/bin/tini","--"]
|
||||
CMD ["node","dist/index.js"]
|
||||
@@ -0,0 +1,10 @@
|
||||
{$PUBLIC_HOSTNAME} {
|
||||
encode zstd gzip
|
||||
@sipws path /ws
|
||||
handle @sipws {
|
||||
reverse_proxy 127.0.0.1:8088
|
||||
}
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# SelfPhoneNG behind your own nginx. Replace pbx.example.com and the certificate paths.
|
||||
#
|
||||
# If port 443 is handled by an nginx `stream {}` block that forwards with
|
||||
# `proxy_protocol`, this server must instead:
|
||||
# listen 127.0.0.1:<port> ssl proxy_protocol;
|
||||
# set_real_ip_from 127.0.0.1;
|
||||
# real_ip_header proxy_protocol;
|
||||
# Otherwise the api sees every client as 127.0.0.1, which breaks login rate
|
||||
# limiting and audit logs.
|
||||
|
||||
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name pbx.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/pbx.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/pbx.example.com/privkey.pem;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
client_max_body_size 50m; # audio uploads
|
||||
|
||||
# SIP over WebSocket -> Asterisk
|
||||
location = /ws {
|
||||
proxy_pass http://127.0.0.1:8088/ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
# Everything else (UI, API, live WebSocket /api/live, provisioning) -> api
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
name: selfphoneng
|
||||
|
||||
x-shared-volumes: &shared-volumes
|
||||
- ./data/asterisk-generated:/etc/asterisk/selfphoneng
|
||||
- ./data/voicemail:/var/spool/asterisk/voicemail
|
||||
- ./data/recordings:/var/spool/asterisk/monitor
|
||||
- ./data/sounds-custom:/var/lib/asterisk/sounds/custom
|
||||
- ./data/moh:/var/lib/asterisk/moh
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: selfphoneng
|
||||
POSTGRES_USER: selfphoneng
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env}
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql # PG 18 images keep data in /var/lib/postgresql/18/docker; mount the parent, NOT .../data
|
||||
ports:
|
||||
- '127.0.0.1:${POSTGRES_PORT:-5433}:5432'
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U selfphoneng -d selfphoneng']
|
||||
interval: 5s
|
||||
retries: 20
|
||||
|
||||
asterisk:
|
||||
build:
|
||||
context: ./asterisk
|
||||
args:
|
||||
ASTERISK_UID: ${APP_UID:-1000}
|
||||
ASTERISK_GID: ${APP_GID:-1000}
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
environment:
|
||||
AMI_SECRET: ${AMI_SECRET:?set in .env}
|
||||
TZ: ${TZ:-America/Chicago}
|
||||
volumes:
|
||||
- ./data/asterisk-generated:/etc/asterisk/selfphoneng
|
||||
- ./data/voicemail:/var/spool/asterisk/voicemail
|
||||
- ./data/recordings:/var/spool/asterisk/monitor
|
||||
- ./data/sounds-custom:/var/lib/asterisk/sounds/custom
|
||||
- ./data/moh:/var/lib/asterisk/moh
|
||||
- ./data/astdb:/var/lib/asterisk/db
|
||||
- ./data/asterisk-logs:/var/log/asterisk
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/api/Dockerfile
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: '${APP_UID:-1000}:${APP_GID:-1000}'
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
asterisk: { condition: service_started }
|
||||
env_file: .env
|
||||
environment:
|
||||
DATABASE_URL: postgres://selfphoneng:${POSTGRES_PASSWORD}@127.0.0.1:${POSTGRES_PORT:-5433}/selfphoneng
|
||||
AMI_HOST: 127.0.0.1
|
||||
AMI_PORT: '5038'
|
||||
AMI_USERNAME: selfphoneng
|
||||
volumes:
|
||||
- ./data/asterisk-generated:/etc/asterisk/selfphoneng
|
||||
- ./data/voicemail:/var/spool/asterisk/voicemail
|
||||
- ./data/recordings:/var/spool/asterisk/monitor
|
||||
- ./data/sounds-custom:/var/lib/asterisk/sounds/custom
|
||||
- ./data/moh:/var/lib/asterisk/moh
|
||||
- ./data/backups:/var/lib/selfphoneng/backups
|
||||
- ./data/tls:/var/lib/selfphoneng/tls
|
||||
|
||||
caddy:
|
||||
image: caddy:2
|
||||
profiles: ['caddy']
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
environment:
|
||||
PUBLIC_HOSTNAME: ${PUBLIC_HOSTNAME:?set in .env}
|
||||
volumes:
|
||||
- ./deploy/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./data/caddy:/data
|
||||
@@ -613,14 +613,18 @@ will see every client as 127.0.0.1, which breaks login rate limiting and audit l
|
||||
## 6. api image (`apps/api/Dockerfile`)
|
||||
|
||||
```dockerfile
|
||||
FROM node:24-bookworm-slim AS build
|
||||
RUN corepack enable
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
FROM node:24-trixie-slim AS build
|
||||
RUN npm install -g corepack@latest && corepack enable
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN pnpm install --frozen-lockfile && pnpm --filter @selfphoneng/shared build \
|
||||
&& pnpm --filter @selfphoneng/web build && pnpm --filter @selfphoneng/api build
|
||||
RUN pnpm install --frozen-lockfile \
|
||||
&& pnpm --filter @selfphoneng/shared build \
|
||||
&& pnpm --filter @selfphoneng/web build \
|
||||
&& pnpm --filter @selfphoneng/api build \
|
||||
&& pnpm --filter @selfphoneng/api deploy --legacy --prod /out
|
||||
|
||||
FROM node:24-bookworm-slim
|
||||
FROM node:24-trixie-slim
|
||||
# pg_dump must be the same major version as the server (18). Debian's own
|
||||
# postgresql-client is older, so use the official PostgreSQL apt repo (PGDG).
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg tini ca-certificates curl gnupg \
|
||||
@@ -631,16 +635,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg tini ca-
|
||||
&& apt-get update && apt-get install -y --no-install-recommends postgresql-client-18 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/package.json ./package.json
|
||||
COPY --from=build /out/node_modules ./node_modules
|
||||
COPY --from=build /src/apps/api/dist ./dist
|
||||
COPY --from=build /src/apps/api/node_modules ./node_modules
|
||||
COPY --from=build /src/apps/api/src/db/migrations ./migrations
|
||||
COPY --from=build /src/apps/api/src/provisioning/templates ./provisioning-templates
|
||||
COPY --from=build /src/apps/web/dist ./public
|
||||
# ponytail: migrations (M1) and provisioning templates (M10) get their COPY lines when they exist.
|
||||
ENV NODE_ENV=production
|
||||
ENTRYPOINT ["/usr/bin/tini","--"]
|
||||
CMD ["node","dist/index.js"]
|
||||
```
|
||||
(If pnpm's symlinked `node_modules` doesn't survive `COPY`, use `pnpm deploy --filter @selfphoneng/api --prod /out` and copy `/out` instead.)
|
||||
`pnpm deploy --legacy` copies the api with real (non-symlinked) production `node_modules`; `@selfphoneng/shared` ships its built `dist/` (`"files": ["dist"]`). Debian trixie matches the Asterisk image.
|
||||
|
||||
On start, `index.ts` does, in order: validate env → connect DB → run
|
||||
migrations → create the initial admin if none exists → start Fastify → connect AMI →
|
||||
|
||||
@@ -14,5 +14,8 @@
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# One-command install (spec 01 §1.3). Run from the repo root: ./scripts/install.sh
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# 1. Docker.
|
||||
if ! command -v docker >/dev/null 2>&1 || ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker with the compose plugin is required: https://docs.docker.com/engine/install/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Replace KEY=... in .env (values are safe for sed: hex, base64 and user input with | escaped).
|
||||
set_env() {
|
||||
v=$(printf '%s' "$2" | sed 's/[|&\\]/\\&/g')
|
||||
sed -i "s|^$1=.*|$1=$v|" .env
|
||||
}
|
||||
|
||||
# 2. .env with generated secrets.
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
set_env POSTGRES_PASSWORD "$(openssl rand -hex 32)" # hex: it is embedded in DATABASE_URL
|
||||
set_env AMI_SECRET "$(openssl rand -hex 32)"
|
||||
set_env APP_SECRET "$(openssl rand -base64 32)"
|
||||
printf 'Public hostname (e.g. pbx.example.com): '; read -r host
|
||||
printf 'Admin email: '; read -r email
|
||||
stty -echo 2>/dev/null || true
|
||||
printf 'Admin password (min 10 chars): '; read -r pw; echo
|
||||
stty echo 2>/dev/null || true
|
||||
set_env PUBLIC_HOSTNAME "$host"
|
||||
set_env INITIAL_ADMIN_EMAIL "$email"
|
||||
set_env INITIAL_ADMIN_PASSWORD "$pw"
|
||||
chmod 600 .env
|
||||
echo "Wrote .env"
|
||||
fi
|
||||
|
||||
# 3. Data folders owned by the app user.
|
||||
uid=$(sed -n 's/^APP_UID=\([0-9]*\).*/\1/p' .env); uid=${uid:-1000}
|
||||
gid=$(sed -n 's/^APP_GID=\([0-9]*\).*/\1/p' .env); gid=${gid:-1000}
|
||||
for d in postgres asterisk-generated voicemail recordings sounds-custom moh astdb asterisk-logs tls backups caddy; do
|
||||
mkdir -p "data/$d"
|
||||
done
|
||||
# postgres manages its own folder's ownership; everything else is shared by asterisk and the api.
|
||||
chown -R "$uid:$gid" data/asterisk-generated data/voicemail data/recordings data/sounds-custom data/moh \
|
||||
data/astdb data/asterisk-logs data/tls data/backups 2>/dev/null \
|
||||
|| sudo chown -R "$uid:$gid" data/asterisk-generated data/voicemail data/recordings data/sounds-custom \
|
||||
data/moh data/astdb data/asterisk-logs data/tls data/backups
|
||||
|
||||
# 4. Start.
|
||||
printf 'Use bundled Caddy for HTTPS? [y/N] '; read -r caddy
|
||||
case "$caddy" in
|
||||
[yY]*) docker compose --profile caddy up -d --build ;;
|
||||
*)
|
||||
docker compose up -d --build
|
||||
echo "Configure your reverse proxy from deploy/nginx/selfphoneng.conf.example"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 5. Done.
|
||||
host=$(sed -n 's/^PUBLIC_HOSTNAME=\([^ #]*\).*/\1/p' .env)
|
||||
echo "SelfPhoneNG is starting. Open https://$host/ (health: curl http://127.0.0.1:8080/healthz)"
|
||||
Reference in new issue
Block a user