Intermediate May 3, 2026 · 3 min read

Take a Loom project from loom serve on your laptop to a running production container. Uses the artefacts loom deploy produces.

What you'll have at the end

  • A Docker image that runs loom serve against your Threads
  • A production database on PlanetScale (or any MySQL 8 host)
  • Secrets supplied via env vars — no plaintext in the image
  • A health-checked container that auto-restarts

Step 1 — Run loom deploy

cd my-app
loom deploy

This emits three files:

Dockerfile           # multi-stage build, runs as non-root
.dockerignore        # excludes .git/, .env, .loom/pending-reports/
deploy/start.sh      # required-env-var check + exec loom serve

loom deploy --force regenerates all three (use after upgrading the Loom CLI to a newer version).

Step 2 — Review the Dockerfile

The shipped Dockerfile is minimal:

FROM golang:1.24-alpine AS build
WORKDIR /src
RUN apk add --no-cache git ca-certificates
RUN go install github.com/orbweaver-dev/loom@latest

FROM alpine:3.20
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 loom
COPY --from=build /go/bin/loom /usr/local/bin/loom

WORKDIR /app
COPY --chown=loom:loom loom.yaml ./
COPY --chown=loom:loom threads/ ./threads/
COPY --chown=loom:loom custom/ ./custom/

USER loom
EXPOSE 8080
ENV LOOM_PORT=8080
ENTRYPOINT ["loom", "serve"]

Notes:

  • The Loom binary is fetched in the build stage via go install. Pin a specific version in production: RUN go install github.com/orbweaver-dev/loom@v0.1.0.
  • custom/ is copied even if empty — that's where ejected models live.
  • The runtime image runs as a non-root loom user (UID 10001).
  • .loom/ is not copied — it's regenerated on demand and contains nothing the running server needs in interpreter mode.

Step 3 — Apply migrations against production

Before deploying the container, your production database needs the right schema. Connect to the production DSN:

export DATABASE_URL='mysql://...prod-branch...'
loom stitch --preview      # always preview first
loom stitch -y             # apply

For PlanetScale: stitch against a feature branch, then open a deploy request to merge into main.

Step 4 — Build and push the image

docker build -t registry.example.com/my-app:v0.1.0 .
docker push registry.example.com/my-app:v0.1.0

Step 5 — Run the container

docker run -d \
  --name my-app \
  --restart unless-stopped \
  -p 8080:8080 \
  -e DATABASE_URL='mysql://...prod...' \
  -e LOOM_SECRET="$LOOM_SECRET" \
  registry.example.com/my-app:v0.1.0

For LOOM_SECRET, source from your secret manager (Vault, AWS Secrets Manager, Kubernetes Secret, etc.) — never bake it into the image.

Step 6 — Verify

curl http://localhost:8080/healthz
# {"ok":true,"threads":3,"plugins":0}

If threads is 0, your threads/ directory didn't make it into the image — check .dockerignore.

Health checks

For Kubernetes:

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 2
  periodSeconds: 3

For Docker Compose:

healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
  interval: 10s
  timeout: 3s
  retries: 3

Logging

The runtime logs to stdout via log/slog in JSON format by default. Pipe to your log aggregator (Loki, Datadog, CloudWatch). Each request emits one line with method, path, status, dur_ms, req_id, and the requesting remote address.

Scaling

Loom's runtime is stateless — every request loads its data from the database. To scale horizontally, run multiple containers behind a load balancer. JWTs are HS256-signed; every replica needs the same LOOM_SECRET to verify them.

What's not in the box

  • TLS termination. Run behind a reverse proxy (nginx, Caddy, ALB) or use a service mesh.
  • Rate limiting. Same — terminate upstream.
  • Auto-migrations on container start. The Dockerfile does NOT run loom stitch at boot; that's a separate step you do when you're ready to apply schema changes. Coupling them risks unintended migrations on every deploy.

See also

  • Getting Started → Connecting to PlanetScale — DSN format
  • Reference → CLI Command Referenceloom deploy and loom serve flags
Was this article helpful?