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,440 @@
1
+ # Portta environment structure: 1
2
+ # ============================================================================
3
+ # Portta: configuration
4
+ # ============================================================================
5
+ # Copy to `.env` and adjust. `.env` is git-ignored and may contain secrets.
6
+ #
7
+ # cp .env.example .env
8
+ # ./bin/portta bootstrap
9
+ #
10
+ # This template defines installation structure. Preparation fills missing secrets
11
+ # and host identities; subsequent runs preserve configured values.
12
+ # ============================================================================
13
+
14
+ # ----------------------------------------------------------------------------
15
+ # Common: applies to every profile
16
+ # ----------------------------------------------------------------------------
17
+
18
+ # Active profile: local | remote-private | remote-public
19
+ # Used as the default for `portta up` when no profile is given.
20
+ PORTTA_PROFILE=local
21
+
22
+ # Compose project name of the gateway itself. Keep it stable: `doctor` and the
23
+ # ownership checks rely on it to tell gateway resources from consumer ones.
24
+ PORTTA_PROJECT_NAME=portta
25
+
26
+ # Where this Node expects managed Projects to live. One directory per
27
+ # installation. Development examples named portta-demo-* are discovered here.
28
+ # Changing it changes the reference; files are not moved.
29
+ # Default: ~/projects (user) or /srv/projects (root). Not PORTTA_HOME.
30
+ PORTTA_PROJECTS_HOME=
31
+ # PORTTA_PROJECTS_HOME=
32
+
33
+ # Shared external Docker network that consumer projects attach published
34
+ # HTTP services to. Created by `bootstrap`, never removed automatically.
35
+ PORTTA_NETWORK=portta
36
+
37
+ # Private, `internal: true` network carrying Traefik -> Docker socket proxy
38
+ # traffic. Never joined by consumer projects, never published to the host.
39
+ PORTTA_CONTROL_NETWORK=portta-control
40
+
41
+ # Private network used by persistent TCP forwarders (see docs/product/guides/tcp-access.md).
42
+ PORTTA_ACCESS_NETWORK=portta-access
43
+
44
+ # Log level for gateway components: DEBUG | INFO | WARN | ERROR
45
+ PORTTA_LOG_LEVEL=INFO
46
+
47
+ # Traefik access logs. Useful when debugging routing, noisy otherwise.
48
+ PORTTA_ACCESS_LOG=false
49
+
50
+ # How Traefik treats request headers whose names contain characters outside
51
+ # [A-Za-z0-9-]: these can alias a canonical header once a backend normalises
52
+ # them (X_Auth_User -> X-Auth-User in CGI/WSGI/PHP/nginx), letting a client
53
+ # spoof headers Traefik manages.
54
+ # keep forward as is (Traefik's default; fine behind loopback or a VPN)
55
+ # delete silently strip them: also strips legitimate underscore headers
56
+ # reject answer 400 Bad Request
57
+ # The remote-public profile raises this to `delete` on its own.
58
+ PORTTA_ALIAS_HEADERS_STRATEGY=keep
59
+
60
+ # ----------------------------------------------------------------------------
61
+ # Local profile
62
+ # ----------------------------------------------------------------------------
63
+
64
+ # ----------------------------------------------------------------------------
65
+ # Project domain: what every project hostname is built on
66
+ # ----------------------------------------------------------------------------
67
+ # Hostnames are derived as <compose-project>-<service>.<base>, and this decides
68
+ # the base. It is a name, never an exposure: which interface Traefik answers on
69
+ # is PORTTA_BIND_ADDRESS and PUBLIC_ENABLED, and neither changes here.
70
+ #
71
+ # local localhost. Resolves to loopback with no DNS and no /etc/hosts, and
72
+ # is right for a machine you are sitting at — and useless from
73
+ # anywhere else, which is why it is not the default on a VPS.
74
+ # auto <ip-with-dashes>.sslip.io, derived from PORTTA_PUBLIC_IP below.
75
+ # The provider answers for any name embedding an address, so this
76
+ # needs no DNS record, no registration and no account.
77
+ # custom PORTTA_DOMAIN as typed, for a wildcard you own.
78
+ #
79
+ # portta config set domain.mode local|auto|custom
80
+ #
81
+ # switches between them on a running host, re-labelling every project at once.
82
+ # See docs/development/adr/0022-project-domain-modes.md.
83
+ PORTTA_DOMAIN_MODE=local
84
+
85
+ # Used by `auto`. The installer detects it, and `portta config set domain.mode
86
+ # auto` refreshes it. Kept in the file so no command has to make a network call
87
+ # to know what a hostname should be.
88
+ PORTTA_PUBLIC_IP=
89
+
90
+ # sslip.io | nip.io. Both resolve <anything>.<ip-with-dashes>.<provider> to
91
+ # that address; sslip.io is the default.
92
+ PORTTA_AUTO_DOMAIN_PROVIDER=sslip.io
93
+
94
+ # The base itself. Read when the mode is `custom`, and overwritten by the other
95
+ # two modes, so edit the mode rather than this.
96
+ PORTTA_DOMAIN=localhost
97
+
98
+ # Host interface Traefik publishes on. Keep 127.0.0.1 for local development:
99
+ # it makes the gateway unreachable from the local network.
100
+ # The `remote-public` profile overrides this with 0.0.0.0 on purpose.
101
+ PORTTA_BIND_ADDRESS=127.0.0.1
102
+
103
+ # Host ports. Change only if 80/443 are already taken on this machine.
104
+ PORTTA_HTTP_PORT=80
105
+ PORTTA_HTTPS_PORT=443
106
+
107
+ # ----------------------------------------------------------------------------
108
+ # Traefik dashboard: off by default
109
+ # ----------------------------------------------------------------------------
110
+ # When enabled the dashboard is published on loopback only, on its own port.
111
+ # It is never routed through the public entrypoints. See docs/product/concepts/security.md.
112
+ PORTTA_DASHBOARD=false
113
+ PORTTA_DASHBOARD_BIND_ADDRESS=127.0.0.1
114
+ PORTTA_DASHBOARD_PORT=8080
115
+
116
+ # ----------------------------------------------------------------------------
117
+ # Databases by hostname: off by default
118
+ # ----------------------------------------------------------------------------
119
+ # One host port per protocol, however many instances run behind it, told apart
120
+ # by the hostname the client asks for:
121
+ #
122
+ # storefront-postgres.localhost:5432 -> storefront's postgres:5432
123
+ # checkout-postgres.localhost:5432 -> checkout's postgres:5432
124
+ #
125
+ # Only services that opt in are routed, and the projects that do join the
126
+ # access network rather than the shared HTTP one. TLS is required, because the
127
+ # hostname travels in the TLS handshake. PostgreSQL and Redis work; MySQL
128
+ # cannot. Read docs/product/guides/tcp-routing.md before turning this on.
129
+ #
130
+ # Refused on the remote-public profile: a database is never reachable from the
131
+ # internet.
132
+
133
+ PORTTA_TCP=false
134
+
135
+ # Host ports for the entrypoints. Change them if something already holds the
136
+ # standard ones on this machine.
137
+ PORTTA_TCP_POSTGRES_PORT=5432
138
+ PORTTA_TCP_REDIS_PORT=6379
139
+
140
+ # ----------------------------------------------------------------------------
141
+ # Web panel: off by default
142
+ # ----------------------------------------------------------------------------
143
+ # The administration panel (see docs/product/guides/web-ui.md). It is published on loopback
144
+ # and never routed through the public entrypoints. `portta web up` sets
145
+ # these for you.
146
+
147
+ PORTTA_WEB=false
148
+ PORTTA_WEB_BIND_ADDRESS=127.0.0.1
149
+ PORTTA_WEB_PORT=8081
150
+
151
+ # Where the panel can be reached from. This is panel access, and it is
152
+ # deliberately independent of PORTTA_PROFILE: making the panel reachable never
153
+ # makes an application reachable. See docs/development/adr/0021-panel-access-modes.md.
154
+ #
155
+ # local loopback only (default). Reach it over an SSH tunnel:
156
+ # ssh -L 8081:127.0.0.1:8081 user@host
157
+ # tailscale bound to this node's tailnet address only. Set
158
+ # PORTTA_WEB_BIND_ADDRESS to the 100.x.y.z address; nothing is
159
+ # published on the public interface.
160
+ # public published on every interface through Traefik's own `panel`
161
+ # entrypoint. Requires PORTTA_AUTH_MODE=required; the panel
162
+ # authenticates requests itself.
163
+ # vpn routed by Traefik at PORTTA_WEB_HOST.<domain>, which is
164
+ # meaningful only on the remote-private profile. Requires a
165
+ # credential, and is refused on remote-public.
166
+ #
167
+ # portta config set panel.access public|tailscale|local
168
+ #
169
+ # switches between them on an installed host without reinstalling.
170
+ PORTTA_WEB_EXPOSE=local
171
+ PORTTA_WEB_HOST=portta-web
172
+
173
+ # The address the panel is advertised at in `public` mode, where the published
174
+ # port belongs to Traefik and 0.0.0.0 is not something anybody types. The
175
+ # installer fills this with the detected public IP or hostname; it is cosmetic
176
+ # and changes no binding.
177
+ PORTTA_PANEL_ADVERTISED_HOST=
178
+
179
+ # Refuse every mutating endpoint. Useful when an agent is driving the panel,
180
+ # and the default whenever the panel is routed beyond loopback.
181
+ PORTTA_WEB_READ_ONLY=false
182
+
183
+ # The panel's own front door, which is now the panel's own business.
184
+ #
185
+ # disabled every request is the local operator. Only legal on loopback: the
186
+ # panel refuses to start any other way, because reaching it there
187
+ # already means having the machine.
188
+ # required the panel signs people in. Accounts, roles, sessions, tokens and
189
+ # an optional second factor, all in its database. The first person
190
+ # is created at /setup, once, and creates everyone else.
191
+ #
192
+ # See docs/development/adr/0035-authentication-lives-in-the-panel.md and docs/product/guides/authentication.md.
193
+ PORTTA_AUTH_MODE=disabled
194
+
195
+ # Signs sessions, for the panel and for the ForwardAuth service in front of
196
+ # project hostnames. Generated by `portta bootstrap` and `portta web up` without
197
+ # ever being printed. Changing it signs everybody out.
198
+ PORTTA_AUTH_SECRET=
199
+
200
+ # Where a browser reaches the panel. Three things come from it: whether the
201
+ # session cookie may be `Secure`, which origins a sign-in is accepted from, and
202
+ # the address the panel prints. `portta web up` writes it to match the exposure;
203
+ # empty means loopback on the published port.
204
+ PORTTA_PANEL_URL=
205
+
206
+ # Extra origins a browser may sign in from, comma-separated. Needed only when
207
+ # the panel answers on a name the URL above does not name -- a reverse proxy in
208
+ # front of it, a second hostname on the same port.
209
+ PORTTA_PANEL_TRUSTED_ORIGINS=
210
+
211
+ # How many sign-in attempts one address gets every ten minutes, before the panel
212
+ # starts answering 429. Five is enough to make guessing expensive. The window is
213
+ # per address, so a whole team behind one NAT is one address: that is what this
214
+ # is for. Between 3 and 100; anything else is read as the default.
215
+ PORTTA_AUTH_SIGNIN_ATTEMPTS=5
216
+
217
+ # The project's documentation at /docs, served from the panel image. Static
218
+ # text with no host information in it, so a routed panel may serve it.
219
+ PORTTA_RUNTIME_DOCS=true
220
+
221
+ # The API reference and its console at /docs/api. This one issues real requests
222
+ # against this panel, so it keeps the conservative default: empty means enabled
223
+ # on loopback and disabled when routed. The /api/openapi.json contract is
224
+ # always available.
225
+ PORTTA_RUNTIME_API_DOCS=
226
+
227
+ # Panel persistence. `bootstrap` or `web up` generates the password; it is
228
+ # never printed or returned by the API. In managed mode the application derives
229
+ # its URL from these fields. A URL is accepted only in explicit external mode.
230
+ PORTTA_DB_NETWORK=portta-data
231
+ PORTTA_DB_VOLUME=portta-db
232
+ # managed: db:5432 is fixed internal DNS/port, with no host publication.
233
+ # external: only PORTTA_RUNTIME_DATABASE_URL determines the connection.
234
+ PORTTA_RUNTIME_DB_MODE=managed
235
+ PORTTA_RUNTIME_DB_NAME=portta
236
+ PORTTA_RUNTIME_DB_USER=portta
237
+ PORTTA_RUNTIME_DB_PASSWORD=
238
+ PORTTA_RUNTIME_DATABASE_URL=
239
+
240
+ # The panel image. A normal installation pulls this; it never builds, because
241
+ # an installed PORTTA_HOME holds no application source. Pinned like every other
242
+ # component image (docs/development/adr/0004-pinned-versions.md).
243
+ PORTTA_WEB_IMAGE=
244
+ PORTTA_AUTH_IMAGE=
245
+
246
+ # Build the panel image from this checkout instead of pulling it. Only useful
247
+ # inside the repository: the build context is the repository root.
248
+ PORTTA_WEB_BUILD=false
249
+
250
+ # Development mode: Next.js and the API in one container with hot reloading.
251
+ PORTTA_WEB_DEV=false
252
+
253
+ # Private, internal network carrying panel -> its own Docker socket proxy.
254
+ PORTTA_WEB_NETWORK=portta-web
255
+ # Host uid:gid, filled once by preparation so the containers can read/write state.
256
+ PORTTA_WEB_USER=
257
+ PORTTA_AUTH_USER=
258
+
259
+ # User the panel container runs as. .env is owner-only, so the panel has to run
260
+ # as whoever owns it or the Settings page cannot save. `bootstrap`, `web up` and
261
+ # the installer record "$(id -u):$(id -g)" here when it is missing; the image's
262
+ # own `node` is only right when the host uid happens to be 1000.
263
+ # PORTTA_WEB_USER=1000:1000
264
+
265
+ # The user the authentication service runs as. Same reason as above: it reads
266
+ # .env once and the owner-only protection store on every request, and both
267
+ # belong to whoever installed Portta. The installer records the right value.
268
+ # PORTTA_AUTH_USER=1000:1000
269
+
270
+ # ----------------------------------------------------------------------------
271
+ # Applying settings from the panel: optional, off by default
272
+ # ----------------------------------------------------------------------------
273
+ # Traefik reads its static configuration from the environment its container was
274
+ # created with, so a setting the panel saves takes effect only once the gateway
275
+ # containers are RECREATED. Recreating them means Compose, which the panel
276
+ # cannot reach: its Docker permissions stop at start, stop and restart.
277
+ #
278
+ # With this on, `portta up` prepares one stopped, single-purpose container whose
279
+ # command is fixed at creation time -- `portta up`, nothing else, no argument
280
+ # from the panel -- and the panel gains an "Apply and restart" button that only
281
+ # starts it. The panel goes offline for a few seconds while it is recreated, and
282
+ # comes back on its own.
283
+ #
284
+ # Be clear about what this grants: anyone who can write through the panel can
285
+ # run `portta up` on this host. It is refused in read-only mode, the applier
286
+ # takes no input, and the panel never gains `exec`. Leave it false on a host
287
+ # where the panel is reachable by anyone you would not hand a shell.
288
+ #
289
+ # On a repository checkout -- PORTTA_WEB_BUILD or PORTTA_WEB_DEV -- the apply
290
+ # rebuilds the local images first. The build runs on the host daemon, not inside
291
+ # the applier, and Compose builds before it stops anything; the only cost is
292
+ # time, and the panel says so before you confirm.
293
+ # See docs/development/adr/0026-applying-settings-from-the-panel.md.
294
+
295
+ PORTTA_APPLY=true
296
+
297
+ # Prepare the project runner the panel may start to drive Compose for one
298
+ # project (up, stop, restart, build, down). Off by default, and deliberately
299
+ # absent from the Settings catalogue — turning it on is an edit on the host.
300
+ # See docs/development/adr/0030-the-panel-and-a-project-lifecycle.md.
301
+ PORTTA_RUNNER=true
302
+
303
+ # ----------------------------------------------------------------------------
304
+ # TLS
305
+ # ----------------------------------------------------------------------------
306
+
307
+ # Master switch for HTTPS. When false, only the HTTP entrypoint serves routes.
308
+ TLS_ENABLED=false
309
+
310
+ # TLS mode: local | acme
311
+ # local: certificates from a local CA (see `portta tls` and docs)
312
+ # acme : Let's Encrypt / ACME, requires a real domain (remote profiles)
313
+ TLS_MODE=local
314
+
315
+ # Contact address for the ACME account. Required when TLS_MODE=acme.
316
+ ACME_EMAIL=
317
+
318
+ # How the ACME account proves it controls the name: dns | http
319
+ #
320
+ # dns (default) One wildcard certificate for *.<domain>. The only challenge
321
+ # that can issue a wildcard, so a hostname works over HTTPS before
322
+ # anything runs on it, and the only one that works for a private gateway
323
+ # Let's Encrypt can never reach. Needs a DNS provider credential below.
324
+ #
325
+ # http One certificate per hostname, issued the first time a router asks for
326
+ # it. Needs nothing but :80 reachable from the internet -- no credential.
327
+ # Cannot issue a wildcard, so a new hostname waits a second or two on its
328
+ # first request, and Let's Encrypt counts each name against the weekly
329
+ # limit for the registered domain.
330
+ ACME_CHALLENGE=dns
331
+
332
+ # ACME directory URL. Point at the staging endpoint while testing to avoid
333
+ # burning Let's Encrypt rate limits.
334
+ # staging: https://acme-staging-v02.api.letsencrypt.org/directory
335
+ ACME_CA_SERVER=https://acme-v02.api.letsencrypt.org/directory
336
+
337
+ # DNS-01 provider name as understood by Traefik/lego. Required for wildcard
338
+ # certificates, and ignored when ACME_CHALLENGE=http. `cloudflare` is the
339
+ # reference implementation shipped here.
340
+ ACME_DNS_PROVIDER=cloudflare
341
+
342
+ # Resolvers used to check DNS-01 propagation, comma separated host:port.
343
+ ACME_DNS_RESOLVERS=1.1.1.1:53,8.8.8.8:53
344
+
345
+ # ----------------------------------------------------------------------------
346
+ # Private access (VPN / Tailscale)
347
+ # ----------------------------------------------------------------------------
348
+
349
+ # When true, Traefik runs inside the Tailscale container's network namespace
350
+ # and listens on the tailnet address, publishing nothing on the public
351
+ # interface. When false, the remote-private profile falls back to binding
352
+ # PORTTA_BIND_ADDRESS: set that to your VPN interface's address.
353
+ TAILSCALE_ENABLED=false
354
+
355
+ # Hostname this node takes on the tailnet.
356
+ TAILSCALE_HOSTNAME=portta
357
+
358
+ # Auth key or OAuth client secret. NEVER commit this. Prefer an ephemeral,
359
+ # tagged, pre-authorized key. See docs/product/guides/tailscale.md.
360
+ TS_AUTHKEY=
361
+
362
+ # Extra flags forwarded to `tailscale up`. Tag the node so ACLs can name it
363
+ # without depending on a person's identity:
364
+ # TS_EXTRA_ARGS=--advertise-tags=tag:portta
365
+ TS_EXTRA_ARGS=
366
+
367
+ # Private wildcard namespace served only over the VPN, e.g. vpn.dev.example.com
368
+ # Leave empty to reuse PORTTA_DOMAIN.
369
+ PRIVATE_DOMAIN=
370
+
371
+ # ----------------------------------------------------------------------------
372
+ # Public access: opt-in, disabled by default
373
+ # ----------------------------------------------------------------------------
374
+ # Enabling this publishes 80/443 on every host interface. Only HTTP services
375
+ # that explicitly opt in are routed. Databases and caches are never published.
376
+
377
+ PUBLIC_ENABLED=false
378
+
379
+ # Public wildcard namespace, e.g. dev.example.com
380
+ PUBLIC_DOMAIN=
381
+
382
+ # ----------------------------------------------------------------------------
383
+ # Cloudflare: optional DNS provider for wildcard DNS-01
384
+ # ----------------------------------------------------------------------------
385
+ # Use a scoped API Token (Zone:DNS:Edit on the target zone only).
386
+ # Never use the Global API Key. Never commit the token.
387
+
388
+ CLOUDFLARE_ENABLED=false
389
+ CF_DNS_API_TOKEN=
390
+ CLOUDFLARE_ZONE=
391
+
392
+ # ----------------------------------------------------------------------------
393
+ # GitHub App: optional, off by default
394
+ # ----------------------------------------------------------------------------
395
+ # The panel's only outbound network access, and its only long-lived secret that
396
+ # is not a Traefik credential. With GITHUB_APP_ENABLED=false the panel makes no
397
+ # request to github.com and behaves exactly as it did without this section.
398
+ #
399
+ # The private key is a FILE, never a value here: the panel can write this .env,
400
+ # and must not be able to write the key that authenticates it. Create the App,
401
+ # download the .pem into `state/github/`, and `chmod 600` it. See
402
+ # docs/product/guides/github.md for the exact permission set.
403
+
404
+ GITHUB_APP_ENABLED=false
405
+
406
+ # The numeric App id from the App's settings page. Not a secret.
407
+ GITHUB_APP_ID=
408
+
409
+ # The .pem inside the panel container. `state/github/` is the only directory
410
+ # mounted there, so the path must begin /app/state/github/. The filename is
411
+ # yours: keep the one GitHub gave the download if you like.
412
+ GITHUB_APP_PRIVATE_KEY_FILE=/app/state/github/app.pem
413
+
414
+ # Verifies webhook deliveries. Generate one when you create the App.
415
+ GITHUB_APP_WEBHOOK_SECRET=
416
+
417
+ # GitHub Enterprise Server users: your API root, e.g. https://ghe.example.com/api/v3
418
+ GITHUB_API_URL=https://api.github.com
419
+
420
+ # How often the panel re-reads what changed on GitHub, in minutes.
421
+ #
422
+ # A loopback panel cannot receive webhook deliveries, so without this the
423
+ # projection is only as fresh as the last time somebody pressed Sync. A pass
424
+ # asks for issues updated since the stored cursor, so a quiet repository costs
425
+ # one conditional request.
426
+ #
427
+ # Set 0 on a panel that DOES receive webhooks, where the timer would do the
428
+ # same work twice.
429
+ GITHUB_SYNC_INTERVAL_MINUTES=15
430
+
431
+ # ----------------------------------------------------------------------------
432
+ # Cloudflare Tunnel: optional connector (credential stays in state/cloudflared)
433
+ # ----------------------------------------------------------------------------
434
+ CLOUDFLARE_TUNNEL_ENABLED=false
435
+ CLOUDFLARE_TUNNEL_ZONE=
436
+ CLOUDFLARE_TUNNEL_ID=
437
+ PORTTA_CLOUDFLARED_IMAGE=cloudflare/cloudflared:2026.8.3
438
+
439
+ # Optional Traefik API override. Empty derives the endpoint from the attachment.
440
+ PORTTA_RUNTIME_TRAEFIK_API=
@@ -0,0 +1 @@
1
+ 0.8.0
@@ -0,0 +1,24 @@
1
+ # ============================================================================
2
+ # Portta project protection: the working middleware is generated for you
3
+ # ============================================================================
4
+ # Do not copy this file into the dynamic directory. `portta-auth.yaml` already
5
+ # defines `portta-forward-auth`; this disabled example documents the project
6
+ # label that opts one existing router into it.
7
+ #
8
+ # 1. Create or rotate the host credential:
9
+ #
10
+ # portta auth protect demo-web.example.com --project demo --service web
11
+ #
12
+ # 2. Add the middleware to that project's own router:
13
+ #
14
+ # labels:
15
+ # - "traefik.http.routers.demo-web.middlewares=portta-forward-auth@file"
16
+ #
17
+ # Portta deliberately does not edit a consumer project's labels. The project
18
+ # owns that router; Portta owns only the credential, login route and ForwardAuth
19
+ # middleware. `/__portta/auth` is reserved on every protected hostname.
20
+ #
21
+ # To remove the credential (the project label remains yours to remove):
22
+ #
23
+ # portta auth unprotect demo-web.example.com
24
+ # ============================================================================
@@ -0,0 +1,11 @@
1
+ # Generated by `portta tls init`. Safe to delete: re-run the command to
2
+ # recreate it. The certificate and key live in config/tls/, which is git-ignored.
3
+ tls:
4
+ stores:
5
+ default:
6
+ defaultCertificate:
7
+ certFile: /etc/traefik/tls/wildcard.crt
8
+ keyFile: /etc/traefik/tls/wildcard.key
9
+ certificates:
10
+ - certFile: /etc/traefik/tls/wildcard.crt
11
+ keyFile: /etc/traefik/tls/wildcard.key
@@ -0,0 +1,35 @@
1
+ # ============================================================================
2
+ # Portta: reusable dynamic configuration
3
+ # ============================================================================
4
+ # Hot-reloaded by Traefik's file provider. Reference these from a consumer
5
+ # project with, for example:
6
+ #
7
+ # labels:
8
+ # traefik.http.routers.web.middlewares: "portta-secure-headers@file"
9
+ #
10
+ # Nothing here is applied automatically: a project opts in per router.
11
+ # ============================================================================
12
+
13
+ http:
14
+ middlewares:
15
+ # Sensible baseline response headers for browser-facing dev environments.
16
+ portta-secure-headers:
17
+ headers:
18
+ frameDeny: false
19
+ contentTypeNosniff: true
20
+ browserXssFilter: true
21
+ referrerPolicy: strict-origin-when-cross-origin
22
+
23
+ # Marks responses as coming through the gateway. Handy when debugging
24
+ # whether a request reached Traefik at all.
25
+ portta-stamp:
26
+ headers:
27
+ customResponseHeaders:
28
+ X-Portta: "1"
29
+
30
+ # Blocks everything. Attach to a router you want reachable only from the
31
+ # tailnet while you sort out a real policy.
32
+ portta-deny-all:
33
+ ipAllowList:
34
+ sourceRange:
35
+ - 127.0.0.1/32
@@ -0,0 +1,9 @@
1
+ # ============================================================================
2
+ # Generated by Portta panel. Edits are overwritten.
3
+ # ============================================================================
4
+ # Hostname aliases: one additional router per aliased service. The project's
5
+ # own router is never touched, so both hostnames answer. See docs/web-ui.md.
6
+ # ============================================================================
7
+ # portta-aliases: []
8
+
9
+ # No alias is set. A project is reached on the hostname it derives.
@@ -0,0 +1,19 @@
1
+ # ============================================================================
2
+ # Generated by Portta. Edits are overwritten.
3
+ # ============================================================================
4
+ # ForwardAuth is a separate process. The reserved login routers below must
5
+ # never reference either authentication middleware. See ADR 0027.
6
+ # ============================================================================
7
+ http:
8
+ routers:
9
+ services:
10
+ portta-auth:
11
+ loadBalancer:
12
+ servers:
13
+ - url: "http://portta-auth:4180"
14
+ middlewares:
15
+ portta-forward-auth:
16
+ forwardAuth:
17
+ address: "http://portta-auth:4180/verify"
18
+ trustForwardHeader: true
19
+ authResponseHeaders: [X-Forwarded-User, X-Portta-Actor, X-Portta-Actor-Kind]
@@ -0,0 +1,10 @@
1
+ # ============================================================================
2
+ # Generated by Portta panel. Edits are overwritten.
3
+ # ============================================================================
4
+ # Temporary shares: one additional hostname per service, each with an expiry.
5
+ # A project's own router is never touched, and revoking a share deletes a
6
+ # block from this file. See docs/product/guides/sharing.md.
7
+ # ============================================================================
8
+ # portta-shares: []
9
+
10
+ # Nothing is shared. "Private" is the absence of a share, not a deny rule.
@@ -0,0 +1,23 @@
1
+ # ============================================================================
2
+ # TLS options for the TCP entrypoints
3
+ # ============================================================================
4
+ # Loaded by the file provider on every profile. It defines options only; the
5
+ # certificate comes from the default store, which is either the local CA
6
+ # (`portta tls init`) or ACME on the remote profiles.
7
+ # ============================================================================
8
+
9
+ tls:
10
+ options:
11
+ # PostgreSQL 17 registered `postgresql` as an ALPN protocol identifier when
12
+ # it added direct TLS negotiation, and libpq 17+ offers it on every TLS
13
+ # connection, STARTTLS included. Traefik's entrypoints advertise h2,
14
+ # http/1.1 and acme-tls/1, so without this the handshake dies before any
15
+ # Postgres traffic happens:
16
+ #
17
+ # psql: SSL error: tlsv1 alert no application protocol
18
+ #
19
+ # Older clients send no ALPN at all and are unaffected, so one option
20
+ # serves both. See docs/product/guides/tcp-routing.md.
21
+ postgres:
22
+ alpnProtocols:
23
+ - postgresql
@@ -0,0 +1,24 @@
1
+ # ============================================================================
2
+ # Attachment: Traefik in its own network namespace, ports on the host
3
+ # ============================================================================
4
+ # Used by the `local` profile, and by remote profiles when Tailscale is not
5
+ # managed by the gateway (a host-native Tailscale, WireGuard, or a plain
6
+ # public VPS).
7
+ #
8
+ # PORTTA_BIND_ADDRESS decides who can reach it:
9
+ # 127.0.0.1 this machine only (local default)
10
+ # 100.x.y.z the tailnet only (host-native Tailscale)
11
+ # 0.0.0.0 everyone (remote-public, opt-in)
12
+ #
13
+ # `doctor` fails if the local profile binds to anything but loopback, or if
14
+ # the private profile binds to 0.0.0.0.
15
+ # ============================================================================
16
+
17
+ services:
18
+ traefik:
19
+ networks:
20
+ - gateway
21
+ - control
22
+ ports:
23
+ - "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_HTTP_PORT:-80}:80"
24
+ - "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_HTTPS_PORT:-443}:443"
@@ -0,0 +1,70 @@
1
+ # ============================================================================
2
+ # Attachment: Traefik inside the Tailscale container's network namespace
3
+ # ============================================================================
4
+ # The default for the `remote-private` profile.
5
+ #
6
+ # `network_mode: service:tailscale` merges the two containers' network
7
+ # namespaces, so Traefik listens directly on the node's tailnet address. No
8
+ # port is published on the host at all: the gateway is unreachable from the
9
+ # VPS's public interface, and reachable from the tailnet subject to your
10
+ # Tailscale ACLs. See docs/development/adr/0007-tailscale-sidecar.md.
11
+ #
12
+ # Traefik keeps its access to the shared and control networks because the
13
+ # Tailscale container joins them and the two share a namespace.
14
+ #
15
+ # Kernel networking (TS_USERSPACE=false) is required: userspace mode has no
16
+ # real interface in the namespace, so an inbound connection would never reach
17
+ # Traefik. That is what /dev/net/tun and NET_ADMIN are for.
18
+ # ============================================================================
19
+
20
+ services:
21
+ tailscale:
22
+ image: tailscale/tailscale:v1.102.3
23
+ restart: unless-stopped
24
+ hostname: ${TAILSCALE_HOSTNAME:-portta}
25
+ networks:
26
+ - gateway
27
+ - control
28
+ ports:
29
+ # Loopback by default, so `curl` on the VPS itself works for debugging.
30
+ # The remote-public profile raises PORTTA_BIND_ADDRESS to 0.0.0.0,
31
+ # and these become the public listeners: Traefik has none of its own.
32
+ - "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_HTTP_PORT:-80}:80"
33
+ - "${PORTTA_BIND_ADDRESS:-127.0.0.1}:${PORTTA_HTTPS_PORT:-443}:443"
34
+ environment:
35
+ TS_AUTHKEY: ${TS_AUTHKEY:-}
36
+ # Persisting state across restarts keeps the node identity: and so the
37
+ # tailnet address and any Tailscale-issued certificate: stable.
38
+ TS_STATE_DIR: /var/lib/tailscale
39
+ # Authenticate once, then rely on persisted state. Without this a restart
40
+ # re-runs `tailscale up` and burns a non-reusable key.
41
+ TS_AUTH_ONCE: "true"
42
+ TS_USERSPACE: "false"
43
+ # Never let MagicDNS rewrite resolv.conf: Traefik shares this namespace
44
+ # and must keep resolving container names through Docker's resolver.
45
+ TS_ACCEPT_DNS: "false"
46
+ TS_HOSTNAME: ${TAILSCALE_HOSTNAME:-portta}
47
+ TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-}
48
+ volumes:
49
+ - ./state/tailscale:/var/lib/tailscale
50
+ devices:
51
+ - /dev/net/tun:/dev/net/tun
52
+ cap_add:
53
+ - NET_ADMIN
54
+ - NET_RAW
55
+ healthcheck:
56
+ test: ["CMD", "tailscale", "status", "--peers=false"]
57
+ interval: 10s
58
+ timeout: 5s
59
+ retries: 10
60
+ start_period: 20s
61
+ labels:
62
+ traefik.enable: "false"
63
+ portta.managed: "true"
64
+ portta.component: tailscale
65
+
66
+ traefik:
67
+ network_mode: service:tailscale
68
+ depends_on:
69
+ tailscale:
70
+ condition: service_healthy