Part 4 of Goose, then any language on Flisk. docs/byo.md says any app that fits the contract can run as a bring-your-own resource. Fastify is the worked example because it needs nothing beyond the framework. Node, Go, Rust, Java, Ruby, and Goose use the same command. Flisk does not generate the application.
The contract, for whatever language you brought
- Listen on the port in
service.json. Compose setsPORT. It also setsDATABASE_URLin every BYO container. ReadPORT. IgnoreDATABASE_URLunless you asked for Postgres. - Bind
0.0.0.0. - Answer HTTP 200 on
healthCheckPathif it speaks HTTP.--health-path /health. This type does not get a Composecurlprobe. - Provide a Dockerfile or
--image.COPYpaths are relative to the resource directory.
Ports are 4000–5999. If a Dockerfile already exists, Flisk keeps it. If you register with no Dockerfile, Flisk writes an Nginx placeholder. Write yours first.
Goose's standard library can read PORT with os.env. Sockets are not in v1. The tutorial's section 16 is how you call C: extern fn, then cc the emitted C with your C file. That C file is application code, the same way Fastify's listen is application code.
Files
services/shop/goose-api/
Dockerfile
server.goose
http.c
goose-compiler/goose # Linux compiler, same arch as the image
server.goose keeps total from part 3 and stays running:
fn total(xs: i64[:]) -> i64 {
var t = 0;
for x in xs { t += x; }
return t;
}
fn main() {
var grow: i64[>..] = [];
for i in 5 { grow.push(i * i); }
print(grow);
print(total(grow));
var text: u8[>..] = [];
let found = os.env("PORT", text);
let port, parsed = parse_int(text);
if !found || !parsed || port <= 0 || port > 65535 {
abort("PORT missing or not a port");
}
let code = serve(port as i32);
if code != 0 { abort("serve failed"); }
}
extern fn serve(port: i32) -> i32;
print still runs. You will see the array in the container log before the server accepts a connection. serve must not return. The default restart policy is unless-stopped, so a process that exits 0 is started again forever. --restart no is for a one-shot job, not this API.
http.c binds every interface and answers GET /health. It ignores Host. Through flisk up that header is api.<project>.localhost.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int serve(int port) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return 1;
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((unsigned short)port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) return 2;
if (listen(fd, 16) != 0) return 3;
for (;;) {
int client = accept(fd, 0, 0);
if (client < 0) continue;
char req[1024];
int n = (int)read(client, req, sizeof(req) - 1);
if (n < 0) n = 0;
req[n] = 0;
const char *body = "{\"status\":\"ok\"}\n";
char resp;
if (strncmp(req, "GET /health", 11) == 0) {
snprintf(resp, sizeof(resp),
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %zu\r\nConnection: close\r\n\r\n%s",
strlen(body), body);
} else {
const char *missing = "not found\n";
snprintf(resp, sizeof(resp),
"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\nConnection: close\r\n\r\n%s",
strlen(missing), missing);
}
(void)write(client, resp, strlen(resp));
close(client);
}
}
The compiler inside the image must be a Linux binary for that image's architecture. Do not COPY the macOS goose from part 1.
FROM debian:bookworm-slim AS build
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc libc6-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY goose-compiler/ /opt/goose/
ENV PATH="/opt/goose:${PATH}"
WORKDIR /src
COPY server.goose http.c ./
RUN goose -o server.c server.goose \
&& cc server.c http.c -o /out/server -lm -pthread
FROM debian:bookworm-slim
COPY --from=build /out/server /usr/local/bin/server
CMD ["server"]
Same registration as Fastify
cd services/shop/goose-api
docker build -t goose-health .
docker run --rm -d --name goose-health -e PORT=4000 -p 4000:4000 goose-health
curl -fsS http://127.0.0.1:4000/health
docker logs goose-health
docker stop goose-health
The log should contain the printed array from main. The curl should print {"status":"ok"}. That is the check under examples/byo/: build, PORT=4000, HTTP 200.
From the Flisk project root:
flisk resource goose-api --type bring-your-own --stack shop \
--dockerfile ./Dockerfile --health-path /health \
--path services/shop/goose-api --port 4500 --yes
flisk up shop --dry-run
flisk up shop
--type byo is the same type. --port must be unused and inside 4000–5999. The route is http://api.<project>.localhost/api/goose/health. The prefix is stripped, so the process sees /health. <project> is the Flisk project name, not the stack. Use the URL --dry-run prints.
There is no live reload. Change server.goose, rebuild the image. Do not edit .autogenerated/.
A 200 means Flisk routed whatever language you put in the image. Swap the Dockerfile for Fastify, Gin, or Java and the flisk resource line stays the same.
Top comments (0)