Cloud Native 14 min read

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.

Hacker Afternoon Tea
Hacker Afternoon Tea
Hacker Afternoon Tea
Run Multica Self‑Hosted with Three Commands: Safely Launch Three Containers

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 ./server

This 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.

Multica self‑host architecture diagram
Multica self‑host architecture diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DockersecurityPostgreSQLself-hostingMulticaadvisory-lock
Hacker Afternoon Tea
Written by

Hacker Afternoon Tea

You might find something interesting here ^_^

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.