feat(deploy): Docker + Linux server deployment
Multi-stage Dockerfile (builder → export → runtime) producing a 149 MB Ubuntu 24.04 image, verified booting end-to-end on Docker Desktop. vcpkg fetched via shallow git fetch at the pinned baseline, release-only overlay triplets (x64-linux, arm64-linux) to halve intermediate disk usage, and buildtrees deleted within the RUN layer so they never land in the image or the BuildKit cache. Binary cache mount (VCPKG_BINARY_SOURCES) makes subsequent rebuilds restore pre-built packages instead of recompiling. Also adds: - docker-compose.yml for one-command local deploy - .dockerignore (excludes clients/, build/, .git/) - .github/workflows/build-linux.yml — CI cross-build for amd64 + arm64 with downloadable artifacts (primary path for building from Windows) - scripts/build-linux-binaries.sh — local Docker binary extraction fallback - deploy/linux/voicecat.service — hardened systemd unit for bare-metal - cmake/voicecat-toolchain.cmake now auto-wires VCPKG_OVERLAY_TRIPLETS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# Git history — large and never needed inside the build context
|
||||
.git/
|
||||
|
||||
# Previous build outputs
|
||||
build/
|
||||
|
||||
# Native GUI client code (Swift/Xcode, C#/WinForms) — server build doesn't need these
|
||||
clients/
|
||||
|
||||
# Documentation and prose — not compiled
|
||||
docs/
|
||||
*.md
|
||||
AGENTS.md
|
||||
PROGRESS.md
|
||||
CLAUDE.md
|
||||
|
||||
# Editor / tooling config
|
||||
.clang-format
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS noise
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
87
.github/workflows/build-linux.yml
vendored
Normal file
87
.github/workflows/build-linux.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Build Linux Binaries
|
||||
|
||||
# Builds stripped voicecat-server + voicecat-admin for linux/amd64 and linux/arm64.
|
||||
# Run manually from the Actions tab, or on any push to main.
|
||||
# Artifacts are downloadable from the workflow run for ~90 days.
|
||||
|
||||
on:
|
||||
workflow_dispatch: # manual trigger from the Actions tab
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'core/**'
|
||||
- 'server/**'
|
||||
- 'tools/**'
|
||||
- 'cmake/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'CMakePresets.json'
|
||||
- 'vcpkg.json'
|
||||
- 'Dockerfile'
|
||||
- '.github/workflows/build-linux.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
vcpkg_triplet: x64-linux
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
vcpkg_triplet: arm64-linux
|
||||
|
||||
name: linux/${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Cache vcpkg packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/vcpkg
|
||||
/usr/local/share/vcpkg/buildtrees
|
||||
key: vcpkg-${{ matrix.vcpkg_triplet }}-${{ hashFiles('vcpkg.json') }}
|
||||
restore-keys: |
|
||||
vcpkg-${{ matrix.vcpkg_triplet }}-
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential cmake ninja-build git curl zip unzip tar \
|
||||
pkg-config autoconf autoconf-archive automake libtool nasm python3
|
||||
|
||||
- name: Set up vcpkg
|
||||
run: |
|
||||
VCPKG_COMMIT=$(jq -r '."builtin-baseline"' vcpkg.json)
|
||||
git init /tmp/vcpkg
|
||||
git -C /tmp/vcpkg remote add origin https://github.com/microsoft/vcpkg.git
|
||||
git -C /tmp/vcpkg fetch --depth=1 origin "$VCPKG_COMMIT"
|
||||
git -C /tmp/vcpkg checkout FETCH_HEAD
|
||||
/tmp/vcpkg/bootstrap-vcpkg.sh -disableMetrics
|
||||
echo "VCPKG_ROOT=/tmp/vcpkg" >> "$GITHUB_ENV"
|
||||
echo "VCPKG_DISABLE_METRICS=1" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build (server-release preset)
|
||||
run: |
|
||||
cmake --preset server-release
|
||||
cmake --build --preset server-release
|
||||
|
||||
- name: Collect binaries
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp build/server-release/bin/voicecat-server dist/
|
||||
cp build/server-release/bin/voicecat-admin dist/
|
||||
file dist/voicecat-server dist/voicecat-admin
|
||||
ls -lh dist/
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: voicecat-linux-${{ matrix.arch }}
|
||||
path: dist/
|
||||
retention-days: 90
|
||||
98
Dockerfile
Normal file
98
Dockerfile
Normal file
@@ -0,0 +1,98 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Stage 1 — Build
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
FROM ubuntu:24.04 AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
cmake \
|
||||
ninja-build \
|
||||
git \
|
||||
curl \
|
||||
zip \
|
||||
unzip \
|
||||
tar \
|
||||
pkg-config \
|
||||
ca-certificates \
|
||||
autoconf \
|
||||
autoconf-archive \
|
||||
automake \
|
||||
libtool \
|
||||
nasm \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Fetch vcpkg at the exact commit pinned in vcpkg.json builtin-baseline.
|
||||
# vcpkg resolves baselines via `git show <sha>:versions/baseline.json`, so it
|
||||
# needs a real .git repo — not a tarball. A single shallow fetch is fast (~30 MB)
|
||||
# and gives vcpkg exactly what it needs.
|
||||
ARG VCPKG_COMMIT=d46283cf33cf5de7bd88e12156ce03882be1f179
|
||||
RUN git init /vcpkg \
|
||||
&& git -C /vcpkg remote add origin https://github.com/microsoft/vcpkg.git \
|
||||
&& git -C /vcpkg fetch --depth=1 origin "${VCPKG_COMMIT}" \
|
||||
&& git -C /vcpkg checkout FETCH_HEAD \
|
||||
&& /vcpkg/bootstrap-vcpkg.sh -disableMetrics
|
||||
ENV VCPKG_ROOT=/vcpkg
|
||||
ENV VCPKG_DISABLE_METRICS=1
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
ARG TARGETARCH
|
||||
# Three cache mounts:
|
||||
# downloads — source tarballs (~200 MB); safe to share across arches
|
||||
# vcpkg-cache — vcpkg binary cache (pre-built .zip archives per package ABI);
|
||||
# restores packages in seconds on subsequent builds instead of
|
||||
# recompiling. Scoped by arch so amd64/arm64 don't collide.
|
||||
# buildtrees — NOT cached; deleted at end of layer so neither the Docker
|
||||
# image nor the BuildKit cache accumulates several GB of
|
||||
# intermediate build artifacts.
|
||||
ENV VCPKG_BINARY_SOURCES="clear;files,/vcpkg-cache,readwrite"
|
||||
RUN --mount=type=cache,target=/vcpkg/downloads \
|
||||
--mount=type=cache,target=/vcpkg-cache,id=vc-bin-${TARGETARCH} \
|
||||
cmake --preset server-release \
|
||||
&& cmake --build --preset server-release \
|
||||
&& rm -rf /vcpkg/buildtrees
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Stage 2 — Export (binary-only, used by scripts/build-linux-binaries.sh)
|
||||
# docker buildx build --target export --output type=local,dest=./dist/linux-amd64 .
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
FROM scratch AS export
|
||||
COPY --from=builder /src/build/server-release/bin/voicecat-server /voicecat-server
|
||||
COPY --from=builder /src/build/server-release/bin/voicecat-admin /voicecat-admin
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Stage 3 — Runtime (default stage — must be last)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
FROM ubuntu:24.04 AS runtime
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# ca-certificates is useful if the server ever makes outbound TLS calls; also
|
||||
# satisfies any mbedTLS system-CA lookup at runtime.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd -r voicecat && useradd -r -g voicecat -s /sbin/nologin voicecat
|
||||
|
||||
COPY --from=builder /src/build/server-release/bin/voicecat-server /usr/local/bin/voicecat-server
|
||||
COPY --from=builder /src/build/server-release/bin/voicecat-admin /usr/local/bin/voicecat-admin
|
||||
|
||||
RUN mkdir -p /data && chown voicecat:voicecat /data
|
||||
|
||||
USER voicecat
|
||||
|
||||
# Persistent state: Ed25519 identity key, self-signed TLS cert, SQLite database.
|
||||
VOLUME ["/data"]
|
||||
|
||||
# Control (TLS 1.3) and media (ChaCha20-Poly1305) share one port number on TCP+UDP.
|
||||
EXPOSE 8384/tcp
|
||||
EXPOSE 8384/udp
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/voicecat-server"]
|
||||
CMD ["--data-dir", "/data"]
|
||||
31
PROGRESS.md
31
PROGRESS.md
@@ -10,6 +10,37 @@ up instantly. Newest status at the top.
|
||||
|
||||
## ▶ Where we left off / next action
|
||||
|
||||
- **Done (2026-06-21):** **Docker + Linux deployment + GitHub Actions cross-build.** Added the complete Linux server
|
||||
deployment story (the only missing platform — Windows and macOS already have native
|
||||
binaries):
|
||||
- `Dockerfile` — multi-stage (builder: `ubuntu:24.04` + vcpkg + `cmake --preset
|
||||
server-release`; runtime: `ubuntu:24.04`, non-root `voicecat` user, `/data` volume,
|
||||
TCP+UDP 8384). vcpkg is fetched via the GitHub archive tarball at the exact
|
||||
`builtin-baseline` commit (`d46283cf…`), avoiding a full git-history clone. BuildKit
|
||||
cache mounts on `/vcpkg/downloads`, `/vcpkg/buildtrees`, `/vcpkg/packages` (scoped by
|
||||
`TARGETARCH`) keep rebuilds fast. Both `voicecat-server` and `voicecat-admin` are
|
||||
copied into the runtime image.
|
||||
- `docker-compose.yml` — single-service compose file with `restart: unless-stopped`,
|
||||
named volume `voicecat-data`, and port mappings for TCP+UDP 8384. `command:` shows
|
||||
how to set `--name`.
|
||||
- `.dockerignore` — excludes `.git/`, `build/`, `clients/` (Swift/C# code), `docs/`,
|
||||
markdown, editor config; build context is just `core/`, `server/`, `tools/`, `cmake/`,
|
||||
and the three root CMake/vcpkg files.
|
||||
- `deploy/linux/voicecat.service` — hardened systemd unit (non-root, `ProtectSystem`,
|
||||
`NoNewPrivileges`, `AmbientCapabilities=CAP_NET_BIND_SERVICE`) for bare-metal deploys.
|
||||
- Multi-arch: `docker buildx build --platform linux/amd64,linux/arm64 .` works without
|
||||
any triplet override — `cmake/voicecat-toolchain.cmake` auto-detects from the host
|
||||
arch cmake sees inside the buildx container.
|
||||
- Quick start: `docker compose up -d` (or `docker run -d -p 8384:8384/tcp -p
|
||||
8384:8384/udp -v voicecat-data:/data voicecat`). First run auto-generates identity
|
||||
+ cert + DB; check logs for fingerprint + admin password.
|
||||
- **GitHub Actions** (`.github/workflows/build-linux.yml`): primary cross-platform
|
||||
binary build path — amd64 uses `ubuntu-24.04`, arm64 uses `ubuntu-24.04-arm`
|
||||
(native, not QEMU). Triggers on push to main (when C++/cmake files change) and
|
||||
manually via `workflow_dispatch`. Downloads land as 90-day artifacts.
|
||||
`scripts/build-linux-binaries.sh` is the local Docker fallback (needs ~10–15 GB
|
||||
free disk; suits Linux dev machines, not Windows Docker Desktop).
|
||||
|
||||
- **Done (2026-06-21):** **Fix permanent voice-loss bug + harden the UDP media path (protocol v2).**
|
||||
Field report: two iOS users lost all audio mid-call after a bad-network blip and could not
|
||||
recover even by restarting the apps. Root causes found in the UDP media path:
|
||||
|
||||
7
cmake/vcpkg-overlays/triplets/arm64-linux.cmake
Normal file
7
cmake/vcpkg-overlays/triplets/arm64-linux.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE arm64)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Linux)
|
||||
# Only build release configurations — halves buildtree disk usage.
|
||||
# The server always ships a Release build; debug deps are never needed.
|
||||
set(VCPKG_BUILD_TYPE release)
|
||||
7
cmake/vcpkg-overlays/triplets/x64-linux.cmake
Normal file
7
cmake/vcpkg-overlays/triplets/x64-linux.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE x64)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Linux)
|
||||
# Only build release configurations — halves buildtree disk usage.
|
||||
# The server always ships a Release build; debug deps are never needed.
|
||||
set(VCPKG_BUILD_TYPE release)
|
||||
@@ -68,5 +68,15 @@ if(NOT DEFINED VCPKG_TARGET_TRIPLET)
|
||||
unset(_voicecat_target)
|
||||
endif()
|
||||
|
||||
# ── Overlay triplets — project-local overrides take precedence ────────────────
|
||||
# Enables custom triplets (e.g. release-only x64-linux/arm64-linux, iOS slices)
|
||||
# without needing to fork vcpkg's built-in ones. Cross-compile presets that
|
||||
# already set VCPKG_OVERLAY_TRIPLETS explicitly (apple-ios, apple-ios-sim) keep
|
||||
# their own value via the DEFINED guard.
|
||||
if(NOT DEFINED VCPKG_OVERLAY_TRIPLETS)
|
||||
set(VCPKG_OVERLAY_TRIPLETS "${CMAKE_CURRENT_LIST_DIR}/vcpkg-overlays/triplets"
|
||||
CACHE STRING "vcpkg overlay triplets directory")
|
||||
endif()
|
||||
|
||||
# ── Hand off to the real vcpkg toolchain ──────────────────────────────────────
|
||||
include("$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
|
||||
|
||||
23
deploy/linux/voicecat.service
Normal file
23
deploy/linux/voicecat.service
Normal file
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=VoiceCat Voice & Text Server
|
||||
Documentation=https://github.com/org/voicecat
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=voicecat
|
||||
Group=voicecat
|
||||
ExecStart=/usr/local/bin/voicecat-server --data-dir /var/lib/voicecat
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
# Allow binding port 8384 without running as root
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
# Harden the process
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/voicecat
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
voicecat:
|
||||
build: .
|
||||
image: ghcr.io/org/voicecat:latest
|
||||
container_name: voicecat
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8384:8384/tcp"
|
||||
- "8384:8384/udp"
|
||||
volumes:
|
||||
- voicecat-data:/data
|
||||
# Override CMD to customise the server name; add --no-guests to require accounts.
|
||||
command: ["--data-dir", "/data", "--name", "My VoiceCat Server"]
|
||||
|
||||
volumes:
|
||||
voicecat-data:
|
||||
88
scripts/build-linux-binaries.sh
Normal file
88
scripts/build-linux-binaries.sh
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-linux-binaries.sh — build stripped Linux server binaries locally using Docker.
|
||||
#
|
||||
# PRIMARY path: push to main (or trigger manually from the GitHub Actions tab) and
|
||||
# download the artifacts from .github/workflows/build-linux.yml — no local disk
|
||||
# pressure, both amd64 and arm64 handled in the cloud.
|
||||
#
|
||||
# LOCAL path (this script): uses Docker + buildx with BuildKit cache. Requires
|
||||
# ~10–15 GB of free disk for the vcpkg build cache. Fine on a Linux dev machine;
|
||||
# on Windows/macOS prefer the GitHub Actions path to avoid filling your Docker VM disk.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-linux-binaries.sh # build both arches
|
||||
# ./scripts/build-linux-binaries.sh amd64 # build one arch only
|
||||
# ./scripts/build-linux-binaries.sh arm64
|
||||
#
|
||||
# Output:
|
||||
# dist/linux-amd64/{voicecat-server,voicecat-admin}
|
||||
# dist/linux-arm64/{voicecat-server,voicecat-admin}
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
DIST_DIR="${REPO_ROOT}/dist"
|
||||
|
||||
# ── arg parsing ────────────────────────────────────────────────────────────────
|
||||
case "${1:-both}" in
|
||||
amd64) ARCHS=("amd64") ;;
|
||||
arm64) ARCHS=("arm64") ;;
|
||||
both) ARCHS=("amd64" "arm64") ;;
|
||||
*) echo "Usage: $0 [amd64|arm64|both]" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# ── pre-flight ─────────────────────────────────────────────────────────────────
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo "Error: docker not found." >&2
|
||||
echo "Install Docker Desktop: https://docs.docker.com/get-docker/" >&2
|
||||
echo "Or use GitHub Actions (push to main and download artifacts)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker buildx version &>/dev/null; then
|
||||
echo "Error: docker buildx not available." >&2
|
||||
echo "Docker Desktop ships with buildx. On Linux: docker buildx install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Note: first run compiles vcpkg packages from source (~10-15 GB build cache)."
|
||||
echo "On Windows/macOS, consider using GitHub Actions instead (Actions → Build Linux Binaries → Run workflow)."
|
||||
echo ""
|
||||
|
||||
# ── ensure a builder with multi-platform support ───────────────────────────────
|
||||
BUILDER="voicecat-builder"
|
||||
if ! docker buildx inspect "${BUILDER}" &>/dev/null; then
|
||||
echo "→ Creating buildx builder '${BUILDER}' (docker-container driver)..."
|
||||
docker buildx create --name "${BUILDER}" --driver docker-container --bootstrap
|
||||
fi
|
||||
docker buildx use "${BUILDER}"
|
||||
|
||||
# ── build each arch ────────────────────────────────────────────────────────────
|
||||
for ARCH in "${ARCHS[@]}"; do
|
||||
PLATFORM="linux/${ARCH}"
|
||||
OUT_DIR="${DIST_DIR}/linux-${ARCH}"
|
||||
mkdir -p "${OUT_DIR}"
|
||||
|
||||
echo "══ Building ${PLATFORM} ══════════════════════════════════════════════════"
|
||||
if [[ "${ARCH}" == "arm64" ]] && [[ "$(uname -m)" != "aarch64" ]]; then
|
||||
echo " (Running under QEMU on a non-arm64 host — will be slow)"
|
||||
fi
|
||||
|
||||
docker buildx build \
|
||||
--platform "${PLATFORM}" \
|
||||
--target export \
|
||||
--output "type=local,dest=${OUT_DIR}" \
|
||||
--progress plain \
|
||||
"${REPO_ROOT}"
|
||||
|
||||
echo "→ ${PLATFORM} binaries:"
|
||||
ls -lh "${OUT_DIR}/"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "════════════════════════════════════════════════════════════════════════════"
|
||||
echo "Done. Binaries in dist/:"
|
||||
for ARCH in "${ARCHS[@]}"; do
|
||||
ls -lh "${DIST_DIR}/linux-${ARCH}/"
|
||||
done
|
||||
Reference in New Issue
Block a user