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,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
|