PromucFlow_constructor/deploy/docker/fs/opt/appsmith/caddy-reconfigure.mjs
Wyatt Walter 336d318222
fix: logs cleanup (#41275)
## Description
> [!TIP]  
> _Add a TL;DR when the description is longer than 500 words or
extremely technical (helps the content, marketing, and DevRel team)._
>
> _Please also include relevant motivation and context. List any
dependencies that are required for this change. Add links to Notion,
Figma or any other documents that might be relevant to the PR._

Addresses at least a couple of issues in a number of support tickets
about logs volume:

- we were double-logging all messages via Supervisor's
eventlistener:stdout configuration. Once to the sub-process's logs, and
once to another file in the logs/supervisor directory. The purpose of
this listener is to send logs to stdout/stderr so they can be picked up
by log aggregation services, no need to write again.
- we had debug logs enabled for Caddy which was creating quite a bit of
log volume in `logs/editor/<hostname>-stderr.log`
- bonus fix: in a multi-container deployment, all containers were trying
to write to `logs/supervisor/supervisord.log` making trying to
troubleshoot those deployments more difficult.

> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.Sanity"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/18222964844>
> Commit: 54b5a1a1c52408ae30472d1b5f25a157603fd626
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=18222964844&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Fri, 03 Oct 2025 13:38:52 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Simplified logging to route process output to standard output with
hostname tagging, reducing per-file logs and disk usage.
* Improved reliability of log capture with a dedicated stdout event
handler.
* Reduced log noise by disabling debug logging in the web server
configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-03 11:08:14 -05:00

245 lines
6.6 KiB
JavaScript

import * as fs from "fs"
import {dirname} from "path"
import {spawnSync} from "child_process"
import {X509Certificate} from "crypto"
// The custom domain is expected to only have the domain. So if it has a protocol, we ignore the whole value.
// This was the effective behaviour before Caddy.
const CUSTOM_DOMAIN = (process.env.APPSMITH_CUSTOM_DOMAIN || "").replace(/^https?:\/\/.+$/, "")
const CaddyfilePath = process.env.TMP + "/Caddyfile"
const AppsmithCaddy = process.env._APPSMITH_CADDY
// Rate limit environment.
const isRateLimitingEnabled = process.env.APPSMITH_RATE_LIMIT !== "disabled"
const RATE_LIMIT = parseInt(process.env.APPSMITH_RATE_LIMIT || 100, 10)
let certLocation = null
if (CUSTOM_DOMAIN !== "") {
try {
fs.accessSync("/appsmith-stacks/ssl/fullchain.pem", fs.constants.R_OK)
certLocation = "/appsmith-stacks/ssl"
} catch {
// no custom certs, see if old certbot certs are there.
const letsEncryptCertLocation = "/appsmith-stacks/letsencrypt/live/" + CUSTOM_DOMAIN
const fullChainPath = letsEncryptCertLocation + `/fullchain.pem`
try {
fs.accessSync(fullChainPath, fs.constants.R_OK)
console.log("Old Let's Encrypt cert file exists, now checking if it's expired.")
if (!isCertExpired(fullChainPath)) {
certLocation = letsEncryptCertLocation
}
} catch {
// no certs there either, ignore.
}
}
}
const frameAncestorsPolicy = (process.env.APPSMITH_ALLOWED_FRAME_ANCESTORS || "'self'")
.replace(/;.*$/, "")
const parts = []
parts.push(`
{
admin 0.0.0.0:2019
persist_config off
acme_ca_root /etc/ssl/certs/ca-certificates.crt
servers {
protocols h1 h2 h3
trusted_proxies static 0.0.0.0/0
metrics
}
${isRateLimitingEnabled ? "order rate_limit before basicauth" : ""}
}
(file_server) {
file_server {
precompressed br gzip
disable_canonical_uris
}
}
(reverse_proxy) {
reverse_proxy {
to 127.0.0.1:{args[0]}
header_up -Forwarded
header_up X-Appsmith-Request-Id {http.request.uuid}
}
}
(all-config) {
log {
output stdout
}
# skip logs for health check
log_skip /api/v1/health
# skip logs for sourcemap files
@source-map-files {
path_regexp ^.*\.(js|css)\.map$
}
log_skip @source-map-files
# The internal request ID header should never be accepted from an incoming request.
request_header -X-Appsmith-Request-Id
# Ref: https://stackoverflow.com/a/38191078/151048
# We're only accepting v4 UUIDs today, in order to not make it too lax unless needed.
@valid-request-id expression {header.X-Request-Id}.matches("(?i)^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$")
header @valid-request-id X-Request-Id {header.X-Request-Id}
@invalid-request-id expression !{header.X-Request-Id}.matches("(?i)^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$")
header @invalid-request-id X-Request-Id invalid_request_id
request_header @invalid-request-id X-Request-Id invalid_request_id
header {
-Server
Content-Security-Policy "frame-ancestors ${frameAncestorsPolicy}"
X-Content-Type-Options "nosniff"
X-Appsmith-Request-Id {http.request.uuid}
}
header /static/* {
Cache-Control "public, max-age=31536000, immutable"
}
request_body {
max_size ${process.env.APPSMITH_CODEC_SIZE || 150}MB
}
handle {
root * {$WWW_PATH}
try_files /loading.html /index.html
import file_server
}
root * /opt/appsmith/editor
@file file
handle @file {
import file_server
}
handle /static/* {
error 404
}
handle /info {
root * /opt/appsmith
rewrite * /info.json
import file_server
}
@backend path /api/* /oauth2/* /login/*
handle @backend {
import reverse_proxy 8080
}
handle /rts/* {
import reverse_proxy 8091
}
redir /supervisor /supervisor/
handle_path /supervisor/* {
import reverse_proxy 9001
}
${isRateLimitingEnabled ? `rate_limit {
zone dynamic_zone {
# This key is designed to work irrespective of any load balancers running on the Appsmith container.
# We use "+" as the separator here since we don't expect it in any of the placeholder values here, and has no
# significance in header value syntax.
key {header.Forwarded}+{header.X-Forwarded-For}+{remote_host}
events ${RATE_LIMIT}
window 1s
}
}`: ""}
handle_errors {
respond "{err.status_code} {err.status_text}" {err.status_code}
header {
# Remove the Server header from the response.
-Server
# Remove Cache-Control header from the response.
-Cache-Control
}
}
}
# We bind to http on 80, so that localhost requests don't get redirected to https.
:${process.env.PORT || 80} {
import all-config
}
`)
if (CUSTOM_DOMAIN !== "") {
if (certLocation) {
// There's a custom certificate, don't bind to any exact domain.
parts.push(`
https:// {
import all-config
tls ${certLocation}/fullchain.pem ${certLocation}/privkey.pem
}
`)
} else {
// No custom certificate, bind to the custom domain explicitly, so Caddy can auto-provision the cert.
parts.push(`
https://${CUSTOM_DOMAIN} {
import all-config
}
`)
}
// We have to own the http-to-https redirect, since we need to remove the `Server` header from the response.
parts.push(`
http://${CUSTOM_DOMAIN} {
redir https://{host}{uri}
header -Server
header Connection close
}
`)
}
if (!process.argv.includes("--no-finalize-index-html")) {
finalizeHtmlFiles()
}
fs.mkdirSync(dirname(CaddyfilePath), { recursive: true })
fs.writeFileSync(CaddyfilePath, parts.join("\n"))
spawnSync(AppsmithCaddy, ["fmt", "--overwrite", CaddyfilePath])
spawnSync(AppsmithCaddy, ["reload", "--config", CaddyfilePath])
function finalizeHtmlFiles() {
let info = null;
try {
info = JSON.parse(fs.readFileSync("/opt/appsmith/info.json", "utf8"))
} catch(e) {
// info will be empty, that's okay.
console.error("Error reading info.json", e.message)
}
const extraEnv = {
APPSMITH_VERSION_ID: info?.version ?? "",
APPSMITH_VERSION_SHA: info?.commitSha ?? "",
APPSMITH_VERSION_RELEASE_DATE: info?.imageBuiltAt ?? "",
APPSMITH_HOSTNAME: process.env.HOSTNAME ?? "appsmith-0"
}
for (const file of ["index.html", "404.html"]) {
const content = fs.readFileSync("/opt/appsmith/editor/" + file, "utf8").replaceAll(
/\{\{env\s+"(APPSMITH_[A-Z0-9_]+)"}}/g,
(_, name) => (process.env[name] || extraEnv[name] || "")
)
fs.writeFileSync(process.env.WWW_PATH + "/" + file, content)
}
}
function isCertExpired(path) {
const cert = new X509Certificate(fs.readFileSync(path, "utf-8"))
console.log(path, cert)
return new Date(cert.validTo) < new Date()
}