Need help debugging unhealthy container

Hi,

I am new to spin but I really like the concept. I was about to purchase the pro but wanted to get the free version running to test around before that. But exatly that is what giving me headaches right now.

I have an ubuntu server setup and trying to provision and deploy from my Win11 + WSL2 machine to this server.

I always end up with an unhealty container:

  • echo ‘:outbox_tray: Deploying Docker stack with compose files: docker-compose.yml docker-compose.prod.yml on 192.168.10.4…’
    :outbox_tray: Deploying Docker stack with compose files: docker-compose.yml docker-compose.prod.yml on 192.168.10.4…
  • docker -H ssh://[email protected]:22 stack deploy --compose-file docker-compose.yml --compose-file docker-compose.prod.yml --detach=false --prune spin-production
    Updating service spin-production_php (id: mt51k80o08rplzgryz8tjuu1g)
    Updating service spin-production_traefik (id: j8h7jk0gb2msc2cx7jar9y8uz)
    overall progress: 0 out of 1 tasks
    1/1: task: non-zero exit (137): dockerexec: unhealthy container

can anyone point me in the direction how to debug what causes the unhealty container??

thanks

Hi there :wave:

It likely means there is a start script or an application error (like a migration) that is failing to run.

You might want to disable Laravel Automations (AUTORUN_ENABLED=false) and try again:

More info here:

This post was flagged by the community and is temporarily hidden.

Are you able to post your files:

  1. docker-compose.yml
  2. docker-compose.prod.yml
  3. Dockerfile.php

Also, are you using spin deploy or pushing to GitHub Actions?

This post was flagged by the community and is temporarily hidden.

yes, I use spin deploy:

docker-compose.yml:

services:
 
  traefik:
    image: traefik:v3.2
 
  php:
    depends_on:
      - traefik

docker-compose.prod.yml:

services:
 
  traefik:
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
    networks:
        - web-public
    deploy:
      update_config:
        parallelism: 1
        delay: 5s
        order: stop-first
      placement:
        constraints:
          - node.role==manager
    volumes:
      # Mount the Docker socket as read-only so Traefik can listen to events
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - certificates:/certificates
    configs:
      - source: traefik
        target: /etc/traefik/traefik.yml
 
  php:
    image: ${SPIN_IMAGE_DOCKERFILE} # 👈 Change this if you're not using `spin deploy`
    environment:
      #PHP_OPCACHE_ENABLE: "1"
      #AUTORUN_ENABLED: "true" # 👈 Remove this line if you don't want Laravel Automations
      APP_ENV: "${SPIN_DEPLOYMENT_ENVIRONMENT}" # 👈 Remove this if you're not using `spin deploy`
      HEALTHCHECK_PATH: "/up" # Use Laravel's built-in health route
      SSL_MODE: full
      LOG_OUTPUT_LEVEL: debug
    networks:
      - web-public
    volumes:
      - "storage_private:/var/www/html/storage/app/private/"
      - "storage_public:/var/www/html/storage/app/public/"
      - "storage_sessions:/var/www/html/storage/framework/sessions"
      - "storage_logs:/var/www/html/storage/logs"
      - "database_sqlite:/var/www/html/.infrastructure/volume_data/sqlite"
    deploy:
      replicas: 1
      update_config:
        failure_action: rollback
        parallelism: 1
        delay: 5s
        order: start-first
      rollback_config:
        parallelism: 0
        order: stop-first
      restart_policy:
        condition: any
        delay: 10s
        max_attempts: 3
        window: 120s
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.my-php-app.rule=Host(`${SPIN_APP_DOMAIN}`)"
        - "traefik.http.routers.my-php-app.entrypoints=websecure"
        - "traefik.http.routers.my-php-app.tls=true"
        - "traefik.http.routers.my-php-app.tls.certresolver=letsencryptresolver"
        - "traefik.http.services.my-php-app.loadbalancer.server.port=8443"
        - "traefik.http.services.my-php-app.loadbalancer.server.scheme=https"
        # Health check
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.path=/up"
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.interval=30s"
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.timeout=5s"
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.scheme=http"
 
configs:
  traefik:
    name: "traefik-${SPIN_MD5_HASH_TRAEFIK_YML}.yml"
    file: ./.infrastructure/conf/traefik/prod/traefik.yml
 
volumes:
  certificates:
  storage_private:
  storage_public:
  storage_sessions:
  storage_logs:
  database_sqlite:
 
networks:
  web-public:

.spin.yml

##############################################################
# ???? Users - You must set at least one user
##############################################################
 
users:
   - username: alex
     name: Alex
     groups: ['sudo']
     password: "password"
     authorized_keys:
       - public_key: "pubkey"
   - username: deploy
     name: Deploy
     groups: ['sudo']
     password: "password"
     authorized_keys:
       - public_key: "pubkey"
 
 
##############################################################
# ???? Providers - You must set at least one provider
##############################################################
 
providers:
    - name: selfhost
#   - name: digitalocean
#     api_token: Set token here OR delete this line and set environment variable DO_API_TOKEN
 
#   - name: hetzner
#     api_token: Set token here OR delete this line and set environment variable HCLOUD_TOKEN
 
#   - name: vultr
#     api_token: Set token here OR delete this line and set environment variable VULTR_API_KEY
 
##############################################################
# ???? Servers - You must set at least one server
##############################################################
 
servers:
   - server_name: ubuntu-home
     environment: production
     address: 192.168.10.4
  #   hardware_profile: ubuntu-home-profile
 
  # - server_name: ubuntu-1gb-ord-2
  #   environment: staging
  #   hardware_profile: vultr_1c_1gb_ubuntu2404
 
##############################################################
# ???? Hardware Profiles
##############################################################
 
hardware_profiles:
  # Hetzner
  - name: hetzner_2c_2gb_ubuntu2404
    provider: hetzner
    profile_config:
      location: ash
      server_type: cpx11
      image: ubuntu-24.04
      backups: true
 
  # Vultr
  - name: vultr_1c_1gb_ubuntu2404
    provider: vultr
    profile_config:
      region: ord
      plan: vc2-1c-1gb
      os: "Ubuntu 24.04 LTS x64"
      backups: true
 
  # DigitalOcean
  - name: digitalocean_1c_1gb_ubuntu2404
    provider: digitalocean
    profile_config:
      region: nyc3
      size: s-1vcpu-1gb
      image: ubuntu-24-04-x64
      backups: true
 
##############################################################
# ???? Environments
##############################################################
environments:
  - name: production
  - name: staging
  - name: development
 
##############################################################
# ???? Advanced Server Configuration
##############################################################
 
# Timezone and contact settings
server_timezone: "Etc/UTC"
server_contact: [email protected]
 
# If you the SSH port below, you may need to run `spin provision -p <your-default-ssh-port>`
# to get a connection on your first provision. Otherwise, SSH will try connecting
# to your new port before the SSH server configuration is updated.
ssh_port: "22"
 
## You can set this to false to require a password for sudo.
## If you disable passwordless sudo, you must set a password for all sudo users.
## generate an encrypted hash with `spin mkpasswd`. Learn more:
## https://serversideup.net/open-source/spin/docs/command-reference/mkpasswd
use_passwordless_sudo: true
 
## Email Notifications
postfix_hostname: "{{ inventory_hostname }}"
 
## Set variables below to enable external SMTP relay
# postfix_relayhost: "smtp.example.com"
# postfix_relayhost_port: "587"
# postfix_relayhost_username: "myusername"
# postfix_relayhost_password: "mysupersecretpassword"
 
## Deploy user customization - You can customize the deploy user below if you'd like
# docker_user:
#   username: deploy
#   home: /opt/deploy
#   authorized_ssh_keys:
#     - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNJGtd7a4DBHsQi7HGrC5xz0eAEFHZ3Ogh3FEFI2345 fake@key"
#     - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFRfXxUZ8q9vHRcQZ6tLb0KwGHu8xjQHfYopZKLmnopQ anotherfake@key"

Dockerfile

############################################
# Base Image
############################################
 
# Learn more about the Server Side Up PHP Docker Images at:
# https://serversideup.net/open-source/docker-php/
FROM serversideup/php:8.4-fpm-nginx-alpine AS base
 
## Uncomment if you need to install additional PHP extensions
USER root
RUN install-php-extensions intl exif
 
############################################
# Development Image
############################################
FROM base AS development
 
# We can pass USER_ID and GROUP_ID as build arguments
# to ensure the www-data user has the same UID and GID
# as the user running Docker.
ARG USER_ID
ARG GROUP_ID
 
# Switch to root so we can set the user ID and group ID
USER root
 
# Set the user ID and group ID for www-data
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID  && \
    docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx
 
# Drop privileges back to www-data
USER www-data
 
############################################
# CI image
############################################
FROM base AS ci
 
# Sometimes CI images need to run as root
# so we set the ROOT user and configure
# the PHP-FPM pool to run as www-data
USER root
RUN echo "user = www-data" >> /usr/local/etc/php-fpm.d/docker-php-serversideup-pool.conf && \
    echo "group = www-data" >> /usr/local/etc/php-fpm.d/docker-php-serversideup-pool.conf
 
############################################
# Production Image
############################################
FROM base AS deploy
COPY --chown=www-data:www-data . /var/www/html
 
# Create the SQLite directory and set the owner to www-data (remove this if you're not using SQLite)
RUN mkdir -p /var/www/html/.infrastructure/volume_data/sqlite/ && \
    chown -R www-data:www-data /var/www/html/.infrastructure/volume_data/sqlite/
 
USER www-data

The key is troubleshooting the healthcheck routes:

docker-compose.prod.yml:

  php:
    environment:
      HEALTHCHECK_PATH: "/up" # Use Laravel's built-in health route
### More...
    labels:
        # Health check
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.path=/up"
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.interval=30s"
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.timeout=5s"
        - "traefik.http.services.my-php-app.loadbalancer.healthcheck.scheme=http"

The logs were saying /up was not returning a 200 response – meaning the container is not healthy and it will fail.

Things to check

  1. Are you running Laravel 11+ in this image?
  2. Are you sure Laravel is installed?

Workaround

You could:

  1. Comment the healthcheck stuff out entirely for testing
  2. Change these values to be /healthcheck which will check the FPM status instead of Laravel

More here:

Hello,

This can be closed. It works without the healthchecks. What I found out is that even though I am using Laravel11, the template I started with does not have the /up route … so that was a problem for sure.

I have to play around a bit more with getting the health checks to work. But I think I can handle troubleshooting from now on, now that you have pointed me in the right direction.

Big shout out and my appreciation

Glad it worked out! :+1: