diff --git a/README.md b/README.md index 6099ff0..ed2c88d 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,17 @@ special_host: 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. +## Read-only Root Filesystem Support + +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: + +* `/config/.out` + * This base image generates files in this directory at startup. +* `/tmp` + * 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). + +It is possible to mount these directories as writable volumes. When using Kubernetes, one solution is to mount `emptyDir` volumes at these mount points. + ## Development Configuration files are dynamically generated via [gomplate templates](https://docs.gomplate.ca/). diff --git a/config/bootstrap.sh b/config/bootstrap.sh index 6c37e26..4fe0c21 100755 --- a/config/bootstrap.sh +++ b/config/bootstrap.sh @@ -21,7 +21,8 @@ else CONFIG_FILES="merge:${CONFIG_FILES}|file://${CONFIG_DIR}/.internal_default.yaml" fi -rm -rf "${CONFIG_DIR}/.out" +rm -rf "${CONFIG_DIR}/.out/*" +mkdir -p "${CONFIG_DIR}/.out/www" cd "${CONFIG_DIR}" diff --git a/config/main.tmpl b/config/main.tmpl index 9bf2301..d65d390 100644 --- a/config/main.tmpl +++ b/config/main.tmpl @@ -10,5 +10,5 @@ {{- range $key, $server := $effectiveConfig -}} {{- $spa_config_hash := tmpl.Exec "write-spa_config" $server -}} {{- $new_index := tmpl.Exec "write-index" (dict "path" "index.html" "base_href" $server.base_href "spa_config_hash" $spa_config_hash ) -}} - {{- template "write-nginx-server-conf" (dict "name" $key "config" (merge (dict "index" $new_index) $server)) -}} + {{- template "write-nginx-server-conf" (dict "name" $key "config" (merge (dict "index" $new_index "spa_config_hash" $spa_config_hash) $server)) -}} {{ end -}} diff --git a/config/templates/server.tmpl b/config/templates/server.tmpl index fc78edb..ff97dec 100644 --- a/config/templates/server.tmpl +++ b/config/templates/server.tmpl @@ -26,6 +26,9 @@ server { {{ end -}} server { + {{- $config_dir := env.Getenv "CONFIG_DIR" | test.Required "CONFIG_DIR is not defined" -}} + {{- $out_www_dir := (filepath.Join $config_dir ".out" "www") -}} + {{- if and .http.enabled (not (and .https.enabled .http.https_redirect)) }} listen {{ .http.port }}{{ if .http.http2_enabled }} http2{{ end }}{{ if .is_default }} default_server{{ end }}; {{- end }} @@ -41,11 +44,46 @@ server { keepalive_timeout {{ .keepalive.server.timeout_seconds }}s; + root {{ $app_root }}; + {{ tmpl.Exec "access_log" .access_log }} {{ tmpl.Exec "server-hardening" . | strings.Indent 4 " " }} {{ tmpl.Exec "general-security-headers" . | strings.Indent 4 " " }} + location = /spa_config.js { + return 404; + } + + location = /spaConfig.js { + return 404; + } + + location = /SpaConfig.js { + return 404; + } + + location = /spa_config.{{ .spa_config_hash }}.js { + root {{ $out_www_dir }}; + etag off; + add_header Cache-Control "public, max-age=31536000, immutable"; +{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }} + } + + location = /index.html { + add_header Cache-Control "no-cache, max-age=0"; +{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }} + + return 200 /{{ .index }}; + } + + location = /{{- .index }} { + add_header Cache-Control "no-cache, max-age=0"; +{{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }} + + root {{ $out_www_dir }}; + } + error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; @@ -60,14 +98,12 @@ server { # Prevent server requests on hashed resources # We ignore html files with hashes as they might got modified location ~* "\.[a-f0-9]{8,}(\.chunk)?\.(css|ico|pdf|flv|jpg|jpeg|png|gif|svg|ttf|otf|eot|woff|woff2|swf|map)$" { - root {{ $app_root }}; etag off; add_header Cache-Control "public, max-age=31536000, immutable"; {{ tmpl.Exec "general-security-headers" . | strings.Indent 8 " " }} } location ~* "\.[a-f0-9]{8,}(\.chunk)?\.js$" { - root {{ $app_root }}; etag off; add_header Cache-Control "public, max-age=31536000, immutable"; {{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }} @@ -75,26 +111,22 @@ server { # Return resource or 404 if resource could not be found location ~* "\.(css|ico|pdf|flv|jpg|jpeg|png|gif|svg|ttf|otf|eot|woff|woff2|swf|map)$" { - root {{ $app_root }}; add_header Cache-Control "no-cache, max-age=0"; {{ tmpl.Exec "general-security-headers" . | strings.Indent 8 " " }} } # Return resource or 404 if explicitly requested HTML or JavaScript document could not be found location ~* "\.(js|html|htm)$" { - root {{ $app_root }}; add_header Cache-Control "no-cache, max-age=0"; {{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }} } - # Return index.html on other requests by default + # Return index on other requests by default location / { - root {{ $app_root }}; add_header Cache-Control "no-cache, max-age=0"; {{ tmpl.Exec "web-document-hardening" . | strings.Indent 8 " " }} - # Return index.html if it exists (does not exist in root as we delete it) - index index.html; + index {{ .index -}}; try_files $uri /{{- .index -}}; } diff --git a/config/templates/write_index.tmpl b/config/templates/write_index.tmpl index 9e6c0c1..31f1b01 100644 --- a/config/templates/write_index.tmpl +++ b/config/templates/write_index.tmpl @@ -17,6 +17,6 @@ {{- $file_hash := ($file_content | crypto.SHA1) -}} {{- $new_path := .path | regexp.ReplaceLiteral "\\.htm(l)?$" (print "." $file_hash ".html") -}} - {{- $file_content | file.Write (filepath.Join $config_dir ".out" $new_path) -}} + {{- $file_content | file.Write (filepath.Join $config_dir ".out" "www" $new_path) -}} {{- $new_path -}} {{- end -}} diff --git a/config/templates/write_spa_config.js.tmpl b/config/templates/write_spa_config.js.tmpl index 599e93c..0dda539 100644 --- a/config/templates/write_spa_config.js.tmpl +++ b/config/templates/write_spa_config.js.tmpl @@ -6,7 +6,7 @@ var spaConfig = {{ . | toJSON }} {{- $config_dir := env.Getenv "CONFIG_DIR" | test.Required "CONFIG_DIR is not defined" -}} {{- $file_content := (tmpl.Exec "spa_config" .spa_config) -}} {{- $file_hash := ($file_content | crypto.SHA1) -}} -{{- $file_path := (filepath.Join $config_dir (print ".out/spa_config." $file_hash ".js")) -}} +{{- $file_path := (filepath.Join $config_dir ".out" "www" (print "spa_config." $file_hash ".js")) -}} {{- tmpl.Exec "spa_config" .spa_config | file.Write $file_path -}} {{- $file_hash -}} {{- end -}} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 2b12859..4b9204f 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -5,15 +5,4 @@ test -n "${CONFIG_DIR}" || ( echo 'CONFIG_DIR is not defined!' && false ) ${CONFIG_DIR}/bootstrap.sh -find "${APP_ROOT}" -maxdepth 1 -iname 'spa_config.js' -exec rm -f {} \; -find "${APP_ROOT}" -maxdepth 1 -iname 'spa_config.*.js' -exec rm -f {} \; -find "${APP_ROOT}" -maxdepth 1 -iname 'spaConfig.js' -exec rm -f {} \; -find "${APP_ROOT}" -maxdepth 1 -iname 'spaConfig.*.js' -exec rm -f {} \; - -find "${CONFIG_DIR}/.out" -name "spa_config.*.js" -exec cp {} ${APP_ROOT}/ \; -find "${CONFIG_DIR}/.out" -name "index.*.html" -exec cp {} ${APP_ROOT}/ \; - -# Delete existing index in root directory so that each listening server can serve its custom index as default document -rm -f "${APP_ROOT}/index.html" - exec nginx -g "daemon off;" diff --git a/tests/src/test/java/de/codecentric/spa/server/tests/MiscTests.java b/tests/src/test/java/de/codecentric/spa/server/tests/MiscTests.java index 5e0cb76..3021588 100644 --- a/tests/src/test/java/de/codecentric/spa/server/tests/MiscTests.java +++ b/tests/src/test/java/de/codecentric/spa/server/tests/MiscTests.java @@ -3,6 +3,10 @@ import de.codecentric.spa.server.tests.containers.SpaServerContainer; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Network; +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; + +import java.util.HashMap; +import java.util.Objects; import static de.codecentric.spa.server.tests.containers.Curl.curl; import static org.assertj.core.api.Assertions.assertThat; @@ -23,4 +27,22 @@ public void shouldStartWithTtyAndStdin() { } } + @Test + public void shouldSupportReadOnlyFileSystemWithConfigOutVolume() { + try ( + var network = Network.newNetwork(); + var container = new SpaServerContainer() + .withCreateContainerCmdModifier(cmd -> Objects.requireNonNull(cmd.getHostConfig()).withReadonlyRootfs(true)) + .withNetwork(network) + .withNetworkAliases("testcontainer") + .withTmpFs(ImmutableMap.of( + "/config/.out", "rw", + "/tmp", "rw" + ))) { + container.start(); + + assertThat(curl(network, "curl", "http://testcontainer")).contains(""); + } + } + }