gitroomhq / postiz-app

📨 Schedule social media posts, measure them, exchange with other members and get a lot of help from AI 🚀
https://postiz.com
Apache License 2.0
8.75k stars 2.12k forks source link

Compose hosted returns 404 for all images #380

Open troykelly opened 8 hours ago

troykelly commented 8 hours ago

📜 Description

When trying to use postiz in a compose hosted environment - all images return 404. I've tried setting the environment variable NEXT_PUBLIC_UPLOAD_DIRECTORY per https://docs.postiz.com/installation/docker-compose I've tried setting the environment variable NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY per https://github.com/gitroomhq/postiz-app/blob/main/.env.example

Does not work

    - STORAGE_PROVIDER=local
    - UPLOAD_DIRECTORY=/uploads
    - NEXT_PUBLIC_UPLOAD_DIRECTORY=/uploads

Does not work

    - STORAGE_PROVIDER=local
    - UPLOAD_DIRECTORY=/uploads
    - NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=/uploads

👟 Reproduction steps

  1. Generate an environment per the compose file instructions
  2. Try and use media uploaded to local storage
  3. Be sad at the 404

👍 Expected behavior

It should work

👎 Actual Behavior with Screenshots

Container Log

 ⨯ upstream image response failed for https://social.aperim.com/uploads/2024/10/20/82d104989abc73ed01fe9410e3decbd956.jpeg 404
 ⨯ upstream image response failed for https://social.aperim.com/uploads/2024/10/20/82d104989abc73ed01fe9410e3decbd956.jpeg 404

Files are there

 2f97d44f15e2:/app# ls -al /uploads/2024/10/20/82d104989abc73ed01fe9410e3decbd956.jpeg
-rw-r--r--    1 root     root          1807 Oct 20 04:46 /uploads/2024/10/20/82d104989abc73ed01fe9410e3decbd956.jpeg
2f97d44f15e2:/app# ls -al /uploads/2024/10/20/82d104989abc73ed01fe9410e3decbd956.jpeg
-rw-r--r--    1 root     root          1807 Oct 20 04:46 /uploads/2024/10/20/82d104989abc73ed01fe9410e3decbd956.jpeg

💻 Operating system

Linux

🤖 Node Version

v20.17.0

📃 Provide any additional context for the Bug.

Compose

# Default options with logging configuration
x-default-opts: &default-opts
  logging:
    driver: gelf
    options:
      gelf-address: ${GELF_SERVER}
      gelf-compression-type: none
      tag: "{{.ImageName}}/{{.Name}}/{{.ID}}"
      labels: purpose,site

# Base service definition for Postiz
x-postiz-service: &postiz-service
  <<: *default-opts
  image: ghcr.io/gitroomhq/postiz-app:latest
  environment:
    - MAIN_URL=https://${SITE_DOMAIN}
    - FRONTEND_URL=https://${SITE_DOMAIN}
    - NEXT_PUBLIC_BACKEND_URL=https://api.${SITE_DOMAIN}
    - JWT_SECRET=${JWT_SECRET}
    - DATABASE_URL=postgresql://${POSTGRES_USER:-postiz-user}:${POSTGRES_PASSWORD}@postgres-${SITE}:5432/${POSTGRES_DB:-postiz-db-local}
    - REDIS_URL=redis://:${REDIS_PASSWORD}@redis-${SITE}:6379
    - BACKEND_INTERNAL_URL=http://localhost:3000
    - IS_GENERAL=${IS_GENERAL:-true}
    - STORAGE_PROVIDER=local
    - UPLOAD_DIRECTORY=/uploads
    - NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=/uploads
    - OPENAI_API_KEY=${OPENAI_API_KEY:-}
    - EMAIL_PROVIDER
    - EMAIL_HOST
    - EMAIL_PORT
    - EMAIL_SECURE
    - EMAIL_USER
    - EMAIL_PASS
    - EMAIL_FROM_ADDRESS
    - EMAIL_FROM_NAME
    - X_API_KEY
    - X_API_SECRET
    - FACEBOOK_APP_ID
    - FACEBOOK_APP_SECRET
    - LINKEDIN_CLIENT_ID
    - LINKEDIN_CLIENT_SECRET
    - TIKTOK_CLIENT_ID
    - TIKTOK_CLIENT_SECRET

  volumes:
    - config:/config
    - uploads:/uploads
  networks:
    - backend
    - proxy
  sysctls:
    - net.ipv6.conf.all.disable_ipv6=0
  depends_on:
    - postgres
    - redis

services:

  postiz:
    <<: *postiz-service
    labels:
      - purpose=postiz
      - site=${SITE}
    deploy:
      replicas: ${POSTIZ_REPLICAS:-1}
      endpoint_mode: dnsrr
      placement:
        max_replicas_per_node: 1
        constraints:
          - node.role==worker
      rollback_config:
        parallelism: 2
        delay: 120s
        failure_action: continue
        monitor: 60s
        order: start-first
      update_config:
        parallelism: 2
        delay: 10s
        failure_action: rollback
        monitor: 60s
        order: start-first
      resources:
        limits:
          cpus: "6.00"
          memory: 6G
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s
      labels:
        - traefik.enable=true
        - traefik.docker.network=proxy
        - traefik.constraint-label=traefik-public

        #----------------------------------------------- routers for: postiz --------------------------------------------------
        # HTTP router
        - traefik.http.routers.postiz-${SITE}-http.rule=Host(`${SITE_DOMAIN}`)
        - traefik.http.routers.postiz-${SITE}-http.entrypoints=http
        - traefik.http.routers.postiz-${SITE}-http.middlewares=servicests,block-apple,https-redirect,reporturi
        - traefik.http.routers.postiz-${SITE}-http.service=noop@internal

        # HTTPS router
        - traefik.http.routers.postiz-${SITE}-https.rule=Host(`${SITE_DOMAIN}`)
        - traefik.http.routers.postiz-${SITE}-https.entrypoints=http3
        - traefik.http.routers.postiz-${SITE}-https.service=postiz-${SITE}
        - traefik.http.routers.postiz-${SITE}-https.tls.certresolver=${SITE_RESOLVER}
        - traefik.http.routers.postiz-${SITE}-https.middlewares=gzip,limit,reporturi,block-apple

        # HTTPS API router
        - traefik.http.routers.postiz-api-${SITE}-https.rule=Host(`api.${SITE_DOMAIN}`)
        - traefik.http.routers.postiz-api-${SITE}-https.entrypoints=http3
        - traefik.http.routers.postiz-api-${SITE}-https.service=postiz-api-${SITE}
        - traefik.http.routers.postiz-api-${SITE}-https.tls.certresolver=${SITE_RESOLVER}
        - traefik.http.routers.postiz-api-${SITE}-https.middlewares=cors-headers-${SITE},gzip,limit,reporturi,block-apple

        #----------------------------------------------- CORS Middleware --------------------------------------------------
        # Define CORS headers middleware
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowOriginList=https://${SITE_DOMAIN}
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowMethods=GET,OPTIONS,PUT,POST,DELETE
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowHeaders=Origin,Content-Type,Accept,Authorization
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowCredentials=true
        - traefik.http.middlewares.cors-headers-${SITE}.headers.addVaryHeader=true

        #====================================================== services ===========================================================
        - traefik.http.services.postiz-${SITE}.loadbalancer.server.port=4200
        - traefik.http.services.postiz-api-${SITE}.loadbalancer.server.port=3000
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "wget --no-verbose --tries=1 --spider http://localhost:4200/ && wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1",
        ]
      interval: 5s
      timeout: 2s
      retries: 20
      start_period: 60m

  postgres:
    <<: *default-opts
    image: postgis/postgis:${POSTGRES_MAJOR_VERSION}-${POSTGRES_MINOR_VERSION}.${POSTGRES_PATCH_VERSION}
    hostname: "postgres-${SITE}"
    networks:
      - backend
      - proxy
    labels:
      - purpose=postgres-odoo
      - site=${SITE}
    stop_grace_period: 600s
    deploy:
      replicas: 1
      endpoint_mode: dnsrr
      placement:
        max_replicas_per_node: 1
        constraints:
          - node.role==worker
      rollback_config:
        parallelism: 2
        delay: 120s
        failure_action: continue
        monitor: 60s
        order: stop-first
      update_config:
        parallelism: 2
        delay: 10s
        failure_action: rollback
        monitor: 60s
        order: stop-first
      resources:
        limits:
          cpus: "6.00"
          memory: 6G
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s
      labels:
        - traefik.enable=true
        - traefik.docker.network=proxy
        - traefik.constraint-label=traefik-public
        #----------------------------------------------- routers for: postgres --------------------------------------------------
        # postgres
        - traefik.tcp.routers.postgres-${SITE}.rule=HostSNI(`postgres.${SITE_DOMAIN}`)
        - traefik.tcp.routers.postgres-${SITE}.entrypoints=postgres
        - traefik.tcp.routers.postgres-${SITE}.service=postgres-${SITE}
        - traefik.tcp.routers.postgres-${SITE}.tls.certresolver=${SITE_RESOLVER}
        - traefik.tcp.routers.postgres-${SITE}.tls.domains[0].main=postgres.${SITE_DOMAIN}
        #====================================================== services ===========================================================
        - traefik.tcp.services.postgres-${SITE}.loadbalancer.server.port=${POSTGRES_PORT:-5432}
    environment:
      - POSTGRES_DB=${POSTGRES_DB:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_USER=${POSTGRES_USER:-odoo}
      - POSTGRES_PORT=${POSTGRES_PORT:-5432}
      - PGDATA=/var/lib/postgresql/data/pgdata
      - PGADMIN=/var/lib/postgresql/data/pgadmin
      - PGPASSFILE=/root/.pgpass
      - POSTGRES_SYNCHRONOUS_COMMIT=on
    entrypoint:
      - "bash"
      - "-c"
      - |
        apt-get update &&
        DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql-plpython3-$$PG_MAJOR python3-requests postgresql-$$PG_MAJOR-cron &&
        if [ -d "$$PGDATA" ] && [ "$$(ls -A $$PGDATA)" ]; then
            echo "#!/bin/bash
            set -e
            psql -v ON_ERROR_STOP=1 --username \"$$POSTGRES_USER\" --dbname \"$$POSTGRES_DB\" <<-EOSQL
              CREATE EXTENSION IF NOT EXISTS plpython3u;
            EOSQL" > /docker-entrypoint-initdb.d/50_plpython3.sh &&
            chmod +x /docker-entrypoint-initdb.d/50_plpython3.sh &&
            if ! grep -qF "shared_preload_libraries = 'pg_cron'" $$PGDATA/postgresql.conf; then
                echo "shared_preload_libraries = 'pg_cron'" >> $$PGDATA/postgresql.conf;
            fi &&
            if ! grep -qF "cron.database_name = '$${POSTGRES_DB}'" $$PGDATA/postgresql.conf; then
                echo "cron.database_name = '$${POSTGRES_DB}'" >> $$PGDATA/postgresql.conf;
            fi
        fi &&
        mkdir -p $$PGADMIN &&
        echo $$POSTGRES_PASSWORD > "$$PGADMIN/$${POSTGRES_DB}.$${POSTGRES_USER}" &&
        echo "{\"Servers\": {\"1\": {\"Name\": \"$${POSTGRES_DB} $$POSTGRES_USER\", \"Group\": \"Odoo\", \"Host\": \"$$(hostname)\", \"HostAddr\": \"$$(hostname)\", \"Port\": $${POSTGRES_PORT}, \"MaintenanceDB\": \"postgres\", \"Username\": \"$${POSTGRES_USER}\", \"SSLMode\": \"${ODOO_POSTGRES_SSL_MODE:-disable}\", \"Comment\": \"Odoo Postgres Server\", \"PassFile\": \"/data/pgadmin/$${POSTGRES_DB}.$${POSTGRES_USER}\", \"Shared\": true, \"SharedUsername\": \"$${POSTGRES_DB}-$$POSTGRES_USER\"}}}" > "$$PGADMIN/servers.json" &&
        chown -R 5050:5050 $${PGADMIN} &&
        chmod 700 $${PGADMIN} &&
        chmod 0600 "$$PGADMIN/$${POSTGRES_DB}.$${POSTGRES_USER}" &&
        echo "Creating pgAdmin data directory at $${PGADMIN}"
        echo 'localhost:5432:*:'$$POSTGRES_USER':'$$POSTGRES_PASSWORD > /root/.pgpass &&
        chown 999 /root/.pgpass &&
        chmod 0600 /root/.pgpass &&
        exec docker-entrypoint.sh postgres
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=0
    volumes:
      - "/etc/timezone:/etc/timezone:ro"
      - postgres:/var/lib/postgresql/data
      - type: tmpfs
        target: "/tmp"
        tmpfs:
          size: 2147483648
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "PGPASSWORD=$${POSTGRES_PASSWORD} pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} && PGPASSWORD=$${POSTGRES_PASSWORD} psql -U $${POSTGRES_USER} -d template_postgis -tAc \"SELECT 1 FROM pg_extension WHERE extname='postgis';\" | grep -q 1 || exit 1",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 60m

  postgres-backup:
    <<: *default-opts
    image: postgis/postgis:${POSTGRES_MAJOR_VERSION}-${POSTGRES_MINOR_VERSION}.${POSTGRES_PATCH_VERSION}
    networks:
      - backend
    labels:
      - purpose=postgres-backup
      - site=${SITE}
    deploy:
      replicas: 1
      endpoint_mode: dnsrr
      placement:
        max_replicas_per_node: 1
        constraints:
          - node.role==worker
      rollback_config:
        parallelism: 2
        delay: 120s
        failure_action: continue
        monitor: 60s
        order: start-first
      update_config:
        parallelism: 2
        delay: 10s
        failure_action: rollback
        monitor: 60s
        order: start-first
      resources:
        limits:
          cpus: "6.00"
          memory: 1G
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s
    environment:
      - POSTGRES_DB=${POSTGRES_DB:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_USER=${POSTGRES_USER:-odoo}
      - BACKUP_DESTINATION=/backups/dump
      - REMOVE_BEFORE=60
    command:
      - "bash"
      - "-c"
      - |
        apt-get update &&
        DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql-client &&
        BACKUP_INTERVAL=$${BACKUP_INTERVAL:-12} &&
        WAIT_SECONDS=60 &&
        MAX_TRIES=60 &&
        try=0 &&
        until pg_isready -h postgres -U $${POSTGRES_USER}; do
         if [ $$try -gt $$MAX_TRIES ]; then
           echo "Postgres service did not start within the expected time." && exit 1;
         fi
         sleep $$WAIT_SECONDS && let try+=1;
        done &&
        while true; do
          YEAR=`date +"%Y"` && MONTH=`date +"%B"` && DAY=`date +"%d"` && TIME=`date +"%H%M"` &&
          DESTINATION_PATH=$${BACKUP_DESTINATION:-/backups}/$$YEAR/$$MONTH/$$DAY$$TIME &&
          mkdir -p $$DESTINATION_PATH &&
          BACKUP_FILE=$$DESTINATION_PATH/dump.sql &&
          COMPRESSED_FILE=$$BACKUP_FILE.gz &&
          if PGPASSWORD=$${POSTGRES_PASSWORD} pg_dumpall -U $${POSTGRES_USER} -h postgres --clean --if-exists --inserts --on-conflict-do-nothing --quote-all-identifiers --no-password -f $$BACKUP_FILE; then
            gzip $$BACKUP_FILE &&
            echo "Backup taken at $$YEAR/$$MONTH/$$DAY$$TIME" &&
            echo "To restore this backup, use the following command:" &&
            echo "export PGPASSWORD=[your_password] && gunzip -c $$COMPRESSED_FILE | psql -h localhost -U $${POSTGRES_USER} -d $${POSTGRES_DB}"
          else
            echo "Backup failed at $$YEAR/$$MONTH/$$DAY$$TIME"
          fi &&
          find $${BACKUP_DESTINATION:-/backups}/ -mtime +$${REMOVE_BEFORE:-60} -type f -delete &&
          sleep $${BACKUP_INTERVAL}h;
        done
    volumes:
      - "/etc/timezone:/etc/timezone:ro"
      - postgres:/backups

  redis:
    <<: *default-opts
    image: "redis:latest"
    hostname: "redis-${SITE}"
    networks:
      - backend
      - proxy
    labels:
      - purpose=odoo-redis
      - site=${SITE}
    command: redis-server --appendonly yes --replica-read-only no --requirepass ${REDIS_PASSWORD}
    volumes:
      - "/etc/timezone:/etc/timezone:ro"
      - "redis-data:/data"
      - type: tmpfs
        target: "/tmp"
        tmpfs:
          size: 536870912
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=0
    deploy:
      replicas: 1
      endpoint_mode: dnsrr
      placement:
        max_replicas_per_node: 1
        constraints:
          - node.role==worker
      rollback_config:
        parallelism: 2
        delay: 120s
        failure_action: continue
        monitor: 60s
        order: stop-first
      update_config:
        parallelism: 2
        delay: 10s
        failure_action: rollback
        monitor: 60s
        order: stop-first
      resources:
        limits:
          cpus: "6.00"
          memory: 1G
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s
      labels:
        - traefik.enable=true
        - traefik.docker.network=proxy
        - traefik.constraint-label=traefik-public
        #----------------------------------------------- routers for: redis --------------------------------------------------
        # redis
        - traefik.tcp.routers.redis-${SITE}.rule=HostSNI(`redis.${SITE_DOMAIN}`)
        - traefik.tcp.routers.redis-${SITE}.entrypoints=redis
        - traefik.tcp.routers.redis-${SITE}.service=redis-${SITE}
        - traefik.tcp.routers.redis-${SITE}.tls.certresolver=${SITE_RESOLVER}
        - traefik.tcp.routers.redis-${SITE}.tls.domains[0].main=redis.${SITE_DOMAIN}
        #====================================================== services ===========================================================
        - traefik.tcp.services.redis-${SITE}.loadbalancer.server.port=6379
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      retries: 3
      timeout: 5s
      start_period: 10m

volumes:
  postgres:
    driver: glusterfs
    name: ${GLUSTER_VOLUME_POSTGRES}
  redis-data:
    driver: glusterfs
    name: ${GLUSTER_VOLUME_REDIS}
  config:
    driver: glusterfs
    name: ${GLUSTER_VOLUME_CONFIG}
  uploads:
    driver: glusterfs
    name: ${GLUSTER_VOLUME_UPLOADS}

networks:
  proxy:
    name: proxy
    driver: overlay
    external: true
  backend:
    driver: overlay

👀 Have you spent some time to check if this bug has been raised before?

Are you willing to submit PR?

Yes I am willing to submit a PR!

troykelly commented 8 hours ago

The Traefik Instructions don't mention that bypassing the caddy service in the container prevents access to /uploads

A ~fix~ hack for this is:

      labels:
        - traefik.enable=true
        - traefik.docker.network=proxy
        - traefik.constraint-label=traefik-public

        #----------------------------------------------- routers for: postiz --------------------------------------------------
        # HTTP router
        - traefik.http.routers.postiz-${SITE}-http.rule=Host(`${SITE_DOMAIN}`)
        - traefik.http.routers.postiz-${SITE}-http.entrypoints=http
        - traefik.http.routers.postiz-${SITE}-http.middlewares=servicests,block-apple,https-redirect,reporturi
        - traefik.http.routers.postiz-${SITE}-http.service=noop@internal

        # HTTPS router for uploads
        - traefik.http.routers.postiz-uploads-${SITE}-https.rule=Host(`${SITE_DOMAIN}`) && PathPrefix(`/uploads/`)
        - traefik.http.routers.postiz-uploads-${SITE}-https.entrypoints=http3
        - traefik.http.routers.postiz-uploads-${SITE}-https.service=postiz-uploads-${SITE}
        - traefik.http.routers.postiz-uploads-${SITE}-https.tls.certresolver=${SITE_RESOLVER}
        - traefik.http.routers.postiz-uploads-${SITE}-https.middlewares=gzip,limit,reporturi,block-apple
        - traefik.http.routers.postiz-uploads-${SITE}-https.priority=20

        # HTTPS router for frontend
        - traefik.http.routers.postiz-${SITE}-https.rule=Host(`${SITE_DOMAIN}`) && !PathPrefix(`/uploads/`)
        - traefik.http.routers.postiz-${SITE}-https.entrypoints=http3
        - traefik.http.routers.postiz-${SITE}-https.service=postiz-${SITE}
        - traefik.http.routers.postiz-${SITE}-https.tls.certresolver=${SITE_RESOLVER}
        - traefik.http.routers.postiz-${SITE}-https.middlewares=gzip,limit,reporturi,block-apple
        - traefik.http.routers.postiz-${SITE}-https.priority=10

        # HTTPS API router
        - traefik.http.routers.postiz-api-${SITE}-https.rule=Host(`api.${SITE_DOMAIN}`)
        - traefik.http.routers.postiz-api-${SITE}-https.entrypoints=http3
        - traefik.http.routers.postiz-api-${SITE}-https.service=postiz-api-${SITE}
        - traefik.http.routers.postiz-api-${SITE}-https.tls.certresolver=${SITE_RESOLVER}
        - traefik.http.routers.postiz-api-${SITE}-https.middlewares=cors-headers-${SITE},gzip,limit,reporturi,block-apple

        #----------------------------------------------- CORS Middleware --------------------------------------------------
        # Define CORS headers middleware
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowOriginList=https://${SITE_DOMAIN}
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowMethods=GET,OPTIONS,PUT,POST,DELETE
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowHeaders=Origin,Content-Type,Accept,Authorization
        - traefik.http.middlewares.cors-headers-${SITE}.headers.accessControlAllowCredentials=true
        - traefik.http.middlewares.cors-headers-${SITE}.headers.addVaryHeader=true

        #====================================================== services ===========================================================
        - traefik.http.services.postiz-${SITE}.loadbalancer.server.port=4200
        - traefik.http.services.postiz-api-${SITE}.loadbalancer.server.port=3000
        - traefik.http.services.postiz-uploads-${SITE}.loadbalancer.server.port=5000