Как правильно настроить Docker volumes в SpringBoot приложении для отображения фотографий с файловой системы сервера?
У меня есть приложение на SpringBoot, где пользователь может загружать фотографии на сервер. Когда запускаю без докера, все прекрасно работает. Но когда запускаю приложение в докер контейнере, то фотографии не загружаются в папку /var/lib/public/images внутри контейнера (как я указывал в docker-compose), а загружаются в папку masterclass-photos, которая по идее не должна была создаваться внутри контейнера.
Как настроить докер или какие изменения внести в код, чтобы приложение заработало в докер контейнере?
Структура проекта. Папки masterclass-photos и motherforum-data создаются сами при запуске контейнера.
Когда в 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 не создается в файловой системе сервера. Но она создается внутри контейнера.
Содержимое папки внутри контейнера /var/lib/public/images, где я ожидал увидеть фото внутри контейнера - пустое.
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());
}
}



