Skip to content

Commit a776964

Browse files
committed
feat: Support read-only root filesystem
1 parent cc26bb9 commit a776964

File tree

8 files changed

+78
-23
lines changed

8 files changed

+78
-23
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ special_host:
146146

147147
With this configuration the application would have the app title "My Special Application", when it is accessed via the host `special.example.com`, while the endpoints would stay the same in every instance of the application.
148148

149+
## Read-only Root Filesystem Support
150+
151+
It is recommended to use a read-only root filesystem when running containers. However, the following directories must remain writable when using this base image:
152+
153+
* `/config/.out`
154+
* This base image generates files in this directory at startup.
155+
* `/tmp`
156+
* Nginx uses this directory to manage cached files and the nginx.pid file. For more information, see [nginxinc/docker-nginx-unprivileged#troubleshooting-tips](https://github.com/nginxinc/docker-nginx-unprivileged/tree/af6e325d35e6833af9cdda8493866b88649e8aaf?tab=readme-ov-file#troubleshooting-tips).
157+
158+
It is possible to mount these directories as writable volumes. When using Kubernetes, one solution is to mount `emptyDir` volumes at these mount points.
159+
149160
## Development
150161

151162
Configuration files are dynamically generated via [gomplate templates](https://docs.gomplate.ca/).

config/bootstrap.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ else
2121
CONFIG_FILES="merge:${CONFIG_FILES}|file://${CONFIG_DIR}/.internal_default.yaml"
2222
fi
2323

24-
rm -rf "${CONFIG_DIR}/.out"
24+
rm -rf "${CONFIG_DIR}/.out/*"
25+
mkdir -p "${CONFIG_DIR}/.out/www"
2526

2627
cd "${CONFIG_DIR}"
2728

config/main.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
{{- range $key, $server := $effectiveConfig -}}
1111
{{- $spa_config_hash := tmpl.Exec "write-spa_config" $server -}}
1212
{{- $new_index := tmpl.Exec "write-index" (dict "path" "index.html" "base_href" $server.base_href "spa_config_hash" $spa_config_hash ) -}}
13-
{{- template "write-nginx-server-conf" (dict "name" $key "config" (merge (dict "index" $new_index) $server)) -}}
13+
{{- template "write-nginx-server-conf" (dict "name" $key "config" (merge (dict "index" $new_index "spa_config_hash" $spa_config_hash) $server)) -}}
1414
{{ end -}}

config/templates/server.tmpl

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ server {
2626

2727
{{ end -}}
2828
server {
29+
{{- $config_dir := env.Getenv "CONFIG_DIR" | test.Required "CONFIG_DIR is not defined" -}}
30+
{{- $out_www_dir := (filepath.Join $config_dir ".out" "www") -}}
31+
2932
{{- if and .http.enabled (not (and .https.enabled .http.https_redirect)) }}
3033
listen {{ .http.port }}{{ if .http.http2_enabled }} http2{{ end }}{{ if .is_default }} default_server{{ end }};
3134
{{- end }}
@@ -41,11 +44,46 @@ server {
4144

4245
keepalive_timeout {{ .keepalive.server.timeout_seconds }}s;
4346

47+
root {{ $app_root }};
48+
4449
{{ tmpl.Exec "access_log" .access_log }}
4550

4651
{{ tmpl.Exec "server-hardening" . | strings.Indent 4 " " }}
4752
{{ tmpl.Exec "general-security-headers" . | strings.Indent 4 " " }}
4853

54+
location = /spa_config.js {
55+
return 404;
56+
}
57+
58+
location = /spaConfig.js {
59+
return 404;
60+
}
61+
62+
location = /SpaConfig.js {
63+
return 404;
64+
}
65+
66+
location = /spa_config.{{ .spa_config_hash }}.js {
67+
root {{ $out_www_dir }};
68+
etag off;
69+
add_header Cache-Control "public, max-age=31536000, immutable";
70+
{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }}
71+
}
72+
73+
location = /index.html {
74+
add_header Cache-Control "no-cache, max-age=0";
75+
{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }}
76+
77+
return 200 /{{ .index }};
78+
}
79+
80+
location = /{{- .index }} {
81+
add_header Cache-Control "no-cache, max-age=0";
82+
{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }}
83+
84+
root {{ $out_www_dir }};
85+
}
86+
4987
error_page 500 502 503 504 /50x.html;
5088
location = /50x.html {
5189
root /usr/share/nginx/html;
@@ -60,41 +98,35 @@ server {
6098
# Prevent server requests on hashed resources
6199
# We ignore html files with hashes as they might got modified
62100
location ~* "\.[a-f0-9]{8,}(\.chunk)?\.(css|ico|pdf|flv|jpg|jpeg|png|gif|svg|ttf|otf|eot|woff|woff2|swf|map)$" {
63-
root {{ $app_root }};
64101
etag off;
65102
add_header Cache-Control "public, max-age=31536000, immutable";
66103
{{ tmpl.Exec "general-security-headers" . | strings.Indent 8 " " }}
67104
}
68105

69106
location ~* "\.[a-f0-9]{8,}(\.chunk)?\.js$" {
70-
root {{ $app_root }};
71107
etag off;
72108
add_header Cache-Control "public, max-age=31536000, immutable";
73109
{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }}
74110
}
75111

76112
# Return resource or 404 if resource could not be found
77113
location ~* "\.(css|ico|pdf|flv|jpg|jpeg|png|gif|svg|ttf|otf|eot|woff|woff2|swf|map)$" {
78-
root {{ $app_root }};
79114
add_header Cache-Control "no-cache, max-age=0";
80115
{{ tmpl.Exec "general-security-headers" . | strings.Indent 8 " " }}
81116
}
82117

83118
# Return resource or 404 if explicitly requested HTML or JavaScript document could not be found
84119
location ~* "\.(js|html|htm)$" {
85-
root {{ $app_root }};
86120
add_header Cache-Control "no-cache, max-age=0";
87121
{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }}
88122
}
89123

90-
# Return index.html on other requests by default
124+
# Return index on other requests by default
91125
location / {
92-
root {{ $app_root }};
93126
add_header Cache-Control "no-cache, max-age=0";
94127
{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }}
95128

96-
# Return index.html if it exists (does not exist in root as we delete it)
97-
index index.html;
129+
index {{ .index -}};
98130
try_files $uri /{{- .index -}};
99131
}
100132

config/templates/write_index.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717

1818
{{- $file_hash := ($file_content | crypto.SHA1) -}}
1919
{{- $new_path := .path | regexp.ReplaceLiteral "\\.htm(l)?$" (print "." $file_hash ".html") -}}
20-
{{- $file_content | file.Write (filepath.Join $config_dir ".out" $new_path) -}}
20+
{{- $file_content | file.Write (filepath.Join $config_dir ".out" "www" $new_path) -}}
2121
{{- $new_path -}}
2222
{{- end -}}

config/templates/write_spa_config.js.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var spaConfig = {{ . | toJSON }}
66
{{- $config_dir := env.Getenv "CONFIG_DIR" | test.Required "CONFIG_DIR is not defined" -}}
77
{{- $file_content := (tmpl.Exec "spa_config" .spa_config) -}}
88
{{- $file_hash := ($file_content | crypto.SHA1) -}}
9-
{{- $file_path := (filepath.Join $config_dir (print ".out/spa_config." $file_hash ".js")) -}}
9+
{{- $file_path := (filepath.Join $config_dir ".out" "www" (print "spa_config." $file_hash ".js")) -}}
1010
{{- tmpl.Exec "spa_config" .spa_config | file.Write $file_path -}}
1111
{{- $file_hash -}}
1212
{{- end -}}

docker-entrypoint.sh

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,4 @@ test -n "${CONFIG_DIR}" || ( echo 'CONFIG_DIR is not defined!' && false )
55

66
${CONFIG_DIR}/bootstrap.sh
77

8-
find "${APP_ROOT}" -maxdepth 1 -iname 'spa_config.js' -exec rm -f {} \;
9-
find "${APP_ROOT}" -maxdepth 1 -iname 'spa_config.*.js' -exec rm -f {} \;
10-
find "${APP_ROOT}" -maxdepth 1 -iname 'spaConfig.js' -exec rm -f {} \;
11-
find "${APP_ROOT}" -maxdepth 1 -iname 'spaConfig.*.js' -exec rm -f {} \;
12-
13-
find "${CONFIG_DIR}/.out" -name "spa_config.*.js" -exec cp {} ${APP_ROOT}/ \;
14-
find "${CONFIG_DIR}/.out" -name "index.*.html" -exec cp {} ${APP_ROOT}/ \;
15-
16-
# Delete existing index in root directory so that each listening server can serve its custom index as default document
17-
rm -f "${APP_ROOT}/index.html"
18-
198
exec nginx -g "daemon off;"

tests/src/test/java/de/codecentric/spa/server/tests/MiscTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import de.codecentric.spa.server.tests.containers.SpaServerContainer;
44
import org.junit.jupiter.api.Test;
55
import org.testcontainers.containers.Network;
6+
import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
7+
8+
import java.util.HashMap;
9+
import java.util.Objects;
610

711
import static de.codecentric.spa.server.tests.containers.Curl.curl;
812
import static org.assertj.core.api.Assertions.assertThat;
@@ -23,4 +27,22 @@ public void shouldStartWithTtyAndStdin() {
2327
}
2428
}
2529

30+
@Test
31+
public void shouldSupportReadOnlyFileSystemWithConfigOutVolume() {
32+
try (
33+
var network = Network.newNetwork();
34+
var container = new SpaServerContainer()
35+
.withCreateContainerCmdModifier(cmd -> Objects.requireNonNull(cmd.getHostConfig()).withReadonlyRootfs(true))
36+
.withNetwork(network)
37+
.withNetworkAliases("testcontainer")
38+
.withTmpFs(ImmutableMap.of(
39+
"/config/.out", "rw",
40+
"/tmp", "rw"
41+
))) {
42+
container.start();
43+
44+
assertThat(curl(network, "curl", "http://testcontainer")).contains("<base href=\"/\" />");
45+
}
46+
}
47+
2648
}

0 commit comments

Comments
 (0)