DEV Community

flisk
flisk

Posted on

Run pressly/goose Migrations as a Flisk One-Shot Job

The third Goose is a database migrator: pressly/goose. People type goose in a terminal and mean goose up. Flisk will not grow a --language goose for that. Migrations are a job that runs once and exits, which is the shape Flisk already documents for dbmate, Atlas, Prisma, and Drizzle.

This post wires goose that way, next to a real Flisk backend. Local only. Your production migrator stays in your pipeline.

Split the processes

Process Owns Restarts?
API (--language go or python) HTTP, GET /health yes, with the stack
goose job schema up/down no, one-shot
Postgres data Flisk's local database, not goose

Do not bake goose up into the API container entrypoint. A crash loop will re-apply migrations, and a health check will lie about whether the schema move finished.

Layout

shop/
  billing/
    service.json          # flisk backend
    src/ ...
  billing-migrate/
    service.json          # one-shot
    Dockerfile
    migrations/
      20261022120000_init.sql
Enter fullscreen mode Exit fullscreen mode

SQL migrations stay in the job folder so the API image does not need the goose binary.

-- billing-migrate/migrations/20261022120000_init.sql
-- +goose Up
CREATE TABLE IF NOT EXISTS invoices (
  id uuid PRIMARY KEY,
  status text NOT NULL
);

-- +goose Down
DROP TABLE IF EXISTS invoices;
Enter fullscreen mode Exit fullscreen mode

Job image

Use a pinned goose image or copy a pinned binary. Do not go install at container start.

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
  && rm -rf /var/lib/apt/lists/*
ARG GOOSE_VERSION=3.24.1
ADD [https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64](https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64) /usr/local/bin/goose
RUN chmod +x /usr/local/bin/goose
WORKDIR /migrations
COPY migrations /migrations
# Overridden by the job. Default is intentionally not "up" so a bare docker run does nothing destructive.
ENTRYPOINT ["goose"]
CMD ["-dir", "/migrations", "status"]
Enter fullscreen mode Exit fullscreen mode

Check the asset name for your arch on the goose release page before you commit the Dockerfile. The ADD URL above is the pattern, not a promise that every version publishes goose_linux_x86_64.

Flisk side

Create the API with a real provider, and the migrator as bring-your-own / one-shot. The exact one-shot flags are in Flisk's job docs (docs on byo and one-shot jobs). Read them for your CLI version. The intent:

flisk resource billing --type backend --language go --stack shop --yes
flisk resource billing-migrate --type bring-your-own --stack shop --yes
Enter fullscreen mode Exit fullscreen mode

The migrator service.json should declare that it depends on Postgres, restart policy never, and a command of up rather than a long-running server. Something in this family, then verify with flisk doctor because BYO fields drift:

{
  "appName": "billing-migrate",
  "appType": "bring-your-own",
  "stack": "shop",
  "dependsOn": ["postgres"],
  "restart": "no"
}
Enter fullscreen mode Exit fullscreen mode

Pass the DSN with Flisk's env / secrets mechanism, not a URL committed beside the SQL. Goose wants a driver and a directory:

goose -dir /migrations postgres "$DATABASE_URL" up
Enter fullscreen mode Exit fullscreen mode

status in CI, up in the job, down only when a human asked for it. A recipe or agent that runs down because a health check failed is how you drop invoices on a Friday.

Order of operations

  1. flisk up shop starts Postgres and the API.
  2. The one-shot job runs goose up and exits 0.
  3. You hit the health URL Flisk prints for billing.
  4. A later migration is a new SQL file, a rebuilt job image, and another one-shot run. It is not an edit under .autogenerated/.

If the API starts before up finishes, requests fail on missing tables. That is better than the API reporting healthy on an old schema. Gate the API on the job if your Flisk version's dependsOn supports a one-shot, or accept a short retry in the client. Do not paper over it by retrying goose up inside the API process.

What goose is good at here, and what it is not

Goose is a solid migrator: version table, up/down, SQL or Go migrations. Flisk does not need to know that. It needs a container that exits. The same slot fits dbmate or Atlas. Pick goose if your team already has goose files. Do not pick it because the binary is named goose and you hoped --language goose would appear.

Go migrations (goose's .go files) need a build step. Prefer SQL in the one-shot image unless you already compile a custom goose binary in CI. A job that runs go run on every boot is how a one-shot stops being one shot.

Checks before you trust it

# from a throwaway database, not your laptop's only Postgres
goose -dir billing-migrate/migrations postgres "$DATABASE_URL" up
goose -dir billing-migrate/migrations postgres "$DATABASE_URL" status
# down only against that throwaway
goose -dir billing-migrate/migrations postgres "$DATABASE_URL" down
Enter fullscreen mode Exit fullscreen mode

Then the Flisk path: flisk doctor, flisk up shop, confirm the job exited 0 in the Tilt log, then curl the API health URL. A green health check does not mean the migration ran. Read the job log.

The mix-up, one last time

Name Command you wanted Flisk feature
Goose language compile a .goose program none, use bring-your-own
Goose agent goose run --recipe ... you drive the flisk CLI, you do not get a language id
pressly/goose goose up one-shot job beside Postgres
Go flisk resource --language go real provider, not goose

Top comments (0)