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
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;
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"]
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
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"
}
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
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
-
flisk up shopstarts Postgres and the API. - The one-shot job runs
goose upand exits 0. - You hit the health URL Flisk prints for
billing. - 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
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 |







