- Remove Node.js installation (frontend is now static Qute/Alpine)
- Remove src/frontend/node_modules cache (no longer exists)
- Change github.workflow to gitea.workflow for Gitea Actions compatibility
- Increase container memory from 8g to 10g for native builds
- Add BUILDKIT_INLINE_CACHE=1 for faster Docker builds
Sin estos parametros, H2 mantiene buffers en memoria y los cambios
hechos por change-password (u otras mutaciones) no llegaban al
archivo .mv.db hasta un checkpoint que el SIGKILL del container
se saltaba. Resultado: User aparece persistido en memoria (login
con la nueva pass da 200 OK dentro de la misma corrida), pero al
restart el archivo en disco tiene la password vieja.
WRITE_DELAY=0 -- flush cada commit
LOCK_MODE=0 -- no usar lock file (quitamos el .lock.db)
MV_STORE=TRUE -- usar el multi-version store que es mas cooperativo
con escrituras frecuentes
Verificado: el archivo .mv.db crece de 94KB a 106KB tras un
change-password, y la nueva password sobrevive un docker rm + run.
User.getEntityManager().refresh() requiere una tx activa, y el
caller (AuthResource.changePassword) no es @Transactional -> ese
approach tiraba TransactionRequiredException.
En lugar de refrescar en el caller, cambio AuthService.changePassword
para devolver la User ya actualizada en un record
ChangePasswordOutcome, despues de un flush() explicito para que los
cambios lleguen a DB antes de cerrar la tx. Asi el response del
endpoint refleja el estado real de la fila.
El cambio-password persistia bien a la DB (login con la nueva pass
daba 200 OK), pero la respuesta del endpoint seguia reportando
mustChangePassword: true. Era un problema de Panache/Hibernate cache:
la user entity quedaba cacheada en el persistence context con el
estado pre-cambio, y User.findById() siguiente devolvia esa misma
instancia cacheada.
Fix: despues de findById, forzar refresh desde DB con
User.getEntityManager().refresh(refreshed). Asi el response
refleja el estado actual real de la fila.
'create-drop' borra las tablas en cada shutdown, perdiendo todos
los usuarios (y el cambio de password). Aunque el bind mount
persiste el archivo H2 en /root/docker/manual/lineage2/data/, las
tablas se recreaban vacias en cada restart del container.
Solucion: Flyway para manejar el schema, Hibernate solo valida.
- pom.xml: + quarkus-flyway
- src/main/resources/db/migration/V1__init.sql: schema de users,
user_state, production_runs + index idx_runs_user_created
- application.properties: database.generation=create-drop -> validate
+ quarkus.flyway.migrate-at-start=true + baseline-on-migrate=true
BootstrapAdmin queda igual: chequea User.count() y crea admin si 0.
Flyway corre antes de Hibernate, asi que las tablas existen cuando
StartupEvent dispara.
En el fix anterior, await api.me() estaba DESPUES de setUser(u). Eso
significaba que React podria re-renderizar AuthenticatedApp entre el
setUser y el await, disparando el usePersistedState.load() que llama
api.getState() mientras la cookie todavia no estaba aplicada al
cookie store del browser. El await api.me() quedaba corriendo mientras
el 401 ya habia sido reportado.
Invertir el orden: await api.me() PRIMERO (confirma que la cookie
quedo realmente aplicada), despues setUser(u). Asi React re-renderiza
con la cookie ya activa para los fetches internos.
Dos cambios para destrabar el flujo post-change-password:
1. AuthContext.changePassword: despues de setUser(u), hacer un round-trip
a api.me() para confirmar que la nueva cookie ya es utilizable. Sin
este paso, React monta AuthenticatedApp tan rapido que el primer
fetch desde usePersistedState.load() corre antes de que el browser
haya propagado el Set-Cookie (race condition reportado en Chrome).
2. usePersistedState.load: en 401, caer en makeDefaultAppState() en lugar
de setStateInternal(null). Antes dejaba al usuario varado en un
<LoadingScreen>Cargando tu estado...</LoadingScreen> para siempre. Ahora
ve la app vacia con SaveIndicator, y al primer cambio real el
auto-save sube el estado al server.
Found the actual issue was a stale Docker image in the LXC (the CI
runner was stuck for ~30h starting at task 323 on Aug 14). The newly
pulled image with all prior fixes (no @Authenticated, records w/
@RegisterForReflection, create-drop for tables) has been verified
end-to-end: login returns 200, /me returns 200 with the user,
change-password returns 200 after password validation.
So we can drop the LOG statements and the SQL debug log without
regressing anything.
Para diagnosticar por qué el login devuelve 401 con la password
correcta del bootstrap admin. Imprime:
- username/pw.length al entrar
- si la busqueda del user en la DB da null
- hash.len y hash.prefix para confirmar que el user esta bien
almacenado
- bcrypt.matches(true/false)
Tambien activamos quarkus.hibernate-orm.log.sql=true y
quarkus.log.category.'com.l2.shots.auth'.level=DEBUG.
Para diagnosticar por qué el login devuelve 401 con la password
correcta del bootstrap admin. Vamos a ver qué queries corre
Hibernate y qué pasa en el authenticate.
Quarkus security solo lee JWT del header 'Authorization: Bearer ...'
por defecto. Esta app entrega el JWT en una cookie HttpOnly
('auth-token'), entonces @Authenticated/@RolesAllowed rebotaban con
401 antes de que el endpoint pudiera validar manualmente.
Reemplazos:
- @Authenticated en /change-password: sacada. El endpoint ya hace
jwtCookieAuth.extractToken() y devuelve 401 manual si falla.
- @Authenticated en /check: sacada. Endpoint ahora extrae y valida
el JWT a mano.
- @Authenticated en /me: never estuvo, OK.
- @RolesAllowed('admin') en /admin/reset-password y /admin/users:
reemplazada por chequeo manual jwt.getGroups().contains('admin'),
devolviendo 403 si no es admin.
Sigue funcionando el MustChangePasswordFilter porque sigue siendo
un ContainerRequestFilter con @Priority(AUTHENTICATION+100) y no
requiere auth previa para correr: simplemente aborta con 403 si
mustChangePassword=true salvo allowlist (change-password, logout).
Removida inyeccion no usada de currentJwt (JsonWebToken) que solo
estaba para @RolesAllowed.
Las clases record NO se serializan bien en Quarkus 3.20.1 native-image
aunque Jackson 2.17+ las soporta en JVM. GraalVM strip-ea la metadata
<Erecord> del bytecode mas los accessors auto-generados, y Jackson no
puede detectar que la clase es un record -> cae al BeanSerializer
clasico -> 'no properties discovered to create BeanSerializer'.
@RegisterForReflection le indica a Quarkus: incluí esta clase en la
metadata de reflexion del native-image, asi Jackson la ve completa.
Aplicado a los 16 DTOs (records + nested records):
- auth: AuthMeResponse, AdminUserSummary, Credentials,
ChangePasswordRequest, AdminResetPasswordRequest, ErrorBody,
MustChangeBody
- history: RunSummary, RunDetails, RunItem, RunSnapshot, RunIn,
HistoryStats
- state: AppState + Insumos + FormulaDto (nested)
En Quarkus native-image, Jackson no introspecta public fields de clases
con normales (deja de refleccionar field metadata). Salia error:
No serializer found for class com.l2.shots.auth.AuthMeResponse and
no properties discovered to create BeanSerializer
Los records tienen auto-accessors (id(), username(), etc) que Jackson
serializa nativamente, sin necesidad de reflection o
@RegisterForReflection.
Convertidos:
- auth: AuthMeResponse, AdminUserSummary, Credentials,
ChangePasswordRequest, AdminResetPasswordRequest, ErrorBody,
MustChangeBody
- history: RunSummary, RunDetails, RunItem, RunSnapshot, RunIn,
HistoryStats
- state: AppState (con Insumos y FormulaDto anidados)
Callers actualizados para usar accessors en vez de field access:
- AuthService.listUsersForAdmin -> AdminUserSummary.from
- AuthService.saveRun -> RunSummary.from / RunDetails.of
- AuthResource.register/login -> creds.username() / creds.password()
- HistoryResource.saveRun -> input.items() etc
Notas:
- RunDetails NO puede extender RunSummary en records (JLS no permite
extends entre records). Va como record independiente con todos los
campos. El JSON que produce matchea la interface RunDetails del
frontend (que extendia RunSummary).
- HistoryStats ahora se construye de una sola vez al final de
computeStats en lugar de ir mutando campos.
- MustChangeBody quedo con un constructor no-canonico no-arg para
mantener el call site original (new MustChangeBody()).
El schema-gen con database.generation=update NO corre en Quarkus native
(3.20.1) ni forzando quarkus.hibernate-orm.schema-management.run-on-startup=true.
El primer 'Table USERS not found' sigue saliendo en BootstrapAdmin.onStart.
Cambio a 'create-drop':
- En startup: crea las tablas que falten (no dropa las existentes).
- En shutdown limpio: drop -> POR CADA docker stop SE PIERDEN LOS USERS.
Para persistencia real a futuro: integrar Flyway con migrations y pasar
database.generation=validate. Para esta entrega, dejamos create-drop
que al menos arranca la app + bootstrap admin en un H2 fresco, y los
restarts sin SIGTERM (docker kill, caida de energia) preservan datos.
Saca tambien el run-on-startup=true que no estaba surtiendo efecto.
El error 'Table USERS not found' en BootstrapAdmin.User.count() se daba
porque en Quarkus native-image, con database.generation=update, el
schema-gen NO corre en startup del container por defecto (corre solo en
dev mode). Resultado: en un H2 fresco, el primer SELECT COUNT(*) del
bootstrap pierde contra una DB sin tablas.
Dos cambios:
1. application.properties: agregar
quarkus.hibernate-orm.schema-management.run-on-startup=true
para forzar el schema-gen en startup en prod/native.
2. BootstrapAdmin:
- @Transactional + EntityManager (em) inyectados
- em.createNativeQuery('SELECT 1') al inicio de onStart() para
asegurar que el EMF esté listo y la session esté abierta antes
del count(). Belt-and-suspenders.
El chown sigue sin funcionar despues del primer fix. Agregamos logs
para ver:
- con que uid arranca el entrypoint
- ls -la /work/data antes y despues del chown
- si chown falla, lo logueamos en lugar de tragarnos el error
Tambien asegura que /work y /work/application tengan owner 1001:1001
y mode 0755, por si la imagen viene con owner root (que seria el
sintoma de que el build no usaria USER directive).
Cuando el volumen named 'shot-crafter-data' es nuevo, Docker lo monta
con owner root:root. La aplicacion corre como UID 1001 y no puede
escribir -> H2 falla al crear shots.mv.db -> BootstrapAdmin no crea
el admin -> no se puede login.
Quitar 'USER 1001' del Dockerfile, agregar entrypoint.sh que:
1. chown -R 1001:1001 /work/data (la app va a poder escribir)
2. chmod 0755 /work
3. setpriv --reuid=1001 --regid=1001 --init-groups -- /work/application
(drop a no-root antes del exec)
Resultado: el contenedor arranca como root el primer instante, hace
el fix de permisos una sola vez, y desde ese momento la app corre
como 1001. Docker defaults USER a root cuando no hay directiva USER.
GraalVM native-image rejects static fields of type Random/SecureRandom
(their internal state is captured at build time and can't be replayed
deterministically). Move the instance into generatePassword() as a
local variable - it is only used once at startup anyway.
Backend additions:
- User.lastLoginAt column (updated on each successful login)
- AdminUserSummary DTO (id, username, createdAt, lastLoginAt, isAdmin, mustChangePassword)
- GET /api/auth/admin/users (RolesAllowed("admin")) -> array of summaries
- AuthService.listUsersForAdmin() + AuthService.authenticate() now @Transactional and bumps lastLoginAt
Frontend (Phase 1):
- User type extended with mustChangePassword + isAdmin
- api.changePassword() / api.adminResetPassword() / api.adminListUsers()
- AuthContext exposes changePassword
- ChangePasswordPage.tsx (full-page, current + new + confirm, errors inline)
- App.tsx routes LoginPage -> ChangePasswordPage -> AuthenticatedApp
Frontend (Phase 2):
- TabBar supports optional 'usuarios' tab (shown only if user.isAdmin)
- UsersAdminPage.tsx: lista todos los usuarios con badges de rol y estado,
botón "Resetear contraseña" con modal inline que llama adminResetPassword
- Header de AuthenticatedApp muestra un badge 'admin' al lado del username
Both mvn compile and npm tsc + vite build pass clean.
Backend changes (no frontend yet):
Schema (User entity)
- + mustChange_password (boolean NOT NULL, default false)
- + is_admin (boolean NOT NULL, default false)
Hibernate update mode adds both columns automatically.
BootstrapAdmin (new, ApplicationScoped, @Observes StartupEvent)
- runs only when User.count() == 0 and app.bootstrap.admin.enabled=true
- generates a 20-char random password (alphabet without 0/o/O/1/l/I)
- persists the user with isAdmin=true, mustChangePassword=true
- prints a banner to stdout AND to the JBoss logger so docker logs
picks it up:
BOOTSTRAP-ADMIN-USERNAME admin
BOOTSTRAP-ADMIN-PASSWORD <random>
BOOTSTRAP-ADMIN-CHANGE This password MUST be changed on first login ...
- idempotent: skips if any user already exists
MustChangePasswordFilter (new, @Provider ContainerRequestFilter)
- runs after JWT auth (Priorities.AUTHENTICATION + 100)
- for authenticated requests with mustChangePassword=true, returns
403 with {error, mustChangePassword:true} unless the path is
/api/auth/change-password or /api/auth/logout
Change-password endpoint (POST /api/auth/change-password)
- @Authenticated, body {currentPassword, newPassword}
- verifies currentPassword via bcrypt, validates newPassword>=8 chars,
updates hash and sets mustChangePassword=false
- returns updated AuthMeResponse and re-issues the auth cookie
Admin reset endpoint (POST /api/auth/admin/reset-password)
- @RolesAllowed("admin")
- body {username, newPassword}
- sets target's passwordHash and mustChangePassword=true (forces change
on next login)
- security: only users in the JWT 'admin' group can hit it; isAdmin
is stored on the user record so a stale token can't promote itself
JWT groups now include 'admin' for isAdmin users; previously everyone
was just 'user'.
Config (application.properties)
- app.bootstrap.admin.enabled=true
- app.bootstrap.admin.username=admin
Cut the dependency on the public hostname gitea.danielarroyo.cl and
push straight to the LAN Gitea instance. Internal Gitea is plain HTTP,
so the docker daemon on the LXC needs 'insecure-registries' set, and
the gitea-runner-lxc has to be re-registered against the new URL
(separate manual steps; not in this repo).
- ci.yml: docker login, both docker tag, both push_with_retry, and
docker logout now target 10.5.0.195:3000
- compose.yaml: image URL switched to 10.5.0.195:3000
The CI workflow itself, the Dockerfile, and the build command are
unchanged - only the registry target moved.
The native binary is built against glibc 2.35 (Ubuntu jammy in CI) but
quarkus-micro-image:2.0 ships glibc 2.34 (UBI 9), hence the
'GLIBC_2.35 not found' at runtime. Switching to ubuntu:22.04 as the
runtime base matches glibc exactly and lets us stop fighting with
musl / static binaries.
Dockerfile:
- FROM ubuntu:22.04 (was quarkus-micro-image:2.0)
- install curl + ca-certificates via apt (apt + bash are present, so
chmod/echo securerandom run in-line again)
- single-stage: no more curl-builder multi-stage
- useradd UID 1001 (matches the in-container USER)
CI:
- drop binary-type=STATIC and --libc=musl (binary is dynamic again)
- drop the rm -f target/*-runner (cache invalidate)
- 'Stage binary for Docker' is now just 'cp'
- verify step is informational only
Image is ~5 MB larger than quarkus-micro-image but the runtime now
matches the build glibc, so the container starts cleanly.
binary-type=STATIC alone picks glibc on Ubuntu jammy hosts, hence
the GLIBC_2.35 error at runtime. Explicitly force musl:
- -Dquarkus.native.libc=musl
- -Dquarkus.native.additional-build-args=--libc=musl
- delete target/*-runner first so we don't reuse a previously-built
glibc-based binary that the Quarkus plugin cache might return
Verify step now exits 1 if the binary isn't statically linked, so
the next CI run tells us definitively whether we shipped static.
The 'file' package isn't installed in maven:3.9.6-eclipse-temurin-21, so
'file target/...-runner' returned 127 and failed the verify step.
Use only 'ldd'; on a static binary ldd exits non-zero with 'not a
dynamic executable', which we suppress with '|| echo'.
container-build=true runs the native-image inside a Mandrel container,
but in the act runner the workspace mount doesn't resolve
(/workspace/... is a container path, not a host path), so the Mandrel
container can't see shot-crafter-calculator-1.0.0-runner.jar.
Fix: install musl + musl-tools in the CI container, build locally.
Quarkus 3.x's binary-type=STATIC will then produce a genuinely static
binary linked against musl, which has no dependency on the host's
glibc and runs cleanly on quarkus-micro-image:2.0 (UBI 9, glibc 2.34).
The local native-image (GraalVM on Ubuntu jammy) produces a binary
linked against the runner's glibc 2.35. quarkus-micro-image:2.0 ships
glibc 2.34, hence the 'GLIBC_2.35 not found' runtime error.
binary-type=STATIC alone is not enough without musl on the build host.
-Dquarkus.native.container-build=true runs the native build inside the
Mandrel builder image, which ships musl and produces a genuinely static
binary that has no host-libc dependency.
Also adds a verification step that runs file and ldd on the runner
binary so the next CI log shows whether we actually shipped static.
That flag only exists for docker buildx / docker buildx imagetools.
docker push in this version rejects it as unknown. Revert to plain
docker push with the retry loop.
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.