portta 0.8.0
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/cli.js +77013 -0
- package/dist/documentation.json +1 -0
- package/dist/runtime/.env.example +440 -0
- package/dist/runtime/VERSION +1 -0
- package/dist/runtime/config/traefik/dynamic/auth.example.yaml.disabled +24 -0
- package/dist/runtime/config/traefik/dynamic/local-tls.yaml +11 -0
- package/dist/runtime/config/traefik/dynamic/middlewares.yaml +35 -0
- package/dist/runtime/config/traefik/dynamic/portta-aliases.yaml +9 -0
- package/dist/runtime/config/traefik/dynamic/portta-auth.yaml +19 -0
- package/dist/runtime/config/traefik/dynamic/portta-shares.yaml +10 -0
- package/dist/runtime/config/traefik/dynamic/tcp.yaml +23 -0
- package/dist/runtime/docker/compose/attach/host.yaml +24 -0
- package/dist/runtime/docker/compose/attach/tailscale.yaml +70 -0
- package/dist/runtime/docker/compose/compose.yaml +214 -0
- package/dist/runtime/docker/compose/features/auth-build.yaml +19 -0
- package/dist/runtime/docker/compose/features/auth-dev.yaml +20 -0
- package/dist/runtime/docker/compose/features/cloudflare-tunnel.yaml +69 -0
- package/dist/runtime/docker/compose/features/dashboard-tailscale.yaml +21 -0
- package/dist/runtime/docker/compose/features/dashboard.yaml +21 -0
- package/dist/runtime/docker/compose/features/db.yaml +63 -0
- package/dist/runtime/docker/compose/features/panel-domain.yaml +42 -0
- package/dist/runtime/docker/compose/features/panel-public.yaml +47 -0
- package/dist/runtime/docker/compose/features/panel-webhook.yaml +46 -0
- package/dist/runtime/docker/compose/features/tcp-tailscale.yaml +30 -0
- package/dist/runtime/docker/compose/features/tcp.yaml +40 -0
- package/dist/runtime/docker/compose/features/web-bind.yaml +19 -0
- package/dist/runtime/docker/compose/features/web-build.yaml +21 -0
- package/dist/runtime/docker/compose/features/web-dev.yaml +53 -0
- package/dist/runtime/docker/compose/features/web-vpn.yaml +24 -0
- package/dist/runtime/docker/compose/features/web.yaml +283 -0
- package/dist/runtime/docker/compose/profiles/local-tls.yaml +23 -0
- package/dist/runtime/docker/compose/profiles/local.yaml +15 -0
- package/dist/runtime/docker/compose/profiles/public.yaml +19 -0
- package/dist/runtime/docker/compose/profiles/remote-tls-dns.yaml +26 -0
- package/dist/runtime/docker/compose/profiles/remote-tls-http.yaml +32 -0
- package/dist/runtime/docker/compose/profiles/remote-tls.yaml +31 -0
- package/dist/runtime/docker/compose/profiles/remote.yaml +23 -0
- package/dist/runtime/docker/images/README.md +12 -0
- package/dist/runtime/docker/images/apply/Dockerfile +45 -0
- package/dist/runtime/docker/images/toolbox/Dockerfile +38 -0
- package/dist/runtime/scripts/lib/runner-exec.sh +189 -0
- package/dist/runtime/templates/README.md +43 -0
- package/dist/runtime/templates/overlays/01-single-web.yaml +23 -0
- package/dist/runtime/templates/overlays/02-web-api.yaml +30 -0
- package/dist/runtime/templates/overlays/03-web-api-postgres.yaml +33 -0
- package/dist/runtime/templates/overlays/04-web-api-postgres-redis.yaml +30 -0
- package/dist/runtime/templates/overlays/05-multiple-apis.yaml +37 -0
- package/dist/runtime/templates/overlays/06-monorepo.yaml +63 -0
- package/dist/runtime/templates/overlays/07-worktree.env +47 -0
- package/dist/runtime/templates/overlays/08-nonstandard-port.yaml +41 -0
- package/dist/runtime/templates/overlays/09-tcp-routing.yaml +70 -0
- package/dist/runtime/templates/overlays/10-mailpit.yaml +24 -0
- package/dist/runtime/templates/overlays/11-rustfs.yaml +25 -0
- package/dist/runtime/templates/project/PORTTA.md +107 -0
- package/package.json +53 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: base stack
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# This file contains ONLY gateway-owned infrastructure. It never references,
|
|
5
|
+
# mounts or manages anything belonging to a consumer project.
|
|
6
|
+
#
|
|
7
|
+
# Do not run this file directly; use the CLI so the right profile overlays and
|
|
8
|
+
# the resolved environment are applied:
|
|
9
|
+
#
|
|
10
|
+
# ./bin/portta up local
|
|
11
|
+
#
|
|
12
|
+
# docker/compose/compose.yaml alone is intentionally incomplete: it declares no Traefik
|
|
13
|
+
# networks or ports, because exactly one attach-* overlay decides those.
|
|
14
|
+
#
|
|
15
|
+
# Traefik's static configuration lives in `environment:` (TRAEFIK_* variables)
|
|
16
|
+
# rather than a config file. Traefik treats file / CLI / env as mutually
|
|
17
|
+
# exclusive sources, and only the env form composes across Compose overlays.
|
|
18
|
+
# Dynamic configuration stays in config/traefik/dynamic/. See docs/development/adr/0003.
|
|
19
|
+
# ============================================================================
|
|
20
|
+
|
|
21
|
+
name: ${PORTTA_PROJECT_NAME:-portta}
|
|
22
|
+
|
|
23
|
+
networks:
|
|
24
|
+
# Shared, external, created by `bootstrap`. Consumer projects attach their
|
|
25
|
+
# published HTTP services here. Its lifecycle is independent of this stack.
|
|
26
|
+
gateway:
|
|
27
|
+
external: true
|
|
28
|
+
name: ${PORTTA_NETWORK:-portta}
|
|
29
|
+
|
|
30
|
+
# Gateway-private control plane. `internal: true` means no route to the
|
|
31
|
+
# outside world: the Docker socket proxy is unreachable from anywhere else.
|
|
32
|
+
control:
|
|
33
|
+
name: ${PORTTA_CONTROL_NETWORK:-portta-control}
|
|
34
|
+
internal: true
|
|
35
|
+
labels:
|
|
36
|
+
portta.managed: "true"
|
|
37
|
+
portta.component: control-network
|
|
38
|
+
|
|
39
|
+
services:
|
|
40
|
+
# --------------------------------------------------------------------------
|
|
41
|
+
# Docker socket proxy: read-only, filtered view of the Docker API
|
|
42
|
+
# --------------------------------------------------------------------------
|
|
43
|
+
# Traefik needs container discovery, not control. This proxy exposes only the
|
|
44
|
+
# endpoints the Docker provider actually calls and rejects every write.
|
|
45
|
+
# It is reachable exclusively from the internal control network.
|
|
46
|
+
socket-proxy:
|
|
47
|
+
image: tecnativa/docker-socket-proxy:v0.5.0
|
|
48
|
+
restart: unless-stopped
|
|
49
|
+
read_only: true
|
|
50
|
+
environment:
|
|
51
|
+
# Endpoints Traefik's Docker provider requires
|
|
52
|
+
CONTAINERS: "1"
|
|
53
|
+
NETWORKS: "1"
|
|
54
|
+
EVENTS: "1"
|
|
55
|
+
PING: "1"
|
|
56
|
+
VERSION: "1"
|
|
57
|
+
# Everything else stays denied
|
|
58
|
+
POST: "0"
|
|
59
|
+
ALLOW_RESTARTS: "0"
|
|
60
|
+
ALLOW_STOP: "0"
|
|
61
|
+
ALLOW_START: "0"
|
|
62
|
+
AUTH: "0"
|
|
63
|
+
BUILD: "0"
|
|
64
|
+
COMMIT: "0"
|
|
65
|
+
CONFIGS: "0"
|
|
66
|
+
DISTRIBUTION: "0"
|
|
67
|
+
EXEC: "0"
|
|
68
|
+
IMAGES: "0"
|
|
69
|
+
INFO: "0"
|
|
70
|
+
NODES: "0"
|
|
71
|
+
PLUGINS: "0"
|
|
72
|
+
SECRETS: "0"
|
|
73
|
+
SERVICES: "0"
|
|
74
|
+
SESSION: "0"
|
|
75
|
+
SWARM: "0"
|
|
76
|
+
SYSTEM: "0"
|
|
77
|
+
TASKS: "0"
|
|
78
|
+
VOLUMES: "0"
|
|
79
|
+
LOG_LEVEL: warning
|
|
80
|
+
volumes:
|
|
81
|
+
# Read-only bind. The proxy is the only component that sees the socket.
|
|
82
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
83
|
+
networks:
|
|
84
|
+
- control
|
|
85
|
+
# Deliberately no `ports:`: the proxy must never be reachable from the host.
|
|
86
|
+
# read_only above hardens the container; the entrypoint still needs to
|
|
87
|
+
# render the generated haproxy.cfg somewhere, hence these tmpfs mounts.
|
|
88
|
+
tmpfs:
|
|
89
|
+
- /tmp
|
|
90
|
+
- /run
|
|
91
|
+
healthcheck:
|
|
92
|
+
test: ["CMD", "nc", "-z", "127.0.0.1", "2375"]
|
|
93
|
+
interval: 10s
|
|
94
|
+
timeout: 3s
|
|
95
|
+
retries: 5
|
|
96
|
+
start_period: 5s
|
|
97
|
+
labels:
|
|
98
|
+
traefik.enable: "false"
|
|
99
|
+
portta.managed: "true"
|
|
100
|
+
portta.component: socket-proxy
|
|
101
|
+
|
|
102
|
+
# --------------------------------------------------------------------------
|
|
103
|
+
# Traefik: HTTP/HTTPS edge router
|
|
104
|
+
# --------------------------------------------------------------------------
|
|
105
|
+
traefik:
|
|
106
|
+
image: traefik:v3.7.12
|
|
107
|
+
restart: unless-stopped
|
|
108
|
+
depends_on:
|
|
109
|
+
socket-proxy:
|
|
110
|
+
condition: service_healthy
|
|
111
|
+
# No `networks:` and no `ports:` here on purpose. How Traefik is attached to
|
|
112
|
+
# the world is a per-profile decision made by exactly one overlay:
|
|
113
|
+
# docker/compose/attach/host.yaml : its own namespace, ports on the host
|
|
114
|
+
# docker/compose/attach/tailscale.yaml : inside the Tailscale container's
|
|
115
|
+
# network namespace, reachable over the
|
|
116
|
+
# tailnet with nothing published
|
|
117
|
+
# `networks:` and `network_mode:` are mutually exclusive and Compose merges
|
|
118
|
+
# rather than replaces both, so neither can be declared here and undone later.
|
|
119
|
+
environment:
|
|
120
|
+
# -- global ----------------------------------------------------------
|
|
121
|
+
TRAEFIK_GLOBAL_CHECKNEWVERSION: "false"
|
|
122
|
+
TRAEFIK_GLOBAL_SENDANONYMOUSUSAGE: "false"
|
|
123
|
+
|
|
124
|
+
# -- observability ---------------------------------------------------
|
|
125
|
+
TRAEFIK_LOG_LEVEL: ${PORTTA_LOG_LEVEL:-INFO}
|
|
126
|
+
TRAEFIK_ACCESSLOG: ${PORTTA_ACCESS_LOG:-false}
|
|
127
|
+
TRAEFIK_PING: "true"
|
|
128
|
+
|
|
129
|
+
# -- api / dashboard -------------------------------------------------
|
|
130
|
+
# Off by default. docker/compose/features/dashboard.yaml turns it on, bound
|
|
131
|
+
# to loopback.
|
|
132
|
+
TRAEFIK_API: "false"
|
|
133
|
+
|
|
134
|
+
# -- entrypoints -----------------------------------------------------
|
|
135
|
+
TRAEFIK_ENTRYPOINTS_WEB_ADDRESS: ":80"
|
|
136
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS: ":443"
|
|
137
|
+
# Headers whose names contain characters outside [A-Za-z0-9-] can alias a
|
|
138
|
+
# canonical header once a backend normalises them (X_Auth_User becoming
|
|
139
|
+
# X-Auth-User in CGI/WSGI/PHP/nginx), which lets a client spoof headers
|
|
140
|
+
# Traefik manages. `keep` is Traefik's default and is fine behind
|
|
141
|
+
# loopback; the public profile raises this to `delete`. Setting it
|
|
142
|
+
# explicitly also silences Traefik's startup advisory.
|
|
143
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_ALIASHEADERSSTRATEGY: ${PORTTA_ALIAS_HEADERS_STRATEGY:-keep}
|
|
144
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ALIASHEADERSSTRATEGY: ${PORTTA_ALIAS_HEADERS_STRATEGY:-keep}
|
|
145
|
+
|
|
146
|
+
# -- docker provider -------------------------------------------------
|
|
147
|
+
TRAEFIK_PROVIDERS_DOCKER: "true"
|
|
148
|
+
TRAEFIK_PROVIDERS_DOCKER_ENDPOINT: tcp://socket-proxy:2375
|
|
149
|
+
# A service is routed only when it opts in with traefik.enable=true.
|
|
150
|
+
TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: "false"
|
|
151
|
+
# Consumer containers sit on several networks; always dial them over the
|
|
152
|
+
# shared gateway network so we never reach into a project's private one.
|
|
153
|
+
TRAEFIK_PROVIDERS_DOCKER_NETWORK: ${PORTTA_NETWORK:-portta}
|
|
154
|
+
# Hostnames are derived from the labels Compose already injects, so a
|
|
155
|
+
# project opts in without repeating its own name. An explicit
|
|
156
|
+
# `traefik.http.routers.<name>.rule` label always wins.
|
|
157
|
+
TRAEFIK_PROVIDERS_DOCKER_DEFAULTRULE: >-
|
|
158
|
+
Host(`{{ if index .Labels "com.docker.compose.project" }}{{ normalize (index .Labels "com.docker.compose.project") }}-{{ normalize (index .Labels "com.docker.compose.service") }}{{ else }}{{ normalize .Name }}{{ end }}.${PORTTA_DOMAIN:-localhost}`)
|
|
159
|
+
|
|
160
|
+
# -- file provider (dynamic configuration) ---------------------------
|
|
161
|
+
TRAEFIK_PROVIDERS_FILE_DIRECTORY: /etc/traefik/dynamic
|
|
162
|
+
TRAEFIK_PROVIDERS_FILE_WATCH: "true"
|
|
163
|
+
volumes:
|
|
164
|
+
- ./config/traefik/dynamic:/etc/traefik/dynamic:ro
|
|
165
|
+
# Local CA material, when `portta tls init` has been run. Empty
|
|
166
|
+
# otherwise; git-ignored either way.
|
|
167
|
+
- ./config/tls:/etc/traefik/tls:ro
|
|
168
|
+
- ./state/traefik/acme:/etc/traefik/acme
|
|
169
|
+
healthcheck:
|
|
170
|
+
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
|
171
|
+
interval: 10s
|
|
172
|
+
timeout: 3s
|
|
173
|
+
retries: 5
|
|
174
|
+
start_period: 5s
|
|
175
|
+
labels:
|
|
176
|
+
traefik.enable: "false"
|
|
177
|
+
portta.managed: "true"
|
|
178
|
+
portta.component: traefik
|
|
179
|
+
|
|
180
|
+
# --------------------------------------------------------------------------
|
|
181
|
+
# ForwardAuth: browser login and host-scoped sessions
|
|
182
|
+
# --------------------------------------------------------------------------
|
|
183
|
+
portta-auth:
|
|
184
|
+
image: ${PORTTA_AUTH_IMAGE:-ghcr.io/fabioassuncao/portta:${PORTTA_VERSION}}
|
|
185
|
+
restart: unless-stopped
|
|
186
|
+
read_only: true
|
|
187
|
+
# The store it reads is owner-only and belongs to whoever installed Portta,
|
|
188
|
+
# which on a VPS is root. Hard-coding `node` (uid 1000) meant the service
|
|
189
|
+
# could not open its own protection store on any Linux host installed as
|
|
190
|
+
# anybody else -- the same reason the panel takes PORTTA_WEB_USER.
|
|
191
|
+
user: ${PORTTA_AUTH_USER:-node}
|
|
192
|
+
command: ["node", "/app/apps/auth/dist/index.js"]
|
|
193
|
+
environment:
|
|
194
|
+
PORTTA_AUTH_PORT: "4180"
|
|
195
|
+
PORTTA_AUTH_STORE: /app/state/auth/protections.json
|
|
196
|
+
PORTTA_AUTH_SECRET: ${PORTTA_AUTH_SECRET:-}
|
|
197
|
+
PORTTA_AUTH_UI_DIR: /app/apps/auth/dist/ui
|
|
198
|
+
PORTTA_AUTH_SESSION_SECONDS: "43200"
|
|
199
|
+
volumes:
|
|
200
|
+
- ./state/auth:/app/state/auth:ro
|
|
201
|
+
networks:
|
|
202
|
+
- gateway
|
|
203
|
+
tmpfs:
|
|
204
|
+
- /tmp
|
|
205
|
+
healthcheck:
|
|
206
|
+
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:4180/health"]
|
|
207
|
+
interval: 10s
|
|
208
|
+
timeout: 3s
|
|
209
|
+
retries: 5
|
|
210
|
+
start_period: 5s
|
|
211
|
+
labels:
|
|
212
|
+
traefik.enable: "false"
|
|
213
|
+
portta.managed: "true"
|
|
214
|
+
portta.component: auth
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: build the auth image locally (checkout only)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied only when PORTTA_WEB_BUILD=true. Development uses auth-dev.yaml;
|
|
5
|
+
# normal local-release and installed runs consume an already-built image.
|
|
6
|
+
#
|
|
7
|
+
# The runtime target is what the long-running service executes.
|
|
8
|
+
# See docs/development/adr/0020-installer-and-portta-home.md.
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
services:
|
|
12
|
+
portta-auth:
|
|
13
|
+
build:
|
|
14
|
+
context: .
|
|
15
|
+
dockerfile: apps/web/Dockerfile
|
|
16
|
+
target: runtime
|
|
17
|
+
args:
|
|
18
|
+
PORTTA_VERSION: ${PORTTA_VERSION:-}
|
|
19
|
+
image: ${PORTTA_AUTH_IMAGE:-fabioassuncao/portta:${PORTTA_VERSION}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Portta ForwardAuth in development mode
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# The dev image supplies Node and dependencies. Source and UI files come from
|
|
5
|
+
# the checkout, so ordinary edits never require another image build.
|
|
6
|
+
# =============================================================================
|
|
7
|
+
|
|
8
|
+
services:
|
|
9
|
+
portta-auth:
|
|
10
|
+
image: fabioassuncao/portta:dev
|
|
11
|
+
working_dir: /app/apps/auth
|
|
12
|
+
command: ["npm", "run", "dev"]
|
|
13
|
+
volumes:
|
|
14
|
+
- ./apps/auth/src:/app/apps/auth/src:ro
|
|
15
|
+
- ./apps/auth/ui:/app/apps/auth/ui:ro
|
|
16
|
+
- ./apps/auth/vite.config.ts:/app/apps/auth/vite.config.ts:ro
|
|
17
|
+
- ./packages/core/src:/app/packages/core/src:ro
|
|
18
|
+
tmpfs:
|
|
19
|
+
- /app/apps/auth/dist
|
|
20
|
+
- /app/apps/auth/node_modules
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: Cloudflare Tunnel connector (opt-in)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Enabled by CLOUDFLARE_TUNNEL_ENABLED=true, after `portta tunnel setup`.
|
|
5
|
+
#
|
|
6
|
+
# What this buys, and what it does not
|
|
7
|
+
# ------------------------------------
|
|
8
|
+
# It makes the gateway reachable over HTTPS from the internet **without opening
|
|
9
|
+
# a port**. The connector dials Cloudflare outbound, so the host needs no public
|
|
10
|
+
# address, no forwarded 80/443, and nothing on its firewall — which is the only
|
|
11
|
+
# way a home lab or a machine behind CGNAT can publish anything at all.
|
|
12
|
+
#
|
|
13
|
+
# It does not decide what is published. The connector carries one wildcard rule
|
|
14
|
+
# to Traefik, and Traefik still routes only the services that opted in with
|
|
15
|
+
# `traefik.enable=true`. Starting the connector publishes nothing on its own.
|
|
16
|
+
#
|
|
17
|
+
# Why a container, not a systemd unit
|
|
18
|
+
# -----------------------------------
|
|
19
|
+
# Everything else the gateway runs is a container, updated by pulling an image
|
|
20
|
+
# and pinned to a version ([ADR 0004](../../../docs/development/adr/0004-pinned-versions.md)).
|
|
21
|
+
# A connector installed on the host would be the one component with a different
|
|
22
|
+
# lifecycle, a different update path and a different uninstall. An operator who
|
|
23
|
+
# already runs cloudflared under systemd keeps it: Portta detects that connector
|
|
24
|
+
# and leaves this overlay off. See docs/development/adr/0025-cloudflare-tunnel.md.
|
|
25
|
+
# ============================================================================
|
|
26
|
+
|
|
27
|
+
services:
|
|
28
|
+
cloudflared:
|
|
29
|
+
image: ${PORTTA_CLOUDFLARED_IMAGE:-cloudflare/cloudflared:2026.8.3}
|
|
30
|
+
restart: unless-stopped
|
|
31
|
+
# `tunnel run` with a config file, rather than a token on the command line:
|
|
32
|
+
# a token is a credential, and a credential on a command line is visible in
|
|
33
|
+
# `ps` and in every process listing on the host.
|
|
34
|
+
command: ["tunnel", "--no-autoupdate", "--config", "/etc/cloudflared/config.yml", "run"]
|
|
35
|
+
networks:
|
|
36
|
+
# The shared gateway network, and only that: the connector reaches Traefik
|
|
37
|
+
# by name and has no route into any project's private network.
|
|
38
|
+
#
|
|
39
|
+
# Under the Tailscale attachment Traefik has no name of its own here — it
|
|
40
|
+
# runs inside the tailscale container's namespace — so the generated
|
|
41
|
+
# config points at that container instead. `portta tunnel setup` picks the
|
|
42
|
+
# right origin; see docs/product/guides/cloudflare-tunnel.md.
|
|
43
|
+
- gateway
|
|
44
|
+
volumes:
|
|
45
|
+
# Generated by `portta tunnel setup`; the credentials file beside it is
|
|
46
|
+
# written 0600 and is the tunnel's private key.
|
|
47
|
+
- ./state/cloudflared:/etc/cloudflared:ro
|
|
48
|
+
depends_on:
|
|
49
|
+
traefik:
|
|
50
|
+
condition: service_healthy
|
|
51
|
+
healthcheck:
|
|
52
|
+
# The connector serves its own metrics; `ready` reports 200 only while it
|
|
53
|
+
# holds registered connections to the edge, so an unhealthy container here
|
|
54
|
+
# means the tunnel is genuinely down rather than merely starting.
|
|
55
|
+
test: ["CMD", "cloudflared", "--metrics", "127.0.0.1:2000", "tunnel", "ready"]
|
|
56
|
+
interval: 15s
|
|
57
|
+
timeout: 5s
|
|
58
|
+
retries: 4
|
|
59
|
+
start_period: 20s
|
|
60
|
+
security_opt:
|
|
61
|
+
- no-new-privileges:true
|
|
62
|
+
cap_drop:
|
|
63
|
+
- ALL
|
|
64
|
+
labels:
|
|
65
|
+
# Never routed by Traefik: the connector is what traffic arrives through,
|
|
66
|
+
# not something traffic is sent to.
|
|
67
|
+
traefik.enable: "false"
|
|
68
|
+
portta.managed: "true"
|
|
69
|
+
portta.component: cloudflared
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Traefik dashboard overlay for the Tailscale attachment
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Same as docker/compose/features/dashboard.yaml, but the port is published by the
|
|
5
|
+
# Tailscale container, because Traefik has no network namespace of its own here.
|
|
6
|
+
#
|
|
7
|
+
# Note that the dashboard also becomes reachable over the tailnet at
|
|
8
|
+
# http://<tailscale-hostname>:8080 without being published at all. Restrict it
|
|
9
|
+
# with a Tailscale ACL, or leave PORTTA_DASHBOARD=false.
|
|
10
|
+
# ============================================================================
|
|
11
|
+
|
|
12
|
+
services:
|
|
13
|
+
tailscale:
|
|
14
|
+
ports:
|
|
15
|
+
- "${PORTTA_DASHBOARD_BIND_ADDRESS:-127.0.0.1}:${PORTTA_DASHBOARD_PORT:-8080}:8080"
|
|
16
|
+
|
|
17
|
+
traefik:
|
|
18
|
+
environment:
|
|
19
|
+
TRAEFIK_API: "true"
|
|
20
|
+
TRAEFIK_API_DASHBOARD: "true"
|
|
21
|
+
TRAEFIK_API_INSECURE: "true"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: Traefik dashboard overlay (opt-in, loopback only)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Enabled by PORTTA_DASHBOARD=true. The dashboard listens on Traefik's
|
|
5
|
+
# internal entrypoint and is published on a dedicated host port bound to
|
|
6
|
+
# PORTTA_DASHBOARD_BIND_ADDRESS (127.0.0.1 by default).
|
|
7
|
+
#
|
|
8
|
+
# It is never attached to the web/websecure entrypoints, so it can never be
|
|
9
|
+
# reached through the public wildcard domain. See docs/product/concepts/security.md.
|
|
10
|
+
# ============================================================================
|
|
11
|
+
|
|
12
|
+
services:
|
|
13
|
+
traefik:
|
|
14
|
+
ports:
|
|
15
|
+
- "${PORTTA_DASHBOARD_BIND_ADDRESS:-127.0.0.1}:${PORTTA_DASHBOARD_PORT:-8080}:8080"
|
|
16
|
+
environment:
|
|
17
|
+
TRAEFIK_API: "true"
|
|
18
|
+
TRAEFIK_API_DASHBOARD: "true"
|
|
19
|
+
# Serves the dashboard on the built-in `traefik` entrypoint (:8080).
|
|
20
|
+
# Safe only because the published port is loopback-bound above.
|
|
21
|
+
TRAEFIK_API_INSECURE: "true"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: panel persistence (always, with the panel)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# PostgreSQL is a private implementation detail of the panel, and a required
|
|
5
|
+
# one: the panel refuses to start without it, so this overlay is selected
|
|
6
|
+
# wherever web.yaml is (packages/core/src/config.ts). It has no host port, never
|
|
7
|
+
# joins the shared HTTP network, and keeps its data in a named volume that
|
|
8
|
+
# `portta down` does not remove — only `portta reset` does.
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
networks:
|
|
12
|
+
webdata:
|
|
13
|
+
name: ${PORTTA_DB_NETWORK:-portta-data}
|
|
14
|
+
internal: true
|
|
15
|
+
labels:
|
|
16
|
+
portta.managed: "true"
|
|
17
|
+
portta.component: data-network
|
|
18
|
+
|
|
19
|
+
volumes:
|
|
20
|
+
portta-db:
|
|
21
|
+
name: ${PORTTA_DB_VOLUME:-portta-db}
|
|
22
|
+
labels:
|
|
23
|
+
portta.managed: "true"
|
|
24
|
+
portta.component: db-volume
|
|
25
|
+
|
|
26
|
+
services:
|
|
27
|
+
db:
|
|
28
|
+
image: postgres:18.6-alpine
|
|
29
|
+
restart: unless-stopped
|
|
30
|
+
environment:
|
|
31
|
+
POSTGRES_DB: ${PORTTA_RUNTIME_DB_NAME:-portta}
|
|
32
|
+
POSTGRES_USER: ${PORTTA_RUNTIME_DB_USER:-portta}
|
|
33
|
+
# An empty value makes PostgreSQL refuse to initialise; it never makes it
|
|
34
|
+
# trust unauthenticated clients. bootstrap/web up normally fill this.
|
|
35
|
+
POSTGRES_PASSWORD: ${PORTTA_RUNTIME_DB_PASSWORD:-}
|
|
36
|
+
volumes:
|
|
37
|
+
# PostgreSQL 18+ stores data in a major-version subdirectory. Mount the
|
|
38
|
+
# parent so upgrades fail explicitly instead of creating nested data.
|
|
39
|
+
- portta-db:/var/lib/postgresql
|
|
40
|
+
networks:
|
|
41
|
+
- webdata
|
|
42
|
+
healthcheck:
|
|
43
|
+
test: ["CMD-SHELL", 'pg_isready -h 127.0.0.1 -p 5432 -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
|
|
44
|
+
interval: 5s
|
|
45
|
+
timeout: 3s
|
|
46
|
+
retries: 12
|
|
47
|
+
start_period: 5s
|
|
48
|
+
labels:
|
|
49
|
+
traefik.enable: "false"
|
|
50
|
+
portta.managed: "true"
|
|
51
|
+
portta.component: db
|
|
52
|
+
# No `ports:`. Only the panel can reach this service.
|
|
53
|
+
|
|
54
|
+
web:
|
|
55
|
+
environment:
|
|
56
|
+
PORTTA_DB_NETWORK: ${PORTTA_DB_NETWORK:-portta-data}
|
|
57
|
+
networks:
|
|
58
|
+
- webdata
|
|
59
|
+
# The panel exits if Postgres is not accepting connections yet. Compose
|
|
60
|
+
# merges this with the socket-proxy wait in web.yaml.
|
|
61
|
+
depends_on:
|
|
62
|
+
db:
|
|
63
|
+
condition: service_healthy
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: the panel on the domain, over HTTPS, behind Portta ForwardAuth
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied when PORTTA_WEB_EXPOSE=domain. The panel answers on one hostname of
|
|
5
|
+
# the gateway's own domain, on the entrypoint every application uses, with the
|
|
6
|
+
# certificate that entrypoint already terminates.
|
|
7
|
+
#
|
|
8
|
+
# Why this is allowed on a public profile when `vpn` is not
|
|
9
|
+
# ---------------------------------------------------------
|
|
10
|
+
# `web-vpn.yaml` is refused on remote-public because it would publish a VPN
|
|
11
|
+
# hostname on every interface. The `public` mode publishes the panel on
|
|
12
|
+
# 0.0.0.0, and what stands in front of it is the panel itself -- accounts,
|
|
13
|
+
# roles, sessions, a second factor and a rate limiter, all of them its own
|
|
14
|
+
# (docs/development/adr/0035-authentication-lives-in-the-panel.md).
|
|
15
|
+
#
|
|
16
|
+
# So the remaining difference between `public` and this is not exposure. It is
|
|
17
|
+
# which entrypoint carries the panel, and what that costs:
|
|
18
|
+
#
|
|
19
|
+
# `public` keeps the panel on its own entrypoint, so it needs no hostname
|
|
20
|
+
# and works on a bare IP -- but that entrypoint terminates no TLS,
|
|
21
|
+
# so the password crosses the network in clear text.
|
|
22
|
+
# `domain` puts the panel on websecure, where it gets a real certificate and
|
|
23
|
+
# the same HTTPS every application gets. It needs a hostname, and
|
|
24
|
+
# it gives up the property that the panel's entrypoint carries
|
|
25
|
+
# nothing else. It carries no application the router does not name.
|
|
26
|
+
#
|
|
27
|
+
# The router names exactly one host, so publishing the panel still publishes no
|
|
28
|
+
# application: an application is reachable only through a router of its own.
|
|
29
|
+
#
|
|
30
|
+
# There is no middleware: the panel signs people in itself, and `portta web up
|
|
31
|
+
# --expose domain` refuses unless PORTTA_AUTH_MODE=required and TLS is on.
|
|
32
|
+
# See docs/development/adr/0035-authentication-lives-in-the-panel.md.
|
|
33
|
+
# ============================================================================
|
|
34
|
+
|
|
35
|
+
services:
|
|
36
|
+
web:
|
|
37
|
+
labels:
|
|
38
|
+
# Overrides the `false` in web.yaml: a later overlay wins for map labels.
|
|
39
|
+
traefik.enable: "true"
|
|
40
|
+
traefik.http.routers.portta-panel.rule: >-
|
|
41
|
+
Host(`${PORTTA_PANEL_ADVERTISED_HOST:-portta-web}`)
|
|
42
|
+
traefik.http.services.portta-panel.loadbalancer.server.port: "8081"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: the panel on a public address, behind Portta ForwardAuth
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied when PORTTA_WEB_EXPOSE=public. This is the mode the installer offers
|
|
5
|
+
# first, because it is the shortest path to a usable development VPS.
|
|
6
|
+
#
|
|
7
|
+
# Two properties make it safe enough to be the default, and both are load
|
|
8
|
+
# bearing:
|
|
9
|
+
#
|
|
10
|
+
# 1. The panel gets its OWN Traefik entrypoint (`panel`, :8090 inside the
|
|
11
|
+
# container). It is never attached to web/websecure, so publishing the
|
|
12
|
+
# panel does not publish a single application, and an application that
|
|
13
|
+
# opts into Traefik is not reachable on this port. Panel access and
|
|
14
|
+
# application exposure stay separate decisions.
|
|
15
|
+
#
|
|
16
|
+
# 2. The panel signs people in itself. There is no middleware on this
|
|
17
|
+
# router: `PORTTA_AUTH_MODE=required` is what stands in front of it, the
|
|
18
|
+
# panel's own process refuses to start in `disabled` mode on anything but
|
|
19
|
+
# loopback, and `portta web up --expose public` refuses before that.
|
|
20
|
+
# See docs/development/adr/0035-authentication-lives-in-the-panel.md.
|
|
21
|
+
#
|
|
22
|
+
# The panel container itself publishes no host port here (web-bind.yaml is not
|
|
23
|
+
# applied), so the only way in is through this entrypoint.
|
|
24
|
+
#
|
|
25
|
+
# HTTP, not HTTPS, when the address is a bare IP: there is no certificate a
|
|
26
|
+
# public CA will issue for one. The password is checked by the panel but the
|
|
27
|
+
# connection is not encrypted, and the installer says so in as many words.
|
|
28
|
+
# `--expose domain` is the mode that gets a real certificate.
|
|
29
|
+
# ============================================================================
|
|
30
|
+
|
|
31
|
+
services:
|
|
32
|
+
traefik:
|
|
33
|
+
ports:
|
|
34
|
+
# 0.0.0.0 by default: `public` means public, and the installer states it.
|
|
35
|
+
- "${PORTTA_WEB_BIND_ADDRESS:-0.0.0.0}:${PORTTA_WEB_PORT:-8081}:8090"
|
|
36
|
+
environment:
|
|
37
|
+
TRAEFIK_ENTRYPOINTS_PANEL_ADDRESS: ":8090"
|
|
38
|
+
|
|
39
|
+
web:
|
|
40
|
+
labels:
|
|
41
|
+
# Overrides the `false` in web.yaml: a later overlay wins for map labels.
|
|
42
|
+
traefik.enable: "true"
|
|
43
|
+
# A bare IP has no hostname to match on, so the rule is the path. The
|
|
44
|
+
# entrypoint is what scopes it: nothing else answers on :8090.
|
|
45
|
+
traefik.http.routers.portta-panel.rule: PathPrefix(`/`)
|
|
46
|
+
traefik.http.routers.portta-panel.entrypoints: panel
|
|
47
|
+
traefik.http.services.portta-panel.loadbalancer.server.port: "8081"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: the one panel path that authenticates itself
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied when GITHUB_APP_ENABLED=true and the panel is routed on a domain.
|
|
5
|
+
#
|
|
6
|
+
# The problem
|
|
7
|
+
# -----------
|
|
8
|
+
# A GitHub App delivers webhooks to a URL of yours. Every panel path sits
|
|
9
|
+
# behind ForwardAuth, which expects a session cookie or a Basic credential --
|
|
10
|
+
# GitHub sends neither, so a delivery is refused before the panel ever sees it.
|
|
11
|
+
# The panel cannot be told about a push, an issue, or a new installation.
|
|
12
|
+
#
|
|
13
|
+
# Why an exemption is safe here, and only here
|
|
14
|
+
# --------------------------------------------
|
|
15
|
+
# This is not a hole in the panel's authentication. It is one path that carries
|
|
16
|
+
# a *different* and, for a machine-to-machine callback, stronger one: GitHub
|
|
17
|
+
# signs the raw body with HMAC-SHA256 under a secret only it and this host
|
|
18
|
+
# know, and `verifySignature` compares in constant time, refusing a missing
|
|
19
|
+
# secret, a missing header and a wrong digest alike. Nothing is parsed before
|
|
20
|
+
# that check passes.
|
|
21
|
+
#
|
|
22
|
+
# So the request is authenticated, just not by a cookie. A session cookie would
|
|
23
|
+
# be the wrong instrument: GitHub has no session, and any scheme that let it in
|
|
24
|
+
# by origin or by address would trust something forgeable.
|
|
25
|
+
#
|
|
26
|
+
# What this deliberately is not
|
|
27
|
+
# -----------------------------
|
|
28
|
+
# It is not a general "these URLs are public" list. Every other panel path
|
|
29
|
+
# authenticates by session and by nothing else, so exempting one would be an
|
|
30
|
+
# unauthenticated door into an API that can start, stop and remove containers.
|
|
31
|
+
# This router names one exact path with `Path(...)`, not a prefix -- there is
|
|
32
|
+
# no `/api/integrations/github/webhook/../projects` to walk out of.
|
|
33
|
+
#
|
|
34
|
+
# The priority is above the panel's own router so the exact path wins over
|
|
35
|
+
# `Host(...)`; both name the same service, so the panel process is unchanged.
|
|
36
|
+
# ============================================================================
|
|
37
|
+
|
|
38
|
+
services:
|
|
39
|
+
web:
|
|
40
|
+
labels:
|
|
41
|
+
traefik.enable: "true"
|
|
42
|
+
traefik.http.routers.portta-panel-webhook.rule: >-
|
|
43
|
+
Host(`${PORTTA_PANEL_ADVERTISED_HOST:-portta-web}`) && Path(`/api/integrations/github/webhook`)
|
|
44
|
+
traefik.http.routers.portta-panel-webhook.priority: "20000"
|
|
45
|
+
traefik.http.routers.portta-panel-webhook.service: portta-panel
|
|
46
|
+
traefik.http.services.portta-panel.loadbalancer.server.port: "8081"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# TCP entrypoints for the Tailscale attachment
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Same as docker/compose/features/tcp.yaml, but the ports and the access network belong
|
|
5
|
+
# to the Tailscale container: Traefik shares its network namespace and has
|
|
6
|
+
# neither of its own (see docs/development/adr/0007-tailscale-sidecar.md).
|
|
7
|
+
#
|
|
8
|
+
# The published ports exist so the VPS itself can reach a database for
|
|
9
|
+
# debugging. What matters is that the entrypoints listen on the tailnet
|
|
10
|
+
# address, so `storefront-postgres.vpn.example.com:5432` works from any machine
|
|
11
|
+
# on your tailnet and from nowhere else.
|
|
12
|
+
# ============================================================================
|
|
13
|
+
|
|
14
|
+
networks:
|
|
15
|
+
access:
|
|
16
|
+
external: true
|
|
17
|
+
name: ${PORTTA_ACCESS_NETWORK:-portta-access}
|
|
18
|
+
|
|
19
|
+
services:
|
|
20
|
+
tailscale:
|
|
21
|
+
networks:
|
|
22
|
+
- access
|
|
23
|
+
ports:
|
|
24
|
+
- "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_TCP_POSTGRES_PORT:-5432}:5432"
|
|
25
|
+
- "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_TCP_REDIS_PORT:-6379}:6379"
|
|
26
|
+
|
|
27
|
+
traefik:
|
|
28
|
+
environment:
|
|
29
|
+
TRAEFIK_ENTRYPOINTS_POSTGRES_ADDRESS: ":5432"
|
|
30
|
+
TRAEFIK_ENTRYPOINTS_REDIS_ADDRESS: ":6379"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: TCP entrypoints, so databases are told apart by hostname
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Enabled by PORTTA_TCP=true. Off by default.
|
|
5
|
+
#
|
|
6
|
+
# One host port per protocol, however many instances are running behind it.
|
|
7
|
+
# Traefik picks the backend from the TLS Server Name Indication, so two
|
|
8
|
+
# projects can both run PostgreSQL on 5432 inside their own containers and
|
|
9
|
+
# neither has to publish a port or renumber anything:
|
|
10
|
+
#
|
|
11
|
+
# storefront-postgres.localhost:5432 -> storefront's postgres:5432
|
|
12
|
+
# checkout-postgres.localhost:5432 -> checkout's postgres:5432
|
|
13
|
+
#
|
|
14
|
+
# Only services that opt in are routed: `exposedByDefault` stays false, and a
|
|
15
|
+
# datastore additionally has to join the access network. Nothing here publishes
|
|
16
|
+
# a database by itself. See docs/product/guides/tcp-routing.md.
|
|
17
|
+
#
|
|
18
|
+
# This overlay is for the `host` attachment. docker/compose/features/tcp-tailscale.yaml
|
|
19
|
+
# is the same thing for the Tailscale attachment, where the ports belong to the
|
|
20
|
+
# Tailscale container because Traefik has no namespace of its own.
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
networks:
|
|
24
|
+
# Created by the CLI before Compose runs, the same way the shared network is.
|
|
25
|
+
# Datastores that opt into hostname routing join THIS network, never the
|
|
26
|
+
# shared HTTP one: Compose validation and routing tests keep that true.
|
|
27
|
+
access:
|
|
28
|
+
external: true
|
|
29
|
+
name: ${PORTTA_ACCESS_NETWORK:-portta-access}
|
|
30
|
+
|
|
31
|
+
services:
|
|
32
|
+
traefik:
|
|
33
|
+
networks:
|
|
34
|
+
- access
|
|
35
|
+
ports:
|
|
36
|
+
- "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_TCP_POSTGRES_PORT:-5432}:5432"
|
|
37
|
+
- "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_TCP_REDIS_PORT:-6379}:6379"
|
|
38
|
+
environment:
|
|
39
|
+
TRAEFIK_ENTRYPOINTS_POSTGRES_ADDRESS: ":5432"
|
|
40
|
+
TRAEFIK_ENTRYPOINTS_REDIS_ADDRESS: ":6379"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: publish the panel on a host interface
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied for every panel access mode except `public`:
|
|
5
|
+
#
|
|
6
|
+
# local 127.0.0.1 this machine, or an SSH tunnel
|
|
7
|
+
# tailscale 100.x.y.z the tailnet only, nothing on the public NIC
|
|
8
|
+
# vpn 127.0.0.1 loopback, plus the router in web-vpn.yaml
|
|
9
|
+
#
|
|
10
|
+
# PORTTA_WEB_BIND_ADDRESS is the whole decision, and the installer and
|
|
11
|
+
# `portta config set panel.access` are the two places that set it. The `public`
|
|
12
|
+
# mode never applies this overlay: there the port belongs to Traefik, so a
|
|
13
|
+
# request cannot reach the panel without passing Portta ForwardAuth first.
|
|
14
|
+
# ============================================================================
|
|
15
|
+
|
|
16
|
+
services:
|
|
17
|
+
web:
|
|
18
|
+
ports:
|
|
19
|
+
- "${PORTTA_WEB_BIND_ADDRESS:-127.0.0.1}:${PORTTA_WEB_PORT:-8081}:8081"
|