percher 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/percher.js +111678 -18547
- package/dist/package.json +13 -4
- package/dist/src/cli-utils.d.ts +11 -0
- package/dist/src/cli-utils.d.ts.map +1 -1
- package/dist/src/cli-utils.js +34 -0
- package/dist/src/cli-utils.js.map +1 -1
- package/dist/src/commands/account.d.ts.map +1 -1
- package/dist/src/commands/account.js +63 -7
- package/dist/src/commands/account.js.map +1 -1
- package/dist/src/commands/admin.d.ts +17 -0
- package/dist/src/commands/admin.d.ts.map +1 -0
- package/dist/src/commands/admin.js +92 -0
- package/dist/src/commands/admin.js.map +1 -0
- package/dist/src/commands/apps.d.ts.map +1 -1
- package/dist/src/commands/apps.js +66 -8
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/deploys.d.ts.map +1 -1
- package/dist/src/commands/deploys.js +12 -2
- package/dist/src/commands/deploys.js.map +1 -1
- package/dist/src/commands/export.d.ts +4 -0
- package/dist/src/commands/export.d.ts.map +1 -0
- package/dist/src/commands/export.js +18 -0
- package/dist/src/commands/export.js.map +1 -0
- package/dist/src/commands/migrate.d.ts +4 -0
- package/dist/src/commands/migrate.d.ts.map +1 -0
- package/dist/src/commands/migrate.js +142 -0
- package/dist/src/commands/migrate.js.map +1 -0
- package/dist/src/commands/sharing.d.ts +23 -0
- package/dist/src/commands/sharing.d.ts.map +1 -0
- package/dist/src/commands/sharing.js +162 -0
- package/dist/src/commands/sharing.js.map +1 -0
- package/dist/src/commands/transfers.d.ts +20 -0
- package/dist/src/commands/transfers.d.ts.map +1 -0
- package/dist/src/commands/transfers.js +117 -0
- package/dist/src/commands/transfers.js.map +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +44 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/output.d.ts.map +1 -1
- package/dist/src/output.js +18 -1
- package/dist/src/output.js.map +1 -1
- package/dist/templates/astro/Caddyfile +6 -0
- package/dist/templates/astro/Dockerfile +34 -0
- package/dist/templates/astro/astro.config.mjs +3 -0
- package/dist/templates/astro/package.json +14 -0
- package/dist/templates/astro/percher.toml +11 -0
- package/dist/templates/astro/src/pages/index.astro +15 -0
- package/dist/templates/bun/index.ts +23 -0
- package/dist/templates/bun/package.json +8 -0
- package/dist/templates/bun/percher.toml +10 -0
- package/dist/templates/django/Procfile +1 -0
- package/dist/templates/django/app/__init__.py +0 -0
- package/dist/templates/django/app/settings.py +44 -0
- package/dist/templates/django/app/urls.py +21 -0
- package/dist/templates/django/app/wsgi.py +5 -0
- package/dist/templates/django/manage.py +19 -0
- package/dist/templates/django/percher.toml +11 -0
- package/dist/templates/django/requirements.txt +2 -0
- package/dist/templates/dotnet/Dockerfile +18 -0
- package/dist/templates/dotnet/Program.cs +23 -0
- package/dist/templates/dotnet/app.csproj +9 -0
- package/dist/templates/dotnet/percher.toml +11 -0
- package/dist/templates/eleventy/.eleventy.js +12 -0
- package/dist/templates/eleventy/Caddyfile +6 -0
- package/dist/templates/eleventy/Dockerfile +34 -0
- package/dist/templates/eleventy/package.json +12 -0
- package/dist/templates/eleventy/percher.toml +11 -0
- package/dist/templates/eleventy/src/_includes/base.njk +12 -0
- package/dist/templates/eleventy/src/assets/style.css +12 -0
- package/dist/templates/eleventy/src/index.md +10 -0
- package/dist/templates/elysia/index.ts +8 -0
- package/dist/templates/elysia/package.json +12 -0
- package/dist/templates/elysia/percher.toml +11 -0
- package/dist/templates/express/index.ts +16 -0
- package/dist/templates/express/package.json +11 -0
- package/dist/templates/express/percher.toml +11 -0
- package/dist/templates/fastify/index.ts +13 -0
- package/dist/templates/fastify/package.json +11 -0
- package/dist/templates/fastify/percher.toml +11 -0
- package/dist/templates/flask/main.py +23 -0
- package/dist/templates/flask/percher.toml +11 -0
- package/dist/templates/flask/requirements.txt +1 -0
- package/dist/templates/go/Dockerfile +11 -0
- package/dist/templates/go/go.mod +3 -0
- package/dist/templates/go/main.go +53 -0
- package/dist/templates/go/percher.toml +11 -0
- package/dist/templates/hono/index.ts +12 -0
- package/dist/templates/hono/package.json +11 -0
- package/dist/templates/hono/percher.toml +11 -0
- package/dist/templates/htmx/index.ts +32 -0
- package/dist/templates/htmx/package.json +15 -0
- package/dist/templates/htmx/percher.toml +11 -0
- package/dist/templates/hugo/Caddyfile +6 -0
- package/dist/templates/hugo/Dockerfile +32 -0
- package/dist/templates/hugo/content/_index.md +7 -0
- package/dist/templates/hugo/hugo.toml +11 -0
- package/dist/templates/hugo/layouts/index.html +22 -0
- package/dist/templates/hugo/percher.toml +11 -0
- package/dist/templates/java/Dockerfile +15 -0
- package/dist/templates/java/Main.java +77 -0
- package/dist/templates/java/percher.toml +11 -0
- package/dist/templates/laravel/Dockerfile +15 -0
- package/dist/templates/laravel/composer.json +15 -0
- package/dist/templates/laravel/percher.toml +11 -0
- package/dist/templates/laravel/public/index.php +37 -0
- package/dist/templates/nextjs/app/api/health/route.ts +3 -0
- package/dist/templates/nextjs/app/layout.tsx +11 -0
- package/dist/templates/nextjs/app/page.tsx +10 -0
- package/dist/templates/nextjs/next.config.ts +5 -0
- package/dist/templates/nextjs/package.json +15 -0
- package/dist/templates/nextjs/percher.toml +15 -0
- package/dist/templates/node/index.js +26 -0
- package/dist/templates/node/index.ts +12 -0
- package/dist/templates/node/package.json +8 -0
- package/dist/templates/node/percher.toml +10 -0
- package/dist/templates/nuxt/app.vue +6 -0
- package/dist/templates/nuxt/nuxt.config.ts +4 -0
- package/dist/templates/nuxt/package.json +15 -0
- package/dist/templates/nuxt/percher.toml +15 -0
- package/dist/templates/nuxt/server/api/health.ts +1 -0
- package/dist/templates/phoenix/Dockerfile +15 -0
- package/dist/templates/phoenix/lib/application.ex +15 -0
- package/dist/templates/phoenix/lib/router.ex +39 -0
- package/dist/templates/phoenix/mix.exs +30 -0
- package/dist/templates/phoenix/percher.toml +11 -0
- package/dist/templates/preact/Caddyfile +7 -0
- package/dist/templates/preact/Dockerfile +34 -0
- package/dist/templates/preact/index.html +12 -0
- package/dist/templates/preact/package.json +19 -0
- package/dist/templates/preact/percher.toml +11 -0
- package/dist/templates/preact/src/main.tsx +13 -0
- package/dist/templates/preact/vite.config.ts +6 -0
- package/dist/templates/python/main.py +13 -0
- package/dist/templates/python/percher.toml +11 -0
- package/dist/templates/python/requirements.txt +2 -0
- package/dist/templates/rails/Dockerfile +11 -0
- package/dist/templates/rails/Gemfile +12 -0
- package/dist/templates/rails/config.ru +32 -0
- package/dist/templates/rails/percher.toml +11 -0
- package/dist/templates/remix/app/root.tsx +19 -0
- package/dist/templates/remix/app/routes/_index.tsx +10 -0
- package/dist/templates/remix/app/routes/health.tsx +3 -0
- package/dist/templates/remix/package.json +30 -0
- package/dist/templates/remix/percher.toml +15 -0
- package/dist/templates/remix/vite.config.ts +7 -0
- package/dist/templates/rust/Cargo.toml +19 -0
- package/dist/templates/rust/Dockerfile +14 -0
- package/dist/templates/rust/percher.toml +11 -0
- package/dist/templates/rust/src/main.rs +45 -0
- package/dist/templates/solid/Caddyfile +7 -0
- package/dist/templates/solid/Dockerfile +34 -0
- package/dist/templates/solid/index.html +12 -0
- package/dist/templates/solid/package.json +19 -0
- package/dist/templates/solid/percher.toml +11 -0
- package/dist/templates/solid/src/main.tsx +13 -0
- package/dist/templates/solid/vite.config.ts +6 -0
- package/dist/templates/static/index.html +15 -0
- package/dist/templates/static/percher.toml +6 -0
- package/dist/templates/static/style.css +32 -0
- package/dist/templates/sveltekit/package.json +19 -0
- package/dist/templates/sveltekit/percher.toml +15 -0
- package/dist/templates/sveltekit/src/app.html +12 -0
- package/dist/templates/sveltekit/src/routes/+page.svelte +2 -0
- package/dist/templates/sveltekit/src/routes/health/+server.ts +5 -0
- package/dist/templates/sveltekit/svelte.config.js +8 -0
- package/dist/templates/sveltekit/vite.config.js +6 -0
- package/dist/templates/vite-react/Caddyfile +7 -0
- package/dist/templates/vite-react/Dockerfile +34 -0
- package/dist/templates/vite-react/index.html +12 -0
- package/dist/templates/vite-react/package.json +22 -0
- package/dist/templates/vite-react/percher.toml +11 -0
- package/dist/templates/vite-react/src/main.tsx +20 -0
- package/dist/templates/vite-react/vite.config.ts +6 -0
- package/dist/templates/vite-vue/Caddyfile +7 -0
- package/dist/templates/vite-vue/Dockerfile +34 -0
- package/dist/templates/vite-vue/index.html +12 -0
- package/dist/templates/vite-vue/package.json +19 -0
- package/dist/templates/vite-vue/percher.toml +11 -0
- package/dist/templates/vite-vue/src/main.ts +12 -0
- package/dist/templates/vite-vue/vite.config.ts +6 -0
- package/package.json +13 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Multi-stage build: a Bun stage compiles the static bundle, then a
|
|
2
|
+
# minimal Caddy image serves it. This project uses runtime = "docker"
|
|
3
|
+
# (not "static") because Percher's static runtime serves pre-built
|
|
4
|
+
# files only — it does not run a build command. Generated by Percher;
|
|
5
|
+
# safe to edit and own.
|
|
6
|
+
|
|
7
|
+
# --- Build stage: install deps and build the static bundle -----------
|
|
8
|
+
FROM oven/bun:1-alpine AS build
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN bun install && bun run build
|
|
12
|
+
|
|
13
|
+
# --- Serve stage: minimal Caddy image --------------------------------
|
|
14
|
+
# caddy:2-alpine ships /usr/bin/caddy with cap_net_bind_service file
|
|
15
|
+
# caps. Percher runs every container with --security-opt
|
|
16
|
+
# no-new-privileges, which makes execve refuse a cap-bearing binary
|
|
17
|
+
# (an "operation not permitted" crash-loop). Copy Caddy onto a plain
|
|
18
|
+
# alpine base and strip the caps with setcap -r — Caddy never needs
|
|
19
|
+
# the cap here, it listens on :8080 (>= 1024).
|
|
20
|
+
FROM caddy:2-alpine AS caddy-source
|
|
21
|
+
FROM alpine:3.21
|
|
22
|
+
COPY --from=caddy-source /usr/bin/caddy /usr/bin/caddy
|
|
23
|
+
RUN apk add --no-cache libcap \
|
|
24
|
+
&& setcap -r /usr/bin/caddy \
|
|
25
|
+
&& apk del libcap \
|
|
26
|
+
&& rm -rf /var/cache/apk/*
|
|
27
|
+
COPY Caddyfile /etc/caddy/Caddyfile
|
|
28
|
+
COPY --from=build /app/_site /srv
|
|
29
|
+
# HOME points at the writable tmpfs so non-root Caddy can persist its
|
|
30
|
+
# XDG state (storage-clean lock, config autosave) under Percher's
|
|
31
|
+
# read-only root filesystem.
|
|
32
|
+
ENV HOME=/tmp
|
|
33
|
+
EXPOSE 8080
|
|
34
|
+
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{ title or "__APP_NAME__" }}</title>
|
|
7
|
+
<link rel="stylesheet" href="/assets/style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>{{ content | safe }}</main>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
|
|
3
|
+
const app = express();
|
|
4
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
5
|
+
|
|
6
|
+
app.get("/health", (_req, res) => {
|
|
7
|
+
res.json({ status: "ok" });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
app.get("/", (_req, res) => {
|
|
11
|
+
res.send("Hello from __APP_NAME__!");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.listen(port, () => {
|
|
15
|
+
console.log(`Listening on http://localhost:${port}`);
|
|
16
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
|
|
3
|
+
const app = Fastify({ logger: true });
|
|
4
|
+
|
|
5
|
+
app.get("/health", async () => ({ status: "ok" }));
|
|
6
|
+
|
|
7
|
+
app.get("/", async () => ({ hello: "__APP_NAME__" }));
|
|
8
|
+
|
|
9
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
10
|
+
app.listen({ port, host: "0.0.0.0" }).catch((err) => {
|
|
11
|
+
app.log.error(err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from flask import Flask, jsonify
|
|
3
|
+
|
|
4
|
+
app = Flask(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@app.route("/health")
|
|
8
|
+
def health():
|
|
9
|
+
return jsonify({"status": "ok"})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.route("/")
|
|
13
|
+
def index():
|
|
14
|
+
return f"Hello from __APP_NAME__!"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
port = int(os.environ.get("PORT", 5000))
|
|
19
|
+
# 0.0.0.0 is required so Flask binds inside the Percher container and
|
|
20
|
+
# Caddy can route to it. Public exposure is gated by Caddy + the
|
|
21
|
+
# platform's egress rules, not by the bind address.
|
|
22
|
+
# nosemgrep: python.flask.security.audit.app-run-param-config.avoid_app_run_with_bad_host
|
|
23
|
+
app.run(host="0.0.0.0", port=port)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flask>=3.0.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM golang:1.23-alpine AS build
|
|
2
|
+
WORKDIR /src
|
|
3
|
+
COPY go.mod ./
|
|
4
|
+
COPY main.go ./
|
|
5
|
+
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app .
|
|
6
|
+
|
|
7
|
+
FROM gcr.io/distroless/static-debian12:nonroot
|
|
8
|
+
COPY --from=build /app /app
|
|
9
|
+
EXPOSE 8080
|
|
10
|
+
USER nonroot
|
|
11
|
+
ENTRYPOINT ["/app"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"log"
|
|
6
|
+
"net/http"
|
|
7
|
+
"os"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func main() {
|
|
11
|
+
port := os.Getenv("PORT")
|
|
12
|
+
if port == "" {
|
|
13
|
+
port = "8080"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
http.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
|
17
|
+
w.Header().Set("Content-Type", "application/json")
|
|
18
|
+
_, _ = fmt.Fprint(w, `{"ok":true}`)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
22
|
+
if r.URL.Path != "/" {
|
|
23
|
+
http.NotFound(w, r)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
27
|
+
_, _ = fmt.Fprint(w, `<!doctype html>
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8" />
|
|
31
|
+
<title>__APP_NAME__</title>
|
|
32
|
+
<style>
|
|
33
|
+
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 48px auto; padding: 0 24px; line-height: 1.6; }
|
|
34
|
+
h1 { letter-spacing: -0.5px; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<h1>__APP_NAME__</h1>
|
|
39
|
+
<p>A Go app deployed on Percher.</p>
|
|
40
|
+
<p>Add handlers in <code>main.go</code> and push to redeploy.</p>
|
|
41
|
+
</body>
|
|
42
|
+
</html>`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
log.Printf("listening on :%s", port)
|
|
46
|
+
// Plain HTTP is correct here: Percher's Caddy ingress terminates
|
|
47
|
+
// TLS in front of the container. The app speaks HTTP on its
|
|
48
|
+
// internal port and Caddy proxies HTTPS traffic to it.
|
|
49
|
+
// nosemgrep: go.lang.security.audit.net.use-tls.use-tls
|
|
50
|
+
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
|
51
|
+
log.Fatal(err)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
|
|
3
|
+
const app = new Hono();
|
|
4
|
+
|
|
5
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
6
|
+
|
|
7
|
+
app.get("/", (c) => c.text("Hello from __APP_NAME__!"));
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
port: Number(process.env.PORT ?? 3000),
|
|
11
|
+
fetch: app.fetch,
|
|
12
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
|
|
3
|
+
const app = express();
|
|
4
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
5
|
+
|
|
6
|
+
app.get("/health", (_req, res) => res.json({ status: "ok" }));
|
|
7
|
+
|
|
8
|
+
app.get("/", (_req, res) => {
|
|
9
|
+
res.type("html").send(`<!doctype html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="UTF-8" />
|
|
13
|
+
<title>__APP_NAME__</title>
|
|
14
|
+
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
|
|
15
|
+
</head>
|
|
16
|
+
<body style="font-family: system-ui; padding: 40px;">
|
|
17
|
+
<h1>Hello from __APP_NAME__</h1>
|
|
18
|
+
<p>Express + HTMX on Percher. No build step, just server-rendered HTML fragments.</p>
|
|
19
|
+
<button hx-get="/click" hx-swap="outerHTML" style="font-size: 16px; padding: 10px 20px;">
|
|
20
|
+
Click me
|
|
21
|
+
</button>
|
|
22
|
+
</body>
|
|
23
|
+
</html>`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.get("/click", (_req, res) => {
|
|
27
|
+
res
|
|
28
|
+
.type("html")
|
|
29
|
+
.send(`<p><strong>Hello back.</strong> Swapped at ${new Date().toISOString()}.</p>`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.listen(port, () => console.log(`__APP_NAME__ listening on :${port}`));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "bun run index.ts"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"express": "^4.19.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/express": "^4.17.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Multi-stage build: Hugo compiles the site, then a minimal Caddy
|
|
2
|
+
# image serves it. runtime = "docker" because Percher's static runtime
|
|
3
|
+
# serves pre-built files only — it does not run Hugo. Edit and own freely.
|
|
4
|
+
|
|
5
|
+
# --- Build stage: compile the Hugo site ------------------------------
|
|
6
|
+
FROM hugomods/hugo:exts-0.139.4 AS build
|
|
7
|
+
WORKDIR /site
|
|
8
|
+
COPY . .
|
|
9
|
+
RUN hugo --minify
|
|
10
|
+
|
|
11
|
+
# --- Serve stage: minimal Caddy image --------------------------------
|
|
12
|
+
# caddy:2-alpine ships /usr/bin/caddy with cap_net_bind_service file
|
|
13
|
+
# caps. Percher runs every container with --security-opt
|
|
14
|
+
# no-new-privileges, which makes execve refuse a cap-bearing binary
|
|
15
|
+
# (an "operation not permitted" crash-loop). Copy Caddy onto a plain
|
|
16
|
+
# alpine base and strip the caps with setcap -r — Caddy never needs
|
|
17
|
+
# the cap here, it listens on :8080 (>= 1024).
|
|
18
|
+
FROM caddy:2-alpine AS caddy-source
|
|
19
|
+
FROM alpine:3.21
|
|
20
|
+
COPY --from=caddy-source /usr/bin/caddy /usr/bin/caddy
|
|
21
|
+
RUN apk add --no-cache libcap \
|
|
22
|
+
&& setcap -r /usr/bin/caddy \
|
|
23
|
+
&& apk del libcap \
|
|
24
|
+
&& rm -rf /var/cache/apk/*
|
|
25
|
+
COPY Caddyfile /etc/caddy/Caddyfile
|
|
26
|
+
COPY --from=build /site/public /srv
|
|
27
|
+
# HOME points at the writable tmpfs so non-root Caddy can persist its
|
|
28
|
+
# XDG state (storage-clean lock, config autosave) under Percher's
|
|
29
|
+
# read-only root filesystem.
|
|
30
|
+
ENV HOME=/tmp
|
|
31
|
+
EXPOSE 8080
|
|
32
|
+
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{ .Site.Title }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
10
|
+
max-width: 640px;
|
|
11
|
+
margin: 48px auto;
|
|
12
|
+
padding: 0 24px;
|
|
13
|
+
line-height: 1.6;
|
|
14
|
+
color: #222;
|
|
15
|
+
}
|
|
16
|
+
h1 { font-weight: 600; letter-spacing: -0.5px; }
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<main>{{ .Content }}</main>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM eclipse-temurin:21-jdk-alpine AS build
|
|
2
|
+
WORKDIR /src
|
|
3
|
+
COPY Main.java ./
|
|
4
|
+
# -d emits class files under /build so the runtime stage can copy the
|
|
5
|
+
# whole compiled tree in one COPY (picks up Main.class plus the
|
|
6
|
+
# Main$HealthHandler / Main$RootHandler inner-class files).
|
|
7
|
+
RUN mkdir -p /build && javac -d /build Main.java
|
|
8
|
+
|
|
9
|
+
FROM eclipse-temurin:21-jre-alpine
|
|
10
|
+
RUN addgroup -S app && adduser -S app -G app
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
COPY --from=build --chown=app:app /build/ ./
|
|
13
|
+
USER app
|
|
14
|
+
EXPOSE 8080
|
|
15
|
+
ENTRYPOINT ["java", "Main"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import com.sun.net.httpserver.HttpExchange;
|
|
2
|
+
import com.sun.net.httpserver.HttpHandler;
|
|
3
|
+
import com.sun.net.httpserver.HttpServer;
|
|
4
|
+
|
|
5
|
+
import java.io.IOException;
|
|
6
|
+
import java.io.OutputStream;
|
|
7
|
+
import java.net.InetSocketAddress;
|
|
8
|
+
import java.nio.charset.StandardCharsets;
|
|
9
|
+
|
|
10
|
+
public class Main {
|
|
11
|
+
public static void main(String[] args) throws IOException {
|
|
12
|
+
String portEnv = System.getenv("PORT");
|
|
13
|
+
int port = 8080;
|
|
14
|
+
if (portEnv != null && !portEnv.isEmpty()) {
|
|
15
|
+
try {
|
|
16
|
+
port = Integer.parseInt(portEnv);
|
|
17
|
+
} catch (NumberFormatException ignored) {
|
|
18
|
+
// fall back to 8080
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
|
|
23
|
+
server.createContext("/health", new HealthHandler());
|
|
24
|
+
server.createContext("/", new RootHandler());
|
|
25
|
+
server.setExecutor(null);
|
|
26
|
+
System.out.println("listening on :" + port);
|
|
27
|
+
server.start();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static class HealthHandler implements HttpHandler {
|
|
31
|
+
@Override
|
|
32
|
+
public void handle(HttpExchange exchange) throws IOException {
|
|
33
|
+
byte[] body = "{\"ok\":true}".getBytes(StandardCharsets.UTF_8);
|
|
34
|
+
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
|
35
|
+
exchange.sendResponseHeaders(200, body.length);
|
|
36
|
+
try (OutputStream os = exchange.getResponseBody()) {
|
|
37
|
+
os.write(body);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static class RootHandler implements HttpHandler {
|
|
43
|
+
@Override
|
|
44
|
+
public void handle(HttpExchange exchange) throws IOException {
|
|
45
|
+
if (!"/".equals(exchange.getRequestURI().getPath())) {
|
|
46
|
+
byte[] notFound = "Not Found".getBytes(StandardCharsets.UTF_8);
|
|
47
|
+
exchange.sendResponseHeaders(404, notFound.length);
|
|
48
|
+
try (OutputStream os = exchange.getResponseBody()) {
|
|
49
|
+
os.write(notFound);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
String html = "<!doctype html>\n"
|
|
54
|
+
+ "<html lang=\"en\">\n"
|
|
55
|
+
+ " <head>\n"
|
|
56
|
+
+ " <meta charset=\"utf-8\" />\n"
|
|
57
|
+
+ " <title>__APP_NAME__</title>\n"
|
|
58
|
+
+ " <style>\n"
|
|
59
|
+
+ " body { font-family: system-ui, sans-serif; max-width: 640px; margin: 48px auto; padding: 0 24px; line-height: 1.6; }\n"
|
|
60
|
+
+ " h1 { letter-spacing: -0.5px; }\n"
|
|
61
|
+
+ " </style>\n"
|
|
62
|
+
+ " </head>\n"
|
|
63
|
+
+ " <body>\n"
|
|
64
|
+
+ " <h1>__APP_NAME__</h1>\n"
|
|
65
|
+
+ " <p>A Java app deployed on Percher.</p>\n"
|
|
66
|
+
+ " <p>Add handlers in <code>Main.java</code> and push to redeploy.</p>\n"
|
|
67
|
+
+ " </body>\n"
|
|
68
|
+
+ "</html>";
|
|
69
|
+
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
|
70
|
+
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
|
|
71
|
+
exchange.sendResponseHeaders(200, body.length);
|
|
72
|
+
try (OutputStream os = exchange.getResponseBody()) {
|
|
73
|
+
os.write(body);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM composer:2 AS vendor
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY composer.json ./
|
|
4
|
+
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
|
|
5
|
+
|
|
6
|
+
FROM php:8.3-cli-alpine
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
RUN addgroup -S app && adduser -S app -G app
|
|
9
|
+
COPY --from=vendor --chown=app:app /app/vendor ./vendor
|
|
10
|
+
COPY --chown=app:app . .
|
|
11
|
+
RUN composer dump-autoload --no-dev --optimize 2>/dev/null || true \
|
|
12
|
+
&& chown -R app:app /app
|
|
13
|
+
USER app
|
|
14
|
+
EXPOSE 8000
|
|
15
|
+
CMD ["php", "-S", "0.0.0.0:8000", "-t", "public", "public/index.php"]
|