--- - name: Pull the app image docker_image: name: '{{ image_name }}' tag: '{{ image_tag }}' source: pull force_source: yes - name: Ensure the app root exists file: path: '{{ app_root }}' state: directory owner: '{{ app_uid }}' group: '{{ app_gid }}' mode: '0750' - name: Ensure the data directory exists file: path: '{{ app_root }}/data' state: directory owner: '{{ app_uid }}' group: '{{ app_gid }}' mode: '0750' - name: Look for the untracked site.env stat: path: '{{ app_root }}/site.env' register: menu_site_env - name: Warn when the admin has no password yet debug: msg: >- {{ app_root }}/site.env is missing, so the admin is disabled: the public menu still serves. Run scripts/provision-site-env.sh {{ app_root }} on this host, then re-run this playbook. when: not menu_site_env.stat.exists - name: Run the menu app docker_container: name: '{{ docker_container }}' image: '{{ image_name }}:{{ image_tag }}' state: started restart_policy: unless-stopped user: '{{ app_uid }}:{{ app_gid }}' # Force a rebuild of the container with -e recreate_container=true: that is # what applies a changed healthcheck or env to an existing container, since # the module otherwise leaves a running one alone. recreate: '{{ recreate_container | default(false) | bool }}' # Declared here rather than relied on from the image: podman builds an OCI # image by default and drops Docker's HEALTHCHECK instruction, so the # Dockerfile's own healthcheck never reaches the container. node is in the # image and the exec form needs no shell quoting. healthcheck: test: - CMD - node - -e - "fetch('http://127.0.0.1:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" interval: 30s timeout: 5s start_period: 30s retries: 3 env: MENU_ROOT: /data MENU_SITE_CONFIG: '/app/config/{{ site }}.site.json' SECURE_COOKIES: 'true' LOG_LEVEL: info PORT: '3000' volumes: - '{{ app_root }}:/data' networks: - name: '{{ docker_network }}' - name: Keep site.env readable only by the app user file: path: '{{ app_root }}/site.env' owner: '{{ app_uid }}' group: '{{ app_gid }}' mode: '0600' when: menu_site_env.stat.exists # Deliberately not the image's HEALTHCHECK: podman builds an OCI image by # default and drops the Docker HEALTHCHECK instruction, so State.Health is absent # and a wait on it fails on a perfectly healthy container — which stops the play # before Caddy is ever pointed at the app. - name: Wait for the app to answer inside its own container ansible.builtin.command: argv: - docker - exec - '{{ docker_container }}' - node - -e - "fetch('http://127.0.0.1:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" register: menu_health changed_when: false retries: 20 delay: 3 until: menu_health.rc == 0 failed_when: false - name: Fail when the app never answers fail: msg: >- {{ docker_container }} did not answer /healthz within a minute. Its logs say why: docker logs {{ docker_container }} when: menu_health.rc != 0 - name: Check the data directory for integrity problems # Reads the same directory the container writes, as the same user, so a # permissions mistake shows up here rather than at 8pm service. command: argv: - docker - run - --rm - --user - '{{ app_uid }}:{{ app_gid }}' - -e - MENU_ROOT=/data - -e - 'MENU_SITE_CONFIG=/app/config/{{ site }}.site.json' - --entrypoint - node - '{{ image_name }}:{{ image_tag }}' - src/cli/verify.ts register: menu_verify changed_when: false failed_when: false - name: Report integrity problems debug: msg: '{{ menu_verify.stdout_lines | default([]) }}' when: menu_verify.rc != 0