Как правильно настроить Docker volumes в SpringBoot приложении для отображения фотографий с файловой системы сервера?

У меня есть приложение на SpringBoot, где пользователь может загружать фотографии на сервер. Когда запускаю без докера, все прекрасно работает. Но когда запускаю приложение в докер контейнере, то фотографии не загружаются в папку /var/lib/public/images внутри контейнера (как я указывал в docker-compose), а загружаются в папку masterclass-photos, которая по идее не должна была создаваться внутри контейнера.

Как настроить докер или какие изменения внести в код, чтобы приложение заработало в докер контейнере?

Структура проекта. Папки masterclass-photos и motherforum-data создаются сами при запуске контейнера.

enter image description here

Когда в Postmane кликаю по ссылке фото, ловлю исключение UnknownHostException. Логи:

2022-11-23 12:18:31 java.net.UnknownHostException: masterclass-photos
2022-11-23 12:18:31     at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:567) ~[na:na]
2022-11-23 12:18:31     at java.base/java.net.Socket.connect(Socket.java:633) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.doConnect(FtpClient.java:1045) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.tryConnect(FtpClient.java:1010) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.connect(FtpClient.java:1102) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.connect(FtpClient.java:1088) ~[na:na]
...
...
a lot of line of stacktrace
ru.rcitsakha.motherforum.filter.CustomAuthorizationFilter.doFilterInternal(CustomAuthorizationFilter.java:73) ~[classes!/:1.0-SNAPSHOT]
...
...
a lot of line of stacktrace 

Класс CustomAuthorizationFilter, строка 73 которого фигурирует в логах

package ru.rcitsakha.motherforum.filter;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static java.util.Arrays.stream;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Slf4j
public class CustomAuthorizationFilter extends OncePerRequestFilter {
    private String jwtSecret;

    public void setJwtSecret(String jwtSecret) {
        this.jwtSecret = jwtSecret;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/token/refresh/**")) {
            filterChain.doFilter(request, response);
        } else {
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                try {
                    String token = authorizationHeader.substring("Bearer ".length());
                    Algorithm algorithm = Algorithm.HMAC256(jwtSecret.getBytes()); // should be same with authenticationFilter
                    JWTVerifier verifier = JWT.require(algorithm).build();
                    DecodedJWT decodedJWT = verifier.verify(token);
                    String username = decodedJWT.getSubject();
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    stream(roles).forEach(role -> {
                        authorities.add(new SimpleGrantedAuthority(role));
                    });
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(username, null, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                    filterChain.doFilter(request, response);
                } catch (Exception exception) {
                    log.error("Error logging in: {}", exception.getMessage());
                    response.setHeader("error", exception.getMessage());
                    response.setStatus(FORBIDDEN.value());

                    Map<String, String> error = new HashMap<>();
                    error.put("error_message", exception.getMessage());
                    response.setContentType(APPLICATION_JSON_VALUE);
                    new ObjectMapper().writeValue(response.getOutputStream(), error);
                }
            } else {
                filterChain.doFilter(request, response); // line 73 from logs
            }
        }
    }
}

docker-compose.yml

services:
  postgres:
    image: 'postgres:15'
    container_name: 'java-postgres'
    env_file:
      - config/.envfile
    volumes:
      - ./motherforum-data:/var/lib/postgresql/data


  app:
    build: ./
    container_name: 'java-app'
    environment:
      - 'PORT=8080'
      - 'SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/motherforumdb'
    ports:
      - 8080:8080
    depends_on:
      - postgres
    volumes:
      - ./masterclass-photos:/var/lib/public/images

Dockerfile

FROM openjdk:17
ADD /target/mother-forum-1.0-SNAPSHOT.jar backend.jar
ENTRYPOINT ["java", "-jar", "/backend.jar"]

Настройки resource handler

package ru.rcitsakha.motherforum.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.file.Path;
import java.nio.file.Paths;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        exposeDirectory("masterclass-photos", registry);
    }

    private void exposeDirectory(String dirName, ResourceHandlerRegistry registry) {
        Path uploadDir = Paths.get(dirName);
        String uploadPath = uploadDir.toFile().getAbsolutePath();

        if (dirName.startsWith("../")) {
            dirName = dirName.replace("../", "");
        }

        registry.addResourceHandler("/" + dirName + "/**")
                .addResourceLocations("file:/" + uploadPath + "/");
    }
}

Postman request (), где в теле JSON body мы видим ссылку на фото. Данный endpoint не требует JWT авторизации, потому что в настройках security прописал http.authorizeHttpRequests().antMatchers("/api/login/**", "/token/refresh", "api/motherforum/**").permitAll();

Подпапка 3 внутри masterclass-photos не создается в файловой системе сервера. Но она создается внутри контейнера.

enter image description here

Содержимое папки внутри контейнера /var/lib/public/images, где я ожидал увидеть фото внутри контейнера - пустое.

enter image description here

Mounts java контейнера

 "Mounts": [
            {
                "Type": "bind",
                "Source": "C:\\ProdApps\\mother-forum\\masterclass-photos",
                "Destination": "/var/lib/public/images",
                "Mode": "rw",
                "RW": true,
                "Propagation": "rprivate"
            }
        ] 

Класс MasterClass, которое содержит ссылки на фото. Может надо изменить путь в методе getPhotosImagePath() ?

package ru.rcitsakha.motherforum.model;


import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Entity
@Table(name = "masterclass")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class MasterClass {

    private static final String DATE_TIME_FORMATTER = "dd.MM.yyyy HH:mm";

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String title;

    @Column(length = 2000)
    private String description;

    private String address;

    private String dateTime;

    private boolean isExpired;

    @ManyToMany(fetch = FetchType.LAZY,
            cascade = {
                    CascadeType.PERSIST,
                    CascadeType.MERGE
            })
    @JoinTable(
            name = "masterclass_mother",
            joinColumns = @JoinColumn(name = "masterclass_id"),
            inverseJoinColumns = @JoinColumn(name = "mother_id")
    )
    @ToString.Exclude
    @JsonIgnore
    private Set<Mother> mothers = new HashSet<>();

    public void addMother(Mother mother) {
        this.mothers.add(mother);
        mother.getMasterClasses().add(this);
    }

    @Column(length = 128)
    @ElementCollection
    private List<String> photos = new ArrayList<>();

    public void removeMother(long motherId) {
        Mother mother = this.mothers.stream()
                .filter(m -> m.getId() == motherId)
                .findFirst()
                .orElse(null);

        if (mother != null) {
            this.mothers.remove(mother);
            mother.getMasterClasses().remove(this);
        }
    }

    public void copy(MasterClass masterClass) {
        this.title = masterClass.title;
        this.description = masterClass.description;
        this.address = masterClass.address;
        this.dateTime = masterClass.dateTime;
        this.isExpired = masterClass.isExpired;
    }

    @Transient
    public List<String> getPhotosImagePath() {
        if (photos == null) {
            return null;
        }
        return photos.stream()
                .map(photoFileName -> "/masterclass-photos/" + id + "/" + photoFileName)
                .collect(Collectors.toList());
    }
}

Ответы (0 шт):