Run Multica Self‑Hosted with Three Commands: Safely Launch Three Containers
The article explains how to self‑host Multica using a three‑container Docker Compose setup—PostgreSQL, backend, and frontend—detailing secure defaults, health‑checked service dependencies, advisory‑lock‑based migrations, loopback port bindings, and two launch methods (official images or local builds) that let you start the system with just three commands.
Overview
The final piece of the Multica deconstruction series shows how to run the whole system on a personal machine. Multica’s self‑hosting consists of three containers (PostgreSQL, backend, and frontend) orchestrated by docker-compose.selfhost.yml. The design emphasizes security, reliable startup ordering, and minimal user interaction.
Container Topology and Docker Compose
The backend connects to PostgreSQL for SQL queries and pgvector‑based vector search, while the frontend never talks directly to the database. Two named volumes, pgdata (database) and backend_uploads (attachments), preserve data across container recreation.
services:
postgres:
image: pgvector/pgvector:pg17
healthcheck:
test: ["CMD-SHELL", "pg_isready -U multica -d multica"]
volumes:
- pgdata:/var/lib/postgresql/data
backend:
image: ghcr.io/multica-ai/multica-backend:latest
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:8080:8080"
frontend:
image: ghcr.io/multica-ai/multica-web:latest
depends_on:
- backend
ports:
- "127.0.0.1:3000:3000"Two subtle details:
The PostgreSQL image is pgvector/pgvector:pg17, which includes the pgvector extension needed for semantic search.
The depends_on clause uses condition: service_healthy together with a healthcheck, ensuring the backend starts only after PostgreSQL is truly ready.
Two Startup Paths
Multica offers two ways to launch:
Official images : Run make selfhost. The Makefile creates a .env from .env.example, generates random JWT_SECRET and POSTGRES_PASSWORD with openssl rand -hex, pulls the images, starts them, and performs a health‑check loop that curls http://localhost:8080/health up to 30 times.
Local builds : Run make selfhost-build. This overrides the compose file with docker-compose.selfhost.build.yml, building multica-backend:dev and multica-web:dev from local Dockerfile s. The :dev tags avoid overwriting previously pulled :latest images, allowing easy switching between stable and modified builds.
Entry‑point Migration Logic
The backend container does not start the server directly. Its entrypoint.sh runs a minimal migration step before launching the server:
#!/bin/sh
set -e
echo "Running database migrations..."
./migrate up
echo "Starting server..."
exec ./serverThis ensures every container startup applies any pending schema changes. In multi‑replica scenarios, each instance runs the migration; PostgreSQL’s advisory lock serialises the operation, preventing conflicts.
Advisory Lock Implementation
The migration code acquires a single connection from the pool and calls SELECT pg_advisory_lock($1). Because the lock is session‑scoped, keeping the same connection for the whole migration loop guarantees the lock remains effective.
conn, err := pool.Acquire(ctx)
defer conn.Release()
conn.Exec(ctx, "SELECT pg_advisory_lock($1)", lockKey)
// ... run migrations ...
conn.Exec(ctx, "SELECT pg_advisory_unlock($1)", lockKey)Each migration version checks the schema_migrations table; already‑applied versions are skipped, making the process fast after the initial run.
Transactional Considerations
Migrations are not wrapped in a single transaction because some files use CREATE INDEX CONCURRENTLY, which PostgreSQL forbids inside a transaction block. This design accepts that a failing migration won’t roll back earlier steps, but it enables safe concurrent index creation in production.
Security‑Focused Port Binding
All exposed ports bind to 127.0.0.1 instead of 0.0.0.0. Docker’s default iptables rules bypass host firewalls, so binding to the loopback interface prevents accidental internet exposure of the default JWT secret and Postgres password.
Services bind to 127.0.0.1 only. Do NOT change these bindings to 0.0.0.0 — Docker bypasses host firewalls (UFW/iptables) by default, exposing raw ports to the internet with default credentials.
If external access is required, the recommended approach is to place a reverse proxy (Caddy, Nginx, Cloudflare Tunnel) in front, terminating TLS and forwarding to the loopback ports.
Login‑Related Pitfall
In production mode, login relies on email verification codes. Without an email service, the code is printed in backend logs. The .env variable MULTICA_DEV_VERIFICATION_CODE can be set to a fixed value for testing, but the comments warn never to use this in a publicly reachable instance.
Conclusion
Multica’s self‑hosting solution packs the entire stack into three containers that can be started with three commands while handling security, migration concurrency, and reliable process management out of the box. The design choices—health‑checked dependencies, advisory‑lock‑based migrations, loopback port bindings, and a robust entrypoint—allow a small team to operate the system safely without deep Docker networking knowledge.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
