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,21 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: build the panel image locally (development only)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied when PORTTA_WEB_BUILD=true, which only makes sense inside a checkout:
|
|
5
|
+
# the build context is the repository root, and an installed PORTTA_HOME holds
|
|
6
|
+
# no application source at all.
|
|
7
|
+
#
|
|
8
|
+
# A normal installation pulls the published image instead, so a host never
|
|
9
|
+
# needs a compiler, a lockfile or the panel's source to run the panel.
|
|
10
|
+
# See docs/development/adr/0020-installer-and-portta-home.md.
|
|
11
|
+
# ============================================================================
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
web:
|
|
15
|
+
build:
|
|
16
|
+
context: .
|
|
17
|
+
dockerfile: apps/web/Dockerfile
|
|
18
|
+
target: runtime
|
|
19
|
+
args:
|
|
20
|
+
PORTTA_VERSION: ${PORTTA_VERSION:-}
|
|
21
|
+
image: ${PORTTA_WEB_IMAGE:-fabioassuncao/portta:${PORTTA_VERSION}}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: panel in development mode
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied when PORTTA_WEB_DEV=true, or by `portta web dev`.
|
|
5
|
+
#
|
|
6
|
+
# One container, one port. The panel is a single Node process — Next, the Hono
|
|
7
|
+
# API, the event stream and the WebSocket upgrades all behind one dispatcher —
|
|
8
|
+
# so HMR arrives on the same 8081 the API answers on.
|
|
9
|
+
#
|
|
10
|
+
# Only source directories are bind-mounted, so the image's node_modules stay in
|
|
11
|
+
# place and the host never needs Node installed.
|
|
12
|
+
# ============================================================================
|
|
13
|
+
|
|
14
|
+
services:
|
|
15
|
+
web:
|
|
16
|
+
build:
|
|
17
|
+
context: .
|
|
18
|
+
dockerfile: apps/web/Dockerfile
|
|
19
|
+
target: dev
|
|
20
|
+
args:
|
|
21
|
+
PORTTA_VERSION: ${PORTTA_VERSION:-}
|
|
22
|
+
image: fabioassuncao/portta:dev
|
|
23
|
+
# The package script, never a restated command line: it carries
|
|
24
|
+
# `--conditions=development`, without which the workspace packages resolve
|
|
25
|
+
# to a dist/ the dev image does not build.
|
|
26
|
+
command: ["npm", "run", "dev"]
|
|
27
|
+
environment:
|
|
28
|
+
NODE_ENV: development
|
|
29
|
+
volumes:
|
|
30
|
+
- ./apps/web/app:/app/apps/web/app
|
|
31
|
+
- ./apps/web/components:/app/apps/web/components
|
|
32
|
+
- ./apps/web/lib:/app/apps/web/lib
|
|
33
|
+
- ./apps/web/messages:/app/apps/web/messages
|
|
34
|
+
- ./apps/web/server:/app/apps/web/server
|
|
35
|
+
- ./apps/web/public:/app/apps/web/public
|
|
36
|
+
# The packages the panel composes, so editing one reloads it like its own
|
|
37
|
+
# source: the shared derivations, the contract, the schema, auth, and the
|
|
38
|
+
# services and API.
|
|
39
|
+
- ./packages/core/src:/app/packages/core/src
|
|
40
|
+
- ./packages/contracts/src:/app/packages/contracts/src
|
|
41
|
+
- ./packages/auth/src:/app/packages/auth/src
|
|
42
|
+
- ./packages/db/src:/app/packages/db/src
|
|
43
|
+
# The generated migrations the process applies on the next migrate.
|
|
44
|
+
# Without this a newly generated file stays in the image until the next
|
|
45
|
+
# rebuild, so `portta db migrate` and a restart would both see a stale set.
|
|
46
|
+
- ./packages/db/drizzle:/app/packages/db/drizzle
|
|
47
|
+
- ./packages/server/src:/app/packages/server/src
|
|
48
|
+
# The documentation itself: `app/docs` reads and renders it, so a change
|
|
49
|
+
# to a Markdown file is visible on the next request.
|
|
50
|
+
- ./docs:/app/docs:ro
|
|
51
|
+
- ./README.md:/app/README.md:ro
|
|
52
|
+
- ./CHANGELOG.md:/app/CHANGELOG.md:ro
|
|
53
|
+
- ./.env:/app/state/.env
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: reach the panel over the VPN
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied when PORTTA_WEB_EXPOSE=vpn. It adds a Traefik router for the
|
|
5
|
+
# panel so it is reachable at <host>.<domain> from the tailnet, instead of only
|
|
6
|
+
# on the VPS's own loopback.
|
|
7
|
+
#
|
|
8
|
+
# The CLI refuses this overlay on the remote-public profile, where Traefik
|
|
9
|
+
# binds every interface and a router would therefore be public. It must never
|
|
10
|
+
# be reachable from the internet.
|
|
11
|
+
#
|
|
12
|
+
# There is no middleware on the router: the panel signs people in itself, and
|
|
13
|
+
# `portta web up --expose vpn` refuses unless PORTTA_AUTH_MODE=required. The
|
|
14
|
+
# tailnet is the first boundary and the panel's own login is the second.
|
|
15
|
+
# See docs/development/adr/0035-authentication-lives-in-the-panel.md.
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
services:
|
|
19
|
+
web:
|
|
20
|
+
labels:
|
|
21
|
+
traefik.enable: "true"
|
|
22
|
+
traefik.http.routers.portta-web.rule: >-
|
|
23
|
+
Host(`${PORTTA_WEB_HOST:-portta-web}.${PORTTA_DOMAIN:-localhost}`)
|
|
24
|
+
traefik.http.services.portta-web.loadbalancer.server.port: "8081"
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: administration panel (opt-in)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Enabled by PORTTA_WEB=true, or by `portta web up`.
|
|
5
|
+
#
|
|
6
|
+
# The panel is a development tool, so it is published on loopback by default
|
|
7
|
+
# and never routed through the public entrypoints. Reaching it over a VPN is a
|
|
8
|
+
# separate, explicit overlay: docker/compose/features/web-vpn.yaml.
|
|
9
|
+
#
|
|
10
|
+
# It gets its OWN Docker socket proxy rather than sharing Traefik's. Traefik
|
|
11
|
+
# needs a strictly read-only view; the panel needs to start, stop, restart and
|
|
12
|
+
# remove containers, and those two permission sets must not be merged into one.
|
|
13
|
+
# See docs/product/guides/web-ui.md and docs/development/adr/0008-web-panel-socket-proxy.md.
|
|
14
|
+
# ============================================================================
|
|
15
|
+
|
|
16
|
+
networks:
|
|
17
|
+
# Panel <-> its socket proxy. `internal: true`: the proxy has no route to the
|
|
18
|
+
# outside world and is reachable from nothing else.
|
|
19
|
+
webcontrol:
|
|
20
|
+
name: ${PORTTA_WEB_NETWORK:-portta-web}
|
|
21
|
+
internal: true
|
|
22
|
+
labels:
|
|
23
|
+
portta.managed: "true"
|
|
24
|
+
portta.component: web-control-network
|
|
25
|
+
|
|
26
|
+
services:
|
|
27
|
+
# --------------------------------------------------------------------------
|
|
28
|
+
# The panel's Docker socket proxy
|
|
29
|
+
# --------------------------------------------------------------------------
|
|
30
|
+
# Write access is limited to container lifecycle and interactive exec.
|
|
31
|
+
# Images, volumes, build, swarm and system endpoints (where prune lives) stay
|
|
32
|
+
# denied, and the panel enforces its narrower path allowlist on top.
|
|
33
|
+
web-socket-proxy:
|
|
34
|
+
image: tecnativa/docker-socket-proxy:v0.5.0
|
|
35
|
+
restart: unless-stopped
|
|
36
|
+
read_only: true
|
|
37
|
+
environment:
|
|
38
|
+
# -- read ------------------------------------------------------------
|
|
39
|
+
CONTAINERS: "1"
|
|
40
|
+
NETWORKS: "1"
|
|
41
|
+
EVENTS: "1"
|
|
42
|
+
INFO: "1"
|
|
43
|
+
VERSION: "1"
|
|
44
|
+
PING: "1"
|
|
45
|
+
# -- write, limited to the container lifecycle ----------------------
|
|
46
|
+
POST: "1"
|
|
47
|
+
ALLOW_START: "1"
|
|
48
|
+
ALLOW_STOP: "1"
|
|
49
|
+
ALLOW_RESTARTS: "1"
|
|
50
|
+
# -- denied ----------------------------------------------------------
|
|
51
|
+
ALLOW_PAUSE: "0"
|
|
52
|
+
ALLOW_UNPAUSE: "0"
|
|
53
|
+
AUTH: "0"
|
|
54
|
+
BUILD: "0"
|
|
55
|
+
COMMIT: "0"
|
|
56
|
+
CONFIGS: "0"
|
|
57
|
+
DISTRIBUTION: "0"
|
|
58
|
+
# Four exec paths are allowed again inside the panel: create, attach,
|
|
59
|
+
# resize and inspect. Traefik's separate proxy remains read-only.
|
|
60
|
+
EXEC: "1"
|
|
61
|
+
GRPC: "0"
|
|
62
|
+
IMAGES: "0"
|
|
63
|
+
NODES: "0"
|
|
64
|
+
PLUGINS: "0"
|
|
65
|
+
SECRETS: "0"
|
|
66
|
+
SERVICES: "0"
|
|
67
|
+
SESSION: "0"
|
|
68
|
+
SWARM: "0"
|
|
69
|
+
SYSTEM: "0"
|
|
70
|
+
TASKS: "0"
|
|
71
|
+
VOLUMES: "0"
|
|
72
|
+
LOG_LEVEL: warning
|
|
73
|
+
volumes:
|
|
74
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
75
|
+
networks:
|
|
76
|
+
- webcontrol
|
|
77
|
+
# No `ports:`. The proxy is reachable only from the panel.
|
|
78
|
+
tmpfs:
|
|
79
|
+
- /tmp
|
|
80
|
+
- /run
|
|
81
|
+
healthcheck:
|
|
82
|
+
test: ["CMD", "nc", "-z", "127.0.0.1", "2375"]
|
|
83
|
+
interval: 10s
|
|
84
|
+
timeout: 3s
|
|
85
|
+
retries: 5
|
|
86
|
+
start_period: 5s
|
|
87
|
+
labels:
|
|
88
|
+
traefik.enable: "false"
|
|
89
|
+
portta.managed: "true"
|
|
90
|
+
portta.component: web-socket-proxy
|
|
91
|
+
|
|
92
|
+
# --------------------------------------------------------------------------
|
|
93
|
+
# The panel: API and UI in one process
|
|
94
|
+
# --------------------------------------------------------------------------
|
|
95
|
+
web:
|
|
96
|
+
# A normal installation never builds: the panel is a published image, and
|
|
97
|
+
# PORTTA_HOME holds no application source to build from. Developers opt
|
|
98
|
+
# back into a local build with PORTTA_WEB_BUILD=true
|
|
99
|
+
# (docker/compose/features/web-build.yaml), and `portta web dev` still
|
|
100
|
+
# builds the dev target. PORTTA_VERSION comes from the generated runtime
|
|
101
|
+
# asset, so the installed CLI and panel select the same image.
|
|
102
|
+
image: ${PORTTA_WEB_IMAGE:-ghcr.io/fabioassuncao/portta:${PORTTA_VERSION}}
|
|
103
|
+
restart: unless-stopped
|
|
104
|
+
depends_on:
|
|
105
|
+
web-socket-proxy:
|
|
106
|
+
condition: service_healthy
|
|
107
|
+
# `node` is in both the runtime and the `dev` image; the Dockerfile's
|
|
108
|
+
# `wget` healthcheck only exists on the runtime stage. start_period covers
|
|
109
|
+
# the first `next.prepare()`; retries then fit `--wait-timeout 180`.
|
|
110
|
+
healthcheck:
|
|
111
|
+
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8081/api/health').then((r)=>process.exit(r.ok||r.status===401?0:1)).catch(()=>process.exit(1))"]
|
|
112
|
+
interval: 5s
|
|
113
|
+
timeout: 3s
|
|
114
|
+
retries: 24
|
|
115
|
+
start_period: 60s
|
|
116
|
+
# Whoever owns .env, which the Settings page writes. `bootstrap` and
|
|
117
|
+
# `web up` record it; the image's own `node` is only a last resort, and is
|
|
118
|
+
# right just when the host uid happens to be 1000.
|
|
119
|
+
user: ${PORTTA_WEB_USER:-node}
|
|
120
|
+
environment:
|
|
121
|
+
PORTTA_RUNTIME_DB_MODE: ${PORTTA_RUNTIME_DB_MODE:-managed}
|
|
122
|
+
PORTTA_RUNTIME_DB_NAME: ${PORTTA_RUNTIME_DB_NAME:-portta}
|
|
123
|
+
PORTTA_RUNTIME_DB_USER: ${PORTTA_RUNTIME_DB_USER:-portta}
|
|
124
|
+
PORTTA_RUNTIME_DB_PASSWORD: ${PORTTA_RUNTIME_DB_PASSWORD:-}
|
|
125
|
+
PORTTA_RUNTIME_DATABASE_URL: ${PORTTA_RUNTIME_DATABASE_URL:-}
|
|
126
|
+
PORTTA_AUTH_SIGNIN_ATTEMPTS: ${PORTTA_AUTH_SIGNIN_ATTEMPTS:-}
|
|
127
|
+
ACME_CHALLENGE: ${ACME_CHALLENGE:-dns}
|
|
128
|
+
ACME_DNS_RESOLVERS: ${ACME_DNS_RESOLVERS:-}
|
|
129
|
+
CLOUDFLARE_TUNNEL_ENABLED: ${CLOUDFLARE_TUNNEL_ENABLED:-false}
|
|
130
|
+
CLOUDFLARE_TUNNEL_ZONE: ${CLOUDFLARE_TUNNEL_ZONE:-}
|
|
131
|
+
PORTTA_RUNTIME_DOCKER_API: http://web-socket-proxy:2375
|
|
132
|
+
PORTTA_RUNTIME_HOST: 0.0.0.0
|
|
133
|
+
PORTTA_RUNTIME_PORT: "8081"
|
|
134
|
+
PORTTA_RUNTIME_ENV_FILE: /app/state/.env
|
|
135
|
+
PORTTA_RUNTIME_READ_ONLY: ${PORTTA_WEB_READ_ONLY:-false}
|
|
136
|
+
# The machine contract is always served; its browser defaults on only
|
|
137
|
+
# while the panel stays on loopback.
|
|
138
|
+
PORTTA_RUNTIME_DOCS: ${PORTTA_RUNTIME_DOCS:-true}
|
|
139
|
+
PORTTA_RUNTIME_API_DOCS: ${PORTTA_RUNTIME_API_DOCS:-}
|
|
140
|
+
# The resolved gateway configuration, so the panel and the CLI always
|
|
141
|
+
# describe the same gateway.
|
|
142
|
+
PORTTA_PROFILE: ${PORTTA_PROFILE:-local}
|
|
143
|
+
# Projects Home as a string, for location classification only: no
|
|
144
|
+
# project directory is ever mounted here (docs/development/adr/0031-projects-home-and-project.md).
|
|
145
|
+
PORTTA_PROJECTS_HOME: ${PORTTA_PROJECTS_HOME:-}
|
|
146
|
+
PORTTA_PROJECT_NAME: ${PORTTA_PROJECT_NAME:-portta}
|
|
147
|
+
PORTTA_NETWORK: ${PORTTA_NETWORK:-portta}
|
|
148
|
+
PORTTA_CONTROL_NETWORK: ${PORTTA_CONTROL_NETWORK:-portta-control}
|
|
149
|
+
PORTTA_ACCESS_NETWORK: ${PORTTA_ACCESS_NETWORK:-portta-access}
|
|
150
|
+
PORTTA_WEB_NETWORK: ${PORTTA_WEB_NETWORK:-portta-web}
|
|
151
|
+
PORTTA_TCP: ${PORTTA_TCP:-false}
|
|
152
|
+
PORTTA_TCP_POSTGRES_PORT: ${PORTTA_TCP_POSTGRES_PORT:-5432}
|
|
153
|
+
PORTTA_TCP_REDIS_PORT: ${PORTTA_TCP_REDIS_PORT:-6379}
|
|
154
|
+
PORTTA_DOMAIN: ${PORTTA_DOMAIN:-localhost}
|
|
155
|
+
# How that base was chosen, so the Settings page can show the mode and
|
|
156
|
+
# the address behind it rather than a bare hostname with no explanation.
|
|
157
|
+
# See docs/development/adr/0022-project-domain-modes.md.
|
|
158
|
+
PORTTA_DOMAIN_MODE: ${PORTTA_DOMAIN_MODE:-local}
|
|
159
|
+
PORTTA_PUBLIC_IP: ${PORTTA_PUBLIC_IP:-}
|
|
160
|
+
PORTTA_AUTO_DOMAIN_PROVIDER: ${PORTTA_AUTO_DOMAIN_PROVIDER:-sslip.io}
|
|
161
|
+
PORTTA_BIND_ADDRESS: ${PORTTA_BIND_ADDRESS:-127.0.0.1}
|
|
162
|
+
PORTTA_HTTP_PORT: ${PORTTA_HTTP_PORT:-80}
|
|
163
|
+
PORTTA_HTTPS_PORT: ${PORTTA_HTTPS_PORT:-443}
|
|
164
|
+
PORTTA_DASHBOARD: ${PORTTA_DASHBOARD:-false}
|
|
165
|
+
PORTTA_DASHBOARD_BIND_ADDRESS: ${PORTTA_DASHBOARD_BIND_ADDRESS:-127.0.0.1}
|
|
166
|
+
PORTTA_DASHBOARD_PORT: ${PORTTA_DASHBOARD_PORT:-8080}
|
|
167
|
+
# Traefik's own API, read-only, and only when the dashboard is enabled.
|
|
168
|
+
# The panel resolves the host from the attachment (`traefik` normally,
|
|
169
|
+
# `tailscale` when Traefik shares that namespace) and reaches it over the
|
|
170
|
+
# shared network it is already on, never over `control`: see
|
|
171
|
+
# docs/development/adr/0011-bounded-traefik-write-surface.md.
|
|
172
|
+
PORTTA_RUNTIME_TRAEFIK_API: ${PORTTA_RUNTIME_TRAEFIK_API:-}
|
|
173
|
+
PORTTA_LOG_LEVEL: ${PORTTA_LOG_LEVEL:-INFO}
|
|
174
|
+
PORTTA_ACCESS_LOG: ${PORTTA_ACCESS_LOG:-false}
|
|
175
|
+
PORTTA_WEB_PORT: ${PORTTA_WEB_PORT:-8081}
|
|
176
|
+
PORTTA_WEB_BIND_ADDRESS: ${PORTTA_WEB_BIND_ADDRESS:-127.0.0.1}
|
|
177
|
+
PORTTA_WEB_EXPOSE: ${PORTTA_WEB_EXPOSE:-local}
|
|
178
|
+
# The panel's own front door, which is now the panel's own business.
|
|
179
|
+
#
|
|
180
|
+
# `required` makes it sign people in: accounts, roles, sessions and
|
|
181
|
+
# tokens, all in its database. `disabled` makes every request the local
|
|
182
|
+
# operator, and the process refuses to start that way on anything but
|
|
183
|
+
# loopback. The secret signs sessions and is shared with the ForwardAuth
|
|
184
|
+
# service, which is the only other thing here that signs anything.
|
|
185
|
+
#
|
|
186
|
+
# PORTTA_PANEL_URL is where a browser reaches it, and three things come
|
|
187
|
+
# from it: whether the session cookie may be `Secure`, which origins a
|
|
188
|
+
# write is accepted from, and the address the panel prints. Empty means
|
|
189
|
+
# loopback on the published port.
|
|
190
|
+
PORTTA_AUTH_MODE: ${PORTTA_AUTH_MODE:-disabled}
|
|
191
|
+
PORTTA_AUTH_SECRET: ${PORTTA_AUTH_SECRET:-}
|
|
192
|
+
PORTTA_PANEL_URL: ${PORTTA_PANEL_URL:-}
|
|
193
|
+
PORTTA_PANEL_TRUSTED_ORIGINS: ${PORTTA_PANEL_TRUSTED_ORIGINS:-}
|
|
194
|
+
PORTTA_RUNTIME_DYNAMIC_DIR: /app/state/traefik-dynamic
|
|
195
|
+
PORTTA_RUNTIME_AUTH_STORE: /app/state/auth/protections.json
|
|
196
|
+
PORTTA_RUNTIME_RUNNER_DIR: /app/state/runner
|
|
197
|
+
PORTTA_RUNTIME_ACCESS_DIR: /app/state/access
|
|
198
|
+
PORTTA_RUNTIME_SSH_DIR: /app/state/ssh
|
|
199
|
+
PORTTA_PANEL_ADVERTISED_HOST: ${PORTTA_PANEL_ADVERTISED_HOST:-}
|
|
200
|
+
TLS_ENABLED: ${TLS_ENABLED:-false}
|
|
201
|
+
TLS_MODE: ${TLS_MODE:-local}
|
|
202
|
+
ACME_EMAIL: ${ACME_EMAIL:-}
|
|
203
|
+
ACME_CA_SERVER: ${ACME_CA_SERVER:-https://acme-v02.api.letsencrypt.org/directory}
|
|
204
|
+
ACME_DNS_PROVIDER: ${ACME_DNS_PROVIDER:-cloudflare}
|
|
205
|
+
TAILSCALE_ENABLED: ${TAILSCALE_ENABLED:-false}
|
|
206
|
+
TAILSCALE_HOSTNAME: ${TAILSCALE_HOSTNAME:-portta}
|
|
207
|
+
PRIVATE_DOMAIN: ${PRIVATE_DOMAIN:-}
|
|
208
|
+
PUBLIC_ENABLED: ${PUBLIC_ENABLED:-false}
|
|
209
|
+
PUBLIC_DOMAIN: ${PUBLIC_DOMAIN:-}
|
|
210
|
+
CLOUDFLARE_ENABLED: ${CLOUDFLARE_ENABLED:-false}
|
|
211
|
+
CLOUDFLARE_ZONE: ${CLOUDFLARE_ZONE:-}
|
|
212
|
+
# The panel's only outbound integration, and its only long-lived secret
|
|
213
|
+
# that is not a Traefik credential. Off by default: with this false the
|
|
214
|
+
# panel makes no request to github.com. The private key is passed as a
|
|
215
|
+
# PATH, never as a value, because the panel can write this .env and must
|
|
216
|
+
# not be able to write the key that authenticates it.
|
|
217
|
+
#
|
|
218
|
+
# The path is the operator's: the Settings field writes it here, and this
|
|
219
|
+
# is the value the panel reads. What is fixed is the DIRECTORY, because
|
|
220
|
+
# `./state/github` below is the only place the key is mounted from; the
|
|
221
|
+
# filename is whatever GitHub called the .pem. The panel refuses a path
|
|
222
|
+
# outside that directory on save, and `portta doctor` fails on one.
|
|
223
|
+
# See docs/product/guides/github.md and docs/product/concepts/security.md.
|
|
224
|
+
GITHUB_APP_ENABLED: ${GITHUB_APP_ENABLED:-false}
|
|
225
|
+
GITHUB_APP_ID: ${GITHUB_APP_ID:-}
|
|
226
|
+
GITHUB_APP_PRIVATE_KEY_FILE: ${GITHUB_APP_PRIVATE_KEY_FILE:-/app/state/github/app.pem}
|
|
227
|
+
GITHUB_APP_WEBHOOK_SECRET: ${GITHUB_APP_WEBHOOK_SECRET:-}
|
|
228
|
+
GITHUB_API_URL: ${GITHUB_API_URL:-https://api.github.com}
|
|
229
|
+
GITHUB_SYNC_INTERVAL_MINUTES: ${GITHUB_SYNC_INTERVAL_MINUTES:-15}
|
|
230
|
+
volumes:
|
|
231
|
+
# Explicit paths from the host, and nothing else. No project directory is ever
|
|
232
|
+
# mounted here: see docs/development/adr/0001-decoupled-infrastructure.md.
|
|
233
|
+
#
|
|
234
|
+
# .env the configuration the Settings page edits
|
|
235
|
+
# state/git metadata `portta repos scan` collected, read-only
|
|
236
|
+
# state/metrics current.json + history from `portta host watch`
|
|
237
|
+
# state/environment host readiness report from the same collector
|
|
238
|
+
# state/runner the `{ verb, project }` request the runner reads
|
|
239
|
+
# state/access tunnel records cleaned when a project is removed
|
|
240
|
+
# state/github the GitHub App private key, read-only
|
|
241
|
+
# state/auth credentials the panel manages, never served directly
|
|
242
|
+
# state/ssh SSH credentials the panel manages, never served directly
|
|
243
|
+
# traefik/dynamic read-write, and the ONE place the panel configures
|
|
244
|
+
# Traefik. It may write four generated filenames there
|
|
245
|
+
# and refuses every other path in its own process.
|
|
246
|
+
# See docs/development/adr/0011-bounded-traefik-write-surface.md.
|
|
247
|
+
- ./.env:/app/state/.env
|
|
248
|
+
- ./.env.example:/app/state/.env.example:ro
|
|
249
|
+
- ./.env-lock:/app/state/.env-lock
|
|
250
|
+
- ./state/git:/app/state/git:ro
|
|
251
|
+
- ./state/metrics:/app/state/metrics:ro
|
|
252
|
+
- ./state/environment:/app/state/environment:ro
|
|
253
|
+
- ./state/runner:/app/state/runner
|
|
254
|
+
- ./state/access:/app/state/access
|
|
255
|
+
# The GitHub App private key, read-only. Empty unless you put one there.
|
|
256
|
+
- ./state/github:/app/state/github:ro
|
|
257
|
+
- ./state/auth:/app/state/auth
|
|
258
|
+
- ./state/ssh:/app/state/ssh
|
|
259
|
+
- ./config/traefik/dynamic:/app/state/traefik-dynamic
|
|
260
|
+
# The Cloudflare Tunnel connector's generated config and its credential.
|
|
261
|
+
# Read-write because setting the tunnel up is something the panel is asked
|
|
262
|
+
# to do; the directory stays 0700 and the credentials file 0600, and the
|
|
263
|
+
# token itself is never stored anywhere else — not in .env, not in a log,
|
|
264
|
+
# not in any API response. See docs/development/adr/0025-cloudflare-tunnel.md.
|
|
265
|
+
- ./state/cloudflared:/app/state/cloudflared
|
|
266
|
+
networks:
|
|
267
|
+
# `gateway` so the panel can be published and, with the VPN overlay,
|
|
268
|
+
# routed by Traefik. `webcontrol` for the socket proxy.
|
|
269
|
+
- gateway
|
|
270
|
+
- webcontrol
|
|
271
|
+
# Deliberately no `ports:` here. Exactly one overlay decides how the panel
|
|
272
|
+
# is reached, so the two ways can never both bind the same host port:
|
|
273
|
+
# docker/compose/features/web-bind.yaml publishes it on PORTTA_WEB_BIND_ADDRESS
|
|
274
|
+
# docker/compose/features/panel-public.yaml routes it through Traefik's
|
|
275
|
+
# own `panel` entrypoint
|
|
276
|
+
# docker/compose/features/panel-domain.yaml routes it on one hostname of
|
|
277
|
+
# the gateway's own domain
|
|
278
|
+
labels:
|
|
279
|
+
# Off unless docker/compose/features/web-vpn.yaml turns it on: the panel is
|
|
280
|
+
# never routed through the public entrypoints by accident.
|
|
281
|
+
traefik.enable: "false"
|
|
282
|
+
portta.managed: "true"
|
|
283
|
+
portta.component: web
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# `local` profile with HTTPS from a local CA
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied automatically when TLS_ENABLED=true and TLS_MODE=local.
|
|
5
|
+
# The certificate itself is generated by `portta tls init` and offered to
|
|
6
|
+
# Traefik through the file provider (config/traefik/dynamic/local-tls.yaml).
|
|
7
|
+
# ============================================================================
|
|
8
|
+
|
|
9
|
+
services:
|
|
10
|
+
traefik:
|
|
11
|
+
environment:
|
|
12
|
+
# Turn TLS on for the entrypoint itself, so a project gets HTTPS without
|
|
13
|
+
# a single certificate label. Without this, routers attached to :443
|
|
14
|
+
# would answer there in plaintext.
|
|
15
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS: "true"
|
|
16
|
+
# Routers now default to :443, and :80 redirects there.
|
|
17
|
+
TRAEFIK_ENTRYPOINTS_WEB_ASDEFAULT: "false"
|
|
18
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_ASDEFAULT: "true"
|
|
19
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO: websecure
|
|
20
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_SCHEME: https
|
|
21
|
+
# Not permanent: a 301 would be cached by the browser and would outlive
|
|
22
|
+
# someone turning local TLS back off.
|
|
23
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_PERMANENT: "false"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta: `local` profile overlay (macOS / Linux workstation)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Plain HTTP on loopback, no TLS, no certificates to trust. Hostnames end in
|
|
5
|
+
# `.localhost`, which resolves to loopback without touching /etc/hosts.
|
|
6
|
+
#
|
|
7
|
+
# Applied automatically by: ./bin/portta up local
|
|
8
|
+
# ============================================================================
|
|
9
|
+
|
|
10
|
+
services:
|
|
11
|
+
traefik:
|
|
12
|
+
environment:
|
|
13
|
+
# With no TLS configured, routers that do not name an entrypoint should
|
|
14
|
+
# attach to :80 only: otherwise they would also answer plaintext on :443.
|
|
15
|
+
TRAEFIK_ENTRYPOINTS_WEB_ASDEFAULT: "true"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# `remote-public` profile: the internet can reach this
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Opt-in, and deliberately never the default. Enabling it only changes who can
|
|
5
|
+
# reach Traefik; it does not publish anything new. A service is still routed
|
|
6
|
+
# only when it sets traefik.enable=true, and databases and caches are still
|
|
7
|
+
# never on the shared network.
|
|
8
|
+
#
|
|
9
|
+
# PORTTA_BIND_ADDRESS is forced to 0.0.0.0 by the CLI for this profile.
|
|
10
|
+
# ============================================================================
|
|
11
|
+
|
|
12
|
+
services:
|
|
13
|
+
traefik:
|
|
14
|
+
environment:
|
|
15
|
+
# Facing the open internet, header aliasing stops being theoretical: a
|
|
16
|
+
# client can spoof a header Traefik manages if a backend normalises
|
|
17
|
+
# underscores. Strip them here rather than trusting every backend.
|
|
18
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_ALIASHEADERSSTRATEGY: ${PORTTA_ALIAS_HEADERS_STRATEGY:-delete}
|
|
19
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ALIASHEADERSSTRATEGY: ${PORTTA_ALIAS_HEADERS_STRATEGY:-delete}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# ACME over DNS-01: one wildcard certificate for the whole domain
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# The default, and the only challenge that can issue `*.example.com`. It is
|
|
5
|
+
# what makes a hostname work over HTTPS *before* anything is running on it,
|
|
6
|
+
# and the only option for a private gateway the ACME server can never reach:
|
|
7
|
+
# Let's Encrypt has to see a TXT record, not this host.
|
|
8
|
+
#
|
|
9
|
+
# The cost is a DNS provider credential. `remote-tls-http.yaml` is the trade
|
|
10
|
+
# in the other direction. See docs/product/guides/dns-and-tls.md.
|
|
11
|
+
# ============================================================================
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
traefik:
|
|
15
|
+
environment:
|
|
16
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS_DOMAINS_0_MAIN: ${PORTTA_DOMAIN}
|
|
17
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS_DOMAINS_0_SANS: "*.${PORTTA_DOMAIN}"
|
|
18
|
+
|
|
19
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE: "true"
|
|
20
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE_PROVIDER: ${ACME_DNS_PROVIDER:-cloudflare}
|
|
21
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE_RESOLVERS: ${ACME_DNS_RESOLVERS:-1.1.1.1:53,8.8.8.8:53}
|
|
22
|
+
|
|
23
|
+
# -- DNS provider credentials ----------------------------------------
|
|
24
|
+
# Read by lego inside Traefik. Empty unless the provider is configured;
|
|
25
|
+
# a scoped token, never a global API key. See docs/product/guides/cloudflare.md.
|
|
26
|
+
CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN:-}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# ACME over HTTP-01: one certificate per hostname, and no credential
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# What a public gateway on a public IP can do with nothing but an A record and
|
|
5
|
+
# a wildcard A record. Traefik asks for a certificate the first time a router
|
|
6
|
+
# is created for a hostname, Let's Encrypt fetches a token from this host over
|
|
7
|
+
# :80, and the certificate arrives a second or two later.
|
|
8
|
+
#
|
|
9
|
+
# No entrypoint-level `domains` here, deliberately: a wildcard SAN cannot be
|
|
10
|
+
# issued this way, and asking for one makes every issuance fail. The router's
|
|
11
|
+
# own rule is what names the certificate.
|
|
12
|
+
#
|
|
13
|
+
# What this gives up against DNS-01:
|
|
14
|
+
#
|
|
15
|
+
# - :80 must be reachable from the internet. A private or VPN-only gateway
|
|
16
|
+
# cannot use this at all.
|
|
17
|
+
# - A hostname has no certificate until something answers on it, so the very
|
|
18
|
+
# first request to a new service waits for issuance.
|
|
19
|
+
# - Let's Encrypt counts certificates per registered domain, not per name.
|
|
20
|
+
# A gateway that churns through hostnames will meet the weekly limit that
|
|
21
|
+
# one wildcard never would.
|
|
22
|
+
#
|
|
23
|
+
# Traefik installs the challenge router ahead of the :80 -> :443 redirection
|
|
24
|
+
# above, so the redirect does not swallow the challenge.
|
|
25
|
+
# See docs/product/guides/dns-and-tls.md.
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
services:
|
|
29
|
+
traefik:
|
|
30
|
+
environment:
|
|
31
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE: "true"
|
|
32
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Remote profiles with TLS: the parts both ACME challenges share
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied on a remote profile when TLS_ENABLED=true, together with exactly one
|
|
5
|
+
# challenge overlay: remote-tls-dns.yaml or remote-tls-http.yaml.
|
|
6
|
+
#
|
|
7
|
+
# Deliberately not applied when TLS is off. Redirecting to an entrypoint with
|
|
8
|
+
# no usable certificate turns a working HTTP URL into a browser warning, which
|
|
9
|
+
# is exactly what a host on an auto domain has: no public CA issues a wildcard
|
|
10
|
+
# for sslip.io, and neither service offers the DNS-01 API one would need.
|
|
11
|
+
# docker/compose/profiles/remote.yaml serves those over plain HTTP instead.
|
|
12
|
+
# See docs/development/adr/0022-project-domain-modes.md.
|
|
13
|
+
# ============================================================================
|
|
14
|
+
|
|
15
|
+
services:
|
|
16
|
+
traefik:
|
|
17
|
+
environment:
|
|
18
|
+
# Routers that name no entrypoint attach to :443 only; :80 just redirects.
|
|
19
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_ASDEFAULT: "true"
|
|
20
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO: websecure
|
|
21
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_SCHEME: https
|
|
22
|
+
TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_PERMANENT: "true"
|
|
23
|
+
|
|
24
|
+
# TLS terminates at the entrypoint, so a project gets HTTPS without a
|
|
25
|
+
# single certificate label of its own.
|
|
26
|
+
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS_CERTRESOLVER: letsencrypt
|
|
27
|
+
|
|
28
|
+
# -- ACME ------------------------------------------------------------
|
|
29
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL: ${ACME_EMAIL:-}
|
|
30
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_CASERVER: ${ACME_CA_SERVER:-https://acme-v02.api.letsencrypt.org/directory}
|
|
31
|
+
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: /etc/traefik/acme/acme.json
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Remote profiles without TLS: plain HTTP on :80
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Applied on a remote profile when TLS_ENABLED=false, which is the default and
|
|
5
|
+
# the only thing that works on an automatic domain: no public certificate
|
|
6
|
+
# authority issues a wildcard for sslip.io or nip.io, and neither offers the
|
|
7
|
+
# DNS-01 API that a wildcard needs.
|
|
8
|
+
#
|
|
9
|
+
# The point is what this does NOT do. The TLS overlay redirects :80 to :443
|
|
10
|
+
# permanently, and doing that without a certificate the browser accepts turns a
|
|
11
|
+
# URL that works into a warning page — the opposite of the goal. So a host with
|
|
12
|
+
# no TLS answers on :80 and says so.
|
|
13
|
+
#
|
|
14
|
+
# docker/compose/profiles/remote-tls.yaml replaces this once TLS is configured.
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
services:
|
|
18
|
+
traefik:
|
|
19
|
+
environment:
|
|
20
|
+
# Routers that name no entrypoint attach to :80 only, exactly as they do
|
|
21
|
+
# on the local profile. Nothing answers on :443, rather than answering
|
|
22
|
+
# with a certificate nobody can verify.
|
|
23
|
+
TRAEFIK_ENTRYPOINTS_WEB_ASDEFAULT: "true"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Operational images
|
|
2
|
+
|
|
3
|
+
These directories are self-contained build contexts for images used by the
|
|
4
|
+
Portta runtime rather than by a Compose service directly:
|
|
5
|
+
|
|
6
|
+
- `apply/` builds the opt-in applier that recreates the gateway after settings
|
|
7
|
+
change.
|
|
8
|
+
- `toolbox/` builds the pinned diagnostic and database-client toolbox.
|
|
9
|
+
|
|
10
|
+
Application images stay with their applications. In particular,
|
|
11
|
+
`apps/web/Dockerfile` remains under `apps/web` because it builds the web and
|
|
12
|
+
authentication workspaces from the repository-root context.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta applier
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# The one image that may drive Compose on the host, so that a settings change
|
|
5
|
+
# saved in the panel can be applied without a terminal.
|
|
6
|
+
#
|
|
7
|
+
# It exists because Traefik reads its static configuration from the environment
|
|
8
|
+
# its container was created with (docs/development/adr/0003-traefik-static-config-via-env.md):
|
|
9
|
+
# a saved setting needs the containers *recreated*, and recreating them means
|
|
10
|
+
# Compose, which the panel deliberately cannot reach
|
|
11
|
+
# (docs/development/adr/0008-web-panel-socket-proxy.md).
|
|
12
|
+
#
|
|
13
|
+
# Built on demand and cached locally, like the toolbox; never published. Off
|
|
14
|
+
# unless PORTTA_APPLY=true. See docs/development/adr/0026-applying-settings-from-the-panel.md.
|
|
15
|
+
#
|
|
16
|
+
# Node runs the packaged Portta CLI; Bash remains only for the closed project
|
|
17
|
+
# runner command.
|
|
18
|
+
#
|
|
19
|
+
# buildx, because `up` passes --build on a repository checkout. Nothing is built
|
|
20
|
+
# in here: the context is streamed over the mounted socket and the host daemon
|
|
21
|
+
# does the work, with the host's network and layer cache. The plugin is only how
|
|
22
|
+
# the compose CLI reaches that daemon, which is why --network none is no
|
|
23
|
+
# obstacle -- and why this image needs no toolchain of its own.
|
|
24
|
+
# ============================================================================
|
|
25
|
+
FROM alpine:3.24.1
|
|
26
|
+
|
|
27
|
+
ARG PORTTA_VERSION
|
|
28
|
+
|
|
29
|
+
# Pinned base, unpinned packages: Alpine's repositories only carry the current
|
|
30
|
+
# patch level for a release, so `apk add` inside 3.24.1 is already constrained.
|
|
31
|
+
RUN apk add --no-cache \
|
|
32
|
+
bash \
|
|
33
|
+
nodejs \
|
|
34
|
+
docker-cli \
|
|
35
|
+
docker-cli-buildx \
|
|
36
|
+
docker-cli-compose
|
|
37
|
+
|
|
38
|
+
LABEL org.opencontainers.image.title="portta-apply" \
|
|
39
|
+
org.opencontainers.image.description="Recreates Portta's own containers on the host" \
|
|
40
|
+
org.opencontainers.image.version="$PORTTA_VERSION" \
|
|
41
|
+
org.opencontainers.image.source="https://github.com/fabioassuncao/portta" \
|
|
42
|
+
org.opencontainers.image.licenses="MIT"
|
|
43
|
+
|
|
44
|
+
ENTRYPOINT []
|
|
45
|
+
CMD ["/bin/bash"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Portta toolbox
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# One small image holding the diagnostic and client tools the gateway needs, so
|
|
5
|
+
# a host only ever has to provide Docker, Git and a shell.
|
|
6
|
+
#
|
|
7
|
+
# Built on demand and cached locally; see `portta toolbox build`.
|
|
8
|
+
# ============================================================================
|
|
9
|
+
FROM alpine:3.24.1
|
|
10
|
+
|
|
11
|
+
ARG PORTTA_VERSION
|
|
12
|
+
|
|
13
|
+
# Pinned base, unpinned packages: Alpine's repositories only carry the current
|
|
14
|
+
# patch level for a release, so `apk add` inside 3.24.1 is already constrained.
|
|
15
|
+
RUN apk add --no-cache \
|
|
16
|
+
bash \
|
|
17
|
+
ca-certificates \
|
|
18
|
+
curl \
|
|
19
|
+
jq \
|
|
20
|
+
bind-tools \
|
|
21
|
+
openssl \
|
|
22
|
+
socat \
|
|
23
|
+
netcat-openbsd \
|
|
24
|
+
openssh-client \
|
|
25
|
+
postgresql-client \
|
|
26
|
+
mysql-client \
|
|
27
|
+
redis \
|
|
28
|
+
iputils \
|
|
29
|
+
&& update-ca-certificates
|
|
30
|
+
|
|
31
|
+
LABEL org.opencontainers.image.title="portta-toolbox" \
|
|
32
|
+
org.opencontainers.image.description="Diagnostic and client tools for Portta" \
|
|
33
|
+
org.opencontainers.image.version="$PORTTA_VERSION" \
|
|
34
|
+
org.opencontainers.image.source="https://github.com/fabioassuncao/portta" \
|
|
35
|
+
org.opencontainers.image.licenses="MIT"
|
|
36
|
+
|
|
37
|
+
ENTRYPOINT []
|
|
38
|
+
CMD ["/bin/bash"]
|