dockernestjslocalstack

Connection refused error (127.0.0.1:4566) for Localstack S3 in NestJS


I'm quite new to AWS and docker.

In my local development environment, I want to upload files from the app server and place the files in localstack s3. However, when I try to access localstack s3 from the app server, the following error occurs.

AggregateError [ECONNREFUSED]: 
    at internalConnectMultiple (node:net:1139:18)
    at afterConnectMultiple (node:net:1712:7) {
  code: 'ECONNREFUSED',
  '$metadata': { attempts: 3, totalRetryDelay: 105 },
  [errors]: [
    Error: connect ECONNREFUSED ::1:4566
        at createConnectionError (node:net:1675:14)
        at afterConnectMultiple (node:net:1705:16) {
      errno: -111,
      code: 'ECONNREFUSED',
      syscall: 'connect',
      address: '::1',
      port: 4566
    },
    Error: connect ECONNREFUSED 127.0.0.1:4566
        at createConnectionError (node:net:1675:14)
        at afterConnectMultiple (node:net:1705:16) {
      errno: -111,
      code: 'ECONNREFUSED',
      syscall: 'connect',
      address: '127.0.0.1',
      port: 4566
    }
  ]
}

The docker-compose configuration is as follows. I used this link as reference.

version: '3'

services:
  test-app:
    build:
      dockerfile: Dockerfile
      context: .
      args:
        - VARIANT=22-bookworm
    container_name: test-app
    stdin_open: true
    dns:
      # Set the DNS server to be the LocalStack container
      - 10.0.2.20
    networks:
      - ls
      
  localstack:
    container_name: localstack
    image: localstack/localstack
    ports:
      - "4566:4566"
      - "4510-4559:4510-4559"
    environment:
      - SERVICES=s3
    networks:
      ls:
        # Set the container IP address in the 10.0.2.0/24 subnet
        ipv4_address: 10.0.2.20

networks:
  ls:
    ipam:
      config:
        # Specify the subnet range for IP address allocation
        - subnet: 10.0.2.0/24

The app runs on NestJS and the S3Client is initialized and injected with the following code snippet:

import { S3Client } from '@aws-sdk/client-s3';
import type { Provider } from '@nestjs/common';

export const S3_CLIENT_TOKEN = 'S3Client';

export const s3ClientProvider: Provider = {
  provide: S3_CLIENT_TOKEN,
  useValue: new S3Client({
    endpoint: 'https://localhost.localstack.cloud:4566',
    region: 'ap-northeast-1',
    credentials: {
      accessKeyId: '',
      secretAccessKey: '',
    },
  }),
};

The Controller works with the following code snippet:

  @Post()
  async uploadFile(
    @AuthorizedUser() _user: User,
    @UploadedFile() file: Express.Multer.File,
  ): Promise<void> {
    const command = new PutObjectCommand({
      Bucket: 'test',
      Key: file.filename,
      Body: await readFile(file.path),
    });

    const response = await this.s3Client.send(command);
    return;
  }

Can anyone please help me with this? Thanks in advance :)

I expected for a basket not existing error rather than a network error, or for the file upload to be successful.

I tried the steps in this link but I didn't get the results I expected.


Solution

  • As everywhere else in Docker, the special IP address 127.0.0.1 refers to "the current container", and not your other container. Official AWS S3 uses a scheme where different buckets are distinguished by host name, and the main function of the external localhost.localstack.cloud service is to allow DNS resolution to still work if you're using that system.

    In Docker, to connect to localstack S3, you need to

    1. Use the other container's name as the endpoint; and
    2. Enable the (older) path-based routing system.

    So your configuration block should look like

    export const s3ClientProvider: Provider = {
      provide: S3_CLIENT_TOKEN,
      useValue: new S3Client({
        endpoint: 'https://localstack:4566', // the other container's Compose service name
        forcePathStyle: true,                // add this setting
        region: 'ap-northeast-1',
        credentials: {
          accessKeyId: '',
          secretAccessKey: '',
        },
      }),
    };
    

    With this, you can remove a lot of the settings in your Compose file. You don't need to manually set up container DNS; you don't need to override the Compose-generated container names; the localstack host name works so long as both containers are on the same network, including the default network that Compose automatically creates; and so on. You can probably reduce the Compose file to just

    version: '3.8'
    services:
      test-app:
        build:
          context: .
          args:
            - VARIANT=22-bookworm
          
      localstack:
        image: localstack/localstack
        ports:  # optional, only needed if you want to access LocalStack from the host
          - "4566:4566"
          - "4510-4559:4510-4559"
        environment:
          - SERVICES=s3