Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions internal/nginx/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,20 @@ const mapsConfigFile = "00-flatrun-maps.conf"
// closing it, so nginx can reuse a pooled keepalive connection to the
// container. Closing here would defeat upstream keepalive on every ordinary
// request. WebSocket requests still get "upgrade".
//
// $flatrun_static_expires drives per-location static-asset caching. It keys off
// the request path's extension, so only static files get a long expiry; every
// other response maps to "off", which leaves the app's own Cache-Control intact.
// A location opts in by using it in an `expires` directive.
const mapsConfigContent = `# Managed by FlatRun. Do not edit.
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}
map $uri $flatrun_static_expires {
default off;
~*\.(?:css|js|mjs|json|png|jpe?g|gif|ico|svg|webp|avif|woff2?|ttf|otf|eot|map)$ 30d;
}
`

// infraConfigFiles are conf.d files the agent manages that are not deployment
Expand Down Expand Up @@ -772,6 +781,7 @@ func (m *Manager) groupDomainsByHost(domains []models.DomainConfig, deploymentNa
StripPrefix: d.StripPrefix,
OriginalPath: d.PathPrefix,
ProxyTimeout: timeout,
StaticCache: d.StaticCache,
})

if d.SSL.Enabled {
Expand Down Expand Up @@ -909,6 +919,7 @@ type locationData struct {
StripPrefix bool
OriginalPath string
ProxyTimeout int
StaticCache bool
// Upstream is the value assigned to $upstream: an upstream block name when
// keepalive is supported, otherwise the literal service:port.
Upstream string
Expand Down Expand Up @@ -1109,6 +1120,9 @@ server {
proxy_connect_timeout 60s;
proxy_send_timeout {{.ProxyTimeout}}s;
proxy_read_timeout {{.ProxyTimeout}}s;
{{- if .StaticCache}}
expires $flatrun_static_expires;
{{- end}}
{{- if $.SecurityEnabled}}
log_by_lua_block {
security.capture_event()
Expand Down Expand Up @@ -1200,6 +1214,9 @@ server {
proxy_connect_timeout 60s;
proxy_send_timeout {{.ProxyTimeout}}s;
proxy_read_timeout {{.ProxyTimeout}}s;
{{- if .StaticCache}}
expires $flatrun_static_expires;
{{- end}}
{{- if $.SecurityEnabled}}
log_by_lua_block {
security.capture_event()
Expand Down Expand Up @@ -1286,6 +1303,9 @@ server {
proxy_connect_timeout 60s;
proxy_send_timeout {{.ProxyTimeout}}s;
proxy_read_timeout {{.ProxyTimeout}}s;
{{- if .StaticCache}}
expires $flatrun_static_expires;
{{- end}}
{{- if $.SecurityEnabled}}
log_by_lua_block {
security.capture_event()
Expand Down Expand Up @@ -1338,6 +1358,9 @@ server {
proxy_connect_timeout 60s;
proxy_send_timeout {{.ProxyTimeout}}s;
proxy_read_timeout {{.ProxyTimeout}}s;
{{- if .StaticCache}}
expires $flatrun_static_expires;
{{- end}}
{{- if $.SecurityEnabled}}
log_by_lua_block {
security.capture_event()
Expand Down
28 changes: 28 additions & 0 deletions internal/nginx/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2288,6 +2288,34 @@ func TestRouteOnlyAliasEmittedIntoServerName(t *testing.T) {
}
}

// Static-asset caching is opt-in per domain: the expires directive appears only
// when the domain enables it, so other domains keep their exact previous output.
func TestStaticCacheEmitsExpiresOnlyWhenEnabled(t *testing.T) {
m := NewManager(&config.NginxConfig{ContainerWebrootPath: "/var/www/html"}, "/deployments", "")
build := func(on bool) string {
dep := &models.Deployment{
Name: "shop",
Metadata: &models.ServiceMetadata{
Domains: []models.DomainConfig{
{ID: "d1", Service: "web", ContainerPort: 80, Domain: "shop.example.com", StaticCache: on},
},
},
}
out, err := m.generateMultiDomainConfig(dep)
if err != nil {
t.Fatalf("generateMultiDomainConfig failed: %v", err)
}
return out
}

if on := build(true); !strings.Contains(on, "expires $flatrun_static_expires;") {
t.Errorf("expected the expires directive when static cache is on, got:\n%s", on)
}
if off := build(false); strings.Contains(off, "expires") {
t.Errorf("expected no expires directive when static cache is off, got:\n%s", off)
}
}

func newManagerWithDeployment(t *testing.T, domains []models.DomainConfig, compose string) (*Manager, *models.Deployment) {
t.Helper()
dir := t.TempDir()
Expand Down
4 changes: 4 additions & 0 deletions pkg/models/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ type DomainConfig struct {
// when unset; raise it for domains that proxy long-lived WebSocket
// connections so idle sockets are not closed mid-connection.
ProxyTimeout int `yaml:"proxy_timeout,omitempty" json:"proxy_timeout,omitempty"`
// StaticCache opts this domain into a long browser cache for static assets
// (css, js, images, fonts). It applies only to responses whose path has a
// static extension; dynamic responses keep the app's own cache headers.
StaticCache bool `yaml:"static_cache,omitempty" json:"static_cache,omitempty"`
}

type DatabaseConfig struct {
Expand Down
Loading