ldd reports library paths as /lib64/... (the legacy short path).
quarkus-micro-image:2.0 has /lib64 as a symlink to /usr/lib64, so
COPY --from=curl-builder /out/ / collides when trying to write into
/lib64 (cannot copy to non-directory).
The fix: in the curl-builder stage, translate /lib64 -> /usr/lib64
(and /lib -> /usr/lib) before installing each library, so the final
image gets libs under /usr/lib64 and doesn't touch the /lib64 symlink.
Earlier cleanup added build-output to .dockerignore, but the runtime
stage does COPY build-output/*-runner /work/application, so the
directory must be present in the build context.
Drop the LXC deploy step. Pipeline now stops at publishing the image
to the Gitea registry; deployment is handled out of band.
Restored:
- Dockerfile (multi-stage: curl-builder + quarkus-micro-image:2.0,
generic via build-output/*-runner wildcard, COPY --chown=1001:1001)
- compose.yaml (one-shot install of the published image)
- .dockerignore (excludes build-output/)
CI workflow:
- Installs docker-buildx (needed for COPY --chown)
- Uses docker buildx build
- chmod 775 and echo securerandom happen in the 'Stage binary for Docker'
step; the final image has no RUN commands
- Tags :latest and :<short-sha>, pushes with retry
No deploy step. Pull the image with docker compose / run it manually.
Switch the runtime from a Docker image to a systemd service running the
native binary on the LXC host. The CI still uses Docker for the build
environment (maven:3.9.6-eclipse-temurin-21), but stops at producing the
static native binary.
Pipeline changes:
- Drop docker.io, docker-buildx, docker buildx, docker push, registry.
- Drop Dockerfile, compose.yaml, .dockerignore (no longer needed).
- Build native binary in CI container, SCP to LXC, run deploy script.
- Deploy script stops the service, swaps the binary, starts it, hits
/q/health/live to verify.
LXC one-time setup (manual, run on the host):
- useradd runner (UID 1001)
- mkdir /opt/shot-crafter-calculator/{data,keys,deploy}
- copy RSA JWT keys into keys/
- install /etc/systemd/system/shot-crafter-calculator.service
- install /usr/local/bin/deploy-shot-crafter-calculator.sh
- useradd deployer + ssh keypair for the CI
- store DEPLOY_SSH_KEY secret in Gitea
Bootstrap the first deploy manually with scp + ssh before relying on CI.
Removed the RUN chmod/chown/echo steps from the Dockerfile. The Quarkus
micro image 2.0 ships without /bin/sh and /usr/bin/sh, so any RUN
instruction fails. The chmod and securerandom.source append now happen
in the CI 'Stage binary for Docker' step, and COPY --chown=1001:1001
takes ownership of the binary in the image.
COPY --chown requires BuildKit, so:
- Install docker-buildx in the CI
- Switch docker build -> docker buildx build
The final image stays minimal (no shell, no microdnf, no extra packages).
The Quarkus micro image 2.0 no longer ships /bin/sh (only /usr/bin/sh).
Docker's default shell is /bin/sh, so RUN commands fail with
'exec /bin/sh: no such file or directory'. Set SHELL explicitly to
/usr/bin/sh to keep the small final image while letting Docker run
RUN commands.
The 2.0 rebuild of quarkus-micro-image removed microdnf to slim the image,
so the inline 'microdnf install curl-minimal' step now fails with
'command not found'.
Build curl-minimal in a ubi9/ubi-minimal builder stage, then copy only the
curl binary + its runtime shared libs + CA bundle into the final
quarkus-micro-image layer. Final image stays slim and the docker-compose
healthcheck keeps working.
Container `maven:3.9.6-eclipse-temurin-21` is Ubuntu 22.04 jammy
with glibc 2.35. Container `quarkus-micro-image:2.0` is UBI 9
minimal with glibc 2.34. Native-image links against the build
host's glibc, so the resulting binary needs GLIBC_2.34+ symbols
that the UBI 9 runtime doesn't provide:
./application: /lib64/libc.so.6: version `GLIBC_2.34' not found
./application: /lib64/libc.so.6: version `GLIBC_2.33' not found
./application: /lib64/libc.so.6: version `GLIBC_2.32' not found
Set quarkus.native.binary-type=STATIC so GraalVM embeds musl into
the binary and removes the glibc dependency. The static binary
runs on any Linux distribution, including UBI minimal. Trade-off:
~50MB -> ~80MB image size.
- pom.xml: add quarkus-smallrye-health dependency.
Exposes /q/health/live and /q/health/ready endpoints (liveness
and readiness probes for Quarkus apps).
- Dockerfile: install curl-minimal in the microdnf layer so the
container has a real HTTP client. quarkus-micro-image is based
on UBI 9 minimal and doesn't ship with curl by default.
- compose.yaml: healthcheck now hits GET /q/health/live with
curl -f instead of the previous kill -0 1 (which only proved
the process was alive, not that the HTTP server was responding).
The next CI run will rebuild the native binary with the health
extension baked in; old images pulled from :latest will keep
working since this is additive.
docker compose pull
docker compose up -d
docker compose logs -f
- Single service `shot-crafter` pulling from
gitea.danielarroyo.cl/proyectos/shot-crafter-calculator:latest
- Named volume `shot-crafter-data` mounted at /work/data
so the H2 DB persists across `down`/`up` cycles
- env vars inline (overridable via -e or .env): DB URL,
HTTP port/host, cookie name + secure flag
- healthcheck using `kill -0 1` (the native binary is PID 1,
POSIX-portable, no extra binaries needed)
- restart: unless-stopped so it survives Docker daemon restarts
Ver .env.example para la lista completa de variables operacionales.
- Add .env.example with all configurable variables documented
(DB URL, HTTP port/host, cookie, JWT issuer/keys, log level)
- Remove hardcoded -Dquarkus.http.host from Dockerfile ENTRYPOINT
(application.properties already sets the default; env vars
can now override it at runtime without conflicting with -D flags)
All env vars follow Quarkus's auto-binding convention:
property.key → PROPERTY_KEY (uppercase)
Most useful for production:
- QUARKUS_DATASOURCE_JDBC_URL: DB file path
- QUARKUS_HTTP_PORT: HTTP port
- QUARKUS_HTTP_HOST: bind interface
- APP_AUTH_COOKIE_SECURE: enable Secure flag behind HTTPS
- MP_JWT_VERIFY_PUBLICKEY_LOCATION: externalize RSA keys
The Gitea Container Registry responded with 'net/http: timeout
awaiting response headers' midway through the push. The image
uploads ~50MB of layers, and the registry can be slow under load.
Add a push_with_retry() shell function that retries each push up
to 3 times with 15s backoff. Most importantly, it retries for
both the 'latest' and the SHA tag, so a partial failure doesn't
leave the registry in a half-pushed state.
Uses POSIX sh-compatible while loop (no bashisms like {1..3}).
The act runner uses /bin/sh (dash on Debian/Ubuntu), not bash.
The syntax ${GITHUB_SHA::7} is bash-specific substring expansion
and throws 'Bad substitution' under dash.
Replace with POSIX-portable equivalent:
SHORT_SHA=$(echo "$GITHUB_SHA" | cut -c1-7)
Works in any sh-compatible shell (dash, bash, zsh, etc.).
The .dockerignore has 'target' which excludes the target/
directory from the docker build context. The Dockerfile was
COPYing the binary from target/, so docker build failed with
'file not found in build context or excluded by .dockerignore'.
Fix: copy the binary to build-output/ (a non-excluded path)
before docker build, and update the Dockerfile to copy from
build-output/.
- Add 'Stage binary for Docker' step that does:
mkdir -p build-output
cp target/shot-crafter-calculator-1.0.0-runner build-output/
- Dockerfile COPY now reads build-output/shot-crafter-calculator-1.0.0-runner
- build-output/ is not in .dockerignore -> only the binary
(~117MB) ships in the build context, not the whole target/
tree (~200MB with classes, generated-sources, node binaries, etc.)
act (the runner the user is running) auto-mounts the docker
socket via volume `GITEA-ACTIONS-TASK-...-env Target:/var/run/act`.
Declaring volumes: in the workflow causes a duplicate mount
(once via the volumes block, once via act's auto-mount):
Binds:[/var/run/docker.sock:/var/run/docker.sock
/var/run/docker.sock:/var/run/docker.sock]
failed to create container:
'Error response from daemon: Duplicate mount point'
Drop the explicit volumes block. The socket is already there.
The docker CLI inside the container can talk to the host daemon
via the already-mounted socket.
The container doesn't have docker, so docker build/docker push
fails with 'docker: not found'. Fix:
- Add docker.io to the apt-get install step (inside the container)
- Mount the host's docker.sock into the container via volumes:
so the in-container docker CLI talks to the host's docker daemon
(same model as Docker-in-Docker but using the host socket).
The runner has the docker label, so the host socket is available.
Now the whole pipeline (build native binary, build container image,
push to registry) runs in a single job without any artifact
upload/download between jobs.
The runner (runner-lxc-quarkus, using act) fails to upload the
117MB native binary via actions/upload-artifact@v4 with exit code 1
after 3 minutes. Avoid the artifact transfer entirely by collapsing
build-native and docker-native into a single job: build native
binary, then build the Docker image directly from the working tree
in the same container.
The container has the binary at target/shot-crafter-calculator-1.0.0-runner
and the Dockerfile expects it at the same path, so the build
chain works without any artifact download/upload.
Push only happens on push to main (not on PRs).
The upload-artifact step failed with exit code 1 but no details.
The most likely cause is that the binary file does not exist
at the expected path `target/shot-crafter-calculator-1.0.0-runner`
because the native build may have failed silently.
Add a 'Verify native binary' step that runs `ls -la target/` and
`file target/shot-crafter-calculator-1.0.0-runner` so the next
log shows exactly what's in target/ and what type the runtime
sees the binary as. Also make the upload's 'if-no-files-found'
explicit so the failure mode is unambiguous.
Native-image was OOM-killed (exit 137) during inlining phase. The
runner has no implicit memory limit on container jobs, so native-image
probed all available memory and got killed by the host OOM killer.
Fix:
- Set container options: --memory=8g (hard cap for the build container)
- Pass -Dquarkus.native.native-image-xmx=4g to mvn (limits the
native-image JVM heap to 4g, leaves 4g headroom for the rest
of the toolchain: GraalVM, glibc, Maven, etc.)
GraalVM native-image requires a C compiler (gcc) and zlib dev
headers to compile native binaries. The maven:3.9.6-eclipse-temurin-21
image is JDK-only and lacks these build tools, causing the build
to fail with:
Error: Default native-compiler executable 'gcc' not found via PATH
Extend the install step with build-essential (gcc, g++, make)
and zlib1g-dev so the native-image pipeline has the required
toolchain.
The .gitignore had 'data/' which matches any 'data' directory at
any depth. That excluded src/frontend/src/data/ from git, so the
file defaults.ts (with DEFAULT_INSUMOS, DEFAULT_FORMULAS, etc.)
was never committed. CI was failing with TS2307 because the file
was in the local working tree but not in the git checkout.
Tighten the rule to '/data/' so only the root runtime H2 database
folder is ignored, not the source data directory.
Gitea Actions run actions as JavaScript via `node` inside the
job container. The maven:3.9.6-eclipse-temurin-21 image has no
node, so action post steps (e.g. Checkout cleanup) fail with
'exec: "node": executable file not found in $PATH'.
Extend the install step to add git AND node 22 via NodeSource
apt repo, so the runner can complete every action's pre/post step.
The runner lacks git even though it has Docker access. The
maven:3.9.6-eclipse-temurin-21 image is Ubuntu-based without
git installed, which broke actions/checkout.
- Remove build-jvm entirely (it was failing too, and the JMV
artifact is not needed for the native pipeline)
- Add 'Install git and basic tools' step inside build-native
(apt-get update + apt-get install -y git ca-certificates)
- docker-native job unchanged: still depends on build-native,
builds the image with the pre-built native binary
Runner has no Maven or JDK installed. Instead of installing
both manually each job, run the jobs inside the official
maven:3.9.6-eclipse-temurin-21 image which has both pre-installed.
- Drop actions/setup-java (image has JDK 21)
- Drop 'Install Maven' step (image has Maven 3.9.6)
- Add 'container: image: maven:3.9.6-eclipse-temurin-21' to each job
- Add cache for ~/.m2/repository (key on pom.xml hash)
- Keep graalvm/setup-graalvm only for the native job (adds GraalVM
inside the container)
- docker-native job unchanged: uses the runner's docker daemon
directly to build the image with the pre-built native binary
The runner has Java (via setup-java) but no Maven. Add an
Install Maven step to build-jvm and build-native that downloads
Maven 3.9.6 from Apache to runner.workspace/.maven and adds it
to the PATH via $GITHUB_PATH. Idempotent: the if-guard skips
redownload if the binary already exists on the runner.
The Quarkus image (docker.io/quarkusio/quarkus-images:tooling-21)
cannot be pulled by the runner (access denied). Replace the
container: block with the standard actions/setup-java (Temurin 21)
and graalvm/setup-graalvm (with native-image component) actions
so the jobs run on the host directly.
Adds:
- Dockerfile based on quarkus-micro-image:2.0 (~50MB base)
Runs the native binary as non-root user 1001, exposes 8080
- .dockerignore to exclude build artifacts
- Gitea Actions workflow with 3 parallel jobs:
- build-jvm: standard JAR (mvn package)
- build-native: GraalVM native binary (mvn package -Pnative)
- docker-native: takes the native binary artifact, builds
the container image and pushes to
gitea.danielarroyo.cl/proyectos/shot-crafter-calculator
with tags 'latest' and short SHA
Triggers: push to main and PRs (docker job only on push).
Required secrets: GITEA_USERNAME, GITEA_TOKEN (write:packages).
All files committed in 9c9fb3a. Branch is up to date with origin/main.
- 57 files committed
- 6260 insertions
- Working tree clean
- 35/35 browser tests passing