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.
Files changed (55) hide show
  1. package/dist/cli.js +77013 -0
  2. package/dist/documentation.json +1 -0
  3. package/dist/runtime/.env.example +440 -0
  4. package/dist/runtime/VERSION +1 -0
  5. package/dist/runtime/config/traefik/dynamic/auth.example.yaml.disabled +24 -0
  6. package/dist/runtime/config/traefik/dynamic/local-tls.yaml +11 -0
  7. package/dist/runtime/config/traefik/dynamic/middlewares.yaml +35 -0
  8. package/dist/runtime/config/traefik/dynamic/portta-aliases.yaml +9 -0
  9. package/dist/runtime/config/traefik/dynamic/portta-auth.yaml +19 -0
  10. package/dist/runtime/config/traefik/dynamic/portta-shares.yaml +10 -0
  11. package/dist/runtime/config/traefik/dynamic/tcp.yaml +23 -0
  12. package/dist/runtime/docker/compose/attach/host.yaml +24 -0
  13. package/dist/runtime/docker/compose/attach/tailscale.yaml +70 -0
  14. package/dist/runtime/docker/compose/compose.yaml +214 -0
  15. package/dist/runtime/docker/compose/features/auth-build.yaml +19 -0
  16. package/dist/runtime/docker/compose/features/auth-dev.yaml +20 -0
  17. package/dist/runtime/docker/compose/features/cloudflare-tunnel.yaml +69 -0
  18. package/dist/runtime/docker/compose/features/dashboard-tailscale.yaml +21 -0
  19. package/dist/runtime/docker/compose/features/dashboard.yaml +21 -0
  20. package/dist/runtime/docker/compose/features/db.yaml +63 -0
  21. package/dist/runtime/docker/compose/features/panel-domain.yaml +42 -0
  22. package/dist/runtime/docker/compose/features/panel-public.yaml +47 -0
  23. package/dist/runtime/docker/compose/features/panel-webhook.yaml +46 -0
  24. package/dist/runtime/docker/compose/features/tcp-tailscale.yaml +30 -0
  25. package/dist/runtime/docker/compose/features/tcp.yaml +40 -0
  26. package/dist/runtime/docker/compose/features/web-bind.yaml +19 -0
  27. package/dist/runtime/docker/compose/features/web-build.yaml +21 -0
  28. package/dist/runtime/docker/compose/features/web-dev.yaml +53 -0
  29. package/dist/runtime/docker/compose/features/web-vpn.yaml +24 -0
  30. package/dist/runtime/docker/compose/features/web.yaml +283 -0
  31. package/dist/runtime/docker/compose/profiles/local-tls.yaml +23 -0
  32. package/dist/runtime/docker/compose/profiles/local.yaml +15 -0
  33. package/dist/runtime/docker/compose/profiles/public.yaml +19 -0
  34. package/dist/runtime/docker/compose/profiles/remote-tls-dns.yaml +26 -0
  35. package/dist/runtime/docker/compose/profiles/remote-tls-http.yaml +32 -0
  36. package/dist/runtime/docker/compose/profiles/remote-tls.yaml +31 -0
  37. package/dist/runtime/docker/compose/profiles/remote.yaml +23 -0
  38. package/dist/runtime/docker/images/README.md +12 -0
  39. package/dist/runtime/docker/images/apply/Dockerfile +45 -0
  40. package/dist/runtime/docker/images/toolbox/Dockerfile +38 -0
  41. package/dist/runtime/scripts/lib/runner-exec.sh +189 -0
  42. package/dist/runtime/templates/README.md +43 -0
  43. package/dist/runtime/templates/overlays/01-single-web.yaml +23 -0
  44. package/dist/runtime/templates/overlays/02-web-api.yaml +30 -0
  45. package/dist/runtime/templates/overlays/03-web-api-postgres.yaml +33 -0
  46. package/dist/runtime/templates/overlays/04-web-api-postgres-redis.yaml +30 -0
  47. package/dist/runtime/templates/overlays/05-multiple-apis.yaml +37 -0
  48. package/dist/runtime/templates/overlays/06-monorepo.yaml +63 -0
  49. package/dist/runtime/templates/overlays/07-worktree.env +47 -0
  50. package/dist/runtime/templates/overlays/08-nonstandard-port.yaml +41 -0
  51. package/dist/runtime/templates/overlays/09-tcp-routing.yaml +70 -0
  52. package/dist/runtime/templates/overlays/10-mailpit.yaml +24 -0
  53. package/dist/runtime/templates/overlays/11-rustfs.yaml +25 -0
  54. package/dist/runtime/templates/project/PORTTA.md +107 -0
  55. 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"