Step 1 : Add a new constructor that supports both local + UpStash

internal/redis/client.go

func NewRedisFromEnv() (*Redis, error) {
		_ = godotenv.Load()

	if url := os.Getenv("REDIS_URL"); url != "" {
		/*
			- Converts URL -> redis.Options
			- Extracts Addr, Username, Password, TLS Settings
		*/
		opts, err := goredis.ParseURL(url)
		if err != nil {
			return nil, fmt.Errorf("parse redis url : %w", err)
		}

		/*
			- Enforces TLS when needed
			- Some providers require TLS , but give URLs like redis:// instead of rediss://
			  If you don’t attach TLSConfig, connection fails.
		*/
		if opts.TLSConfig == nil && len(url) >= 9 && url[:9] == "redis://" {
			opts.TLSConfig = &tls.Config{}
		}
		// Create Redis Client
		rdb := goredis.NewClient(opts)

		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		if err := rdb.Ping(ctx).Err(); err != nil {
			return nil, fmt.Errorf("redis ping (REDIS_URL) : %w", err)
		}
		return &Redis{
			Client: rdb,
		}, nil
	}
	// Fallback to REDIS_ADDR ( LOCAL/DOCKER )
	addr := os.Getenv("REDIS_ADDR")

	if addr == "" {
		return nil, fmt.Errorf("set REDIS_URL (recommended) or REDIS_ADDR")
	}

	// Create Redis Client ( Non-TLS )
	rdb := goredis.NewClient(&goredis.Options{
		Addr: addr,
	})

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := rdb.Ping(ctx).Err(); err != nil {
		return nil, fmt.Errorf("redis ping (REDIS_ADDR) : %w", err)
	}
	return &Redis{
		Client: rdb,
	}, nil
}

Step 2 — Update all your main.go files to use it

Everywhere you currently do:

redisClient,err:=redis.NewRedis(os.Getenv("REDIS_ADDR"))

replace with:

redisClient,err:=redis.NewRedisFromEnv()

Step 3 — Set Upstash URL in .env

Upstash is TLS enabled. Use rediss (TLS):

REDIS_URL=rediss://default:<PASSWORD>@wise-raptor-10797.upstash.io:6379

Step 4 : Write docker-compose.external.yml

services:
  migrate:
    image: migrate/migrate:v4.17.1
    env_file:
      - .env
    volumes:
      - ./apps/api-go/migrations:/migrations
    command: [
      "-path", "/migrations",
      "-database", "${DATABASE_URL}",
      "up"
      ]
    restart: on-failure
    

  api:
    build:
      context: .
      dockerfile: apps/api-go/Dockerfile
    image: runstate-api-go:local
    ports:
      - "${PORT:-3001}:3001"
    env_file:
      - .env
    depends_on:
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

  monitoring-pusher:
    image: runstate-api-go:local
    entrypoint: ["/monitoring-pusher"]
    env_file:
      - .env
    depends_on:
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

  worker-monitoring:
    image: runstate-api-go:local
    entrypoint: ["/worker-monitoring"]
    env_file:
      - .env
    depends_on:
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

  worker-status-change:
    image: runstate-api-go:local
    entrypoint: ["/worker-status-change"]
    env_file:
      - .env
    depends_on:
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

  worker-notification:
    image: runstate-api-go:local 
    entrypoint: ["/worker-notification"]
    env_file:
      - .env
    depends_on:
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

Step 5 : Rebuild the image

docker compose -f docker-compose.external.yml up --build

Step 6 : Check if external services are working